Compare commits

..

1 Commits

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,4 +1,4 @@
name: Server Tests name: Run tests
on: on:
push: push:
branches: branches:
@@ -36,7 +36,7 @@ jobs:
- 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@v2 uses: actions/cache@v2
@@ -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

View File

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

2
.gitignore vendored
View File

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

View File

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

View File

@@ -1,36 +1,17 @@
<p align="center"> <p align="center">
<a href="https://www.frappelms.com/"> <a href="https://www.frappelms.com/">
<img src="https://frappelms.com/files/lms-logo-medium.png" alt="Frappe LMS" width="120px" height="25px"> <img src="https://www.frappelms.com/files/flms.svg" alt="Frappe LMS" width="100" height="100">
</a> </a>
<p align="center">Easy to use, open source, learning management system.</p> <p align="center">Easy to use, open source, learning management system.</p>
</p> </p>
&nbsp;
<p align="center"> <p align="center">
<a href="https://www.producthunt.com/posts/frappe-lms?utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-frappe&#0045;lms" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=396079&theme=dark&period=weekly&topic_id=204" alt="Frappe&#0032;LMS - Easy&#0032;to&#0032;use&#0044;&#0032;100&#0037;&#0032;open&#0032;source&#0032;learning&#0032;management&#0032;system | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
<div align="center" style="max-height: 40px;">
<a href="https://frappecloud.com/lms/signup">
<img src=".github/try-on-f-cloud.svg" height="40">
</a>
</div>
&nbsp;
<p align="center">
<a href="https://dashboard.cypress.io/projects/vandxn/runs">
<img alt="cypress" src="https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/vandxn/main&style=flat&logo=cypress">
</a>
<a href="https://github.com/frappe/lms/blob/main/LICENSE"> <a href="https://github.com/frappe/lms/blob/main/LICENSE">
<img alt="license" src="https://img.shields.io/badge/license-AGPLv3-blue"> <img alt="license" src="https://img.shields.io/badge/license-AGPLv3-blue">
</a> </a>
</p> </p>
<img width="1402" alt="Lesson" src="https://frappelms.com/files/banner.png"> <img width="1402" alt="Lesson" src="https://frappelms.com/files/fs-banner71f330.png">
<details> <details>
<summary>Show more screenshots</summary> <summary>Show more screenshots</summary>
@@ -48,7 +29,7 @@ You can create courses and lessons through simple forms. Lessons can be in the f
- Add detailed descriptions and preview videos to the course. 🎬 - Add detailed descriptions and preview videos to the course. 🎬
- Add videos, quizzes, and assignments to your lessons and make them interesting and interactive 📝 - Add videos, quizzes, and assignments to your lessons and make them interesting and interactive 📝
- Discussions section below each lesson where instructors and students can interact with each other. 💬 - Discussions section below each lesson where instructors and students can interact with each other. 💬
- Create batches to group your students based on courses and track their progress 🏛 - Create classes to group your students based on courses and track their progress 🏛
- Statistics dashboard that provides all important numbers at a glimpse. 📈 - Statistics dashboard that provides all important numbers at a glimpse. 📈
- Job Board where users can post and look for jobs. 💼 - Job Board where users can post and look for jobs. 💼
- People directory with each person's profile page 👨‍👩‍👧‍👦 - People directory with each person's profile page 👨‍👩‍👧‍👦
@@ -71,7 +52,7 @@ These are some of the tools it's built on:
You need Docker, docker-compose, and git setup on your machine. Refer to [Docker documentation](https://docs.docker.com/). After that, run the following commands: You need Docker, docker-compose, and git setup on your machine. Refer to [Docker documentation](https://docs.docker.com/). After that, run the following commands:
``` ```
git clone https://github.com/frappe/lms git clone https://github.com/frappe/lms
cd apps/lms/docker cd lms/docker
docker-compose up docker-compose up
``` ```
@@ -90,7 +71,7 @@ Currently, this app depends on the `develop` branch of [frappe](https://github.c
bench --site lms.test install-app lms bench --site lms.test install-app lms
bench --site lms.test add-to-hosts bench --site lms.test add-to-hosts
1. Now, you can access the site at `http://lms.test:8000` 1. Now, you can access the site at `http://gameplan.test:8080`
## Deployment ## Deployment

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,8 +8,6 @@ else
echo "Creating new bench..." echo "Creating new bench..."
fi fi
export PATH="${NVM_DIR}/versions/node/v${NODE_VERSION_DEVELOP}/bin/:${PATH}"
bench init --skip-redis-config-generation frappe-bench bench init --skip-redis-config-generation frappe-bench
cd frappe-bench cd frappe-bench
@@ -38,4 +36,4 @@ bench --site lms.localhost clear-cache
bench --site lms.localhost set-config mute_emails 1 bench --site lms.localhost set-config mute_emails 1
bench use lms.localhost bench use lms.localhost
bench start bench start

View File

@@ -1 +1 @@
__version__ = "1.0.0" __version__ = "0.0.1"

View File

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

View File

@@ -60,7 +60,7 @@ web_include_js = ["website.bundle.js"]
# before_install = "lms.install.before_install" # before_install = "lms.install.before_install"
after_install = "lms.install.after_install" after_install = "lms.install.after_install"
after_sync = "lms.install.after_sync" after_sync = "lms.install.after_sync"
before_uninstall = "lms.install.before_uninstall" after_uninstall = "lms.install.after_uninstall"
setup_wizard_requires = "assets/lms/js/setup_wizard.js" setup_wizard_requires = "assets/lms/js/setup_wizard.js"
@@ -103,11 +103,11 @@ doc_events = {
# Scheduled Tasks # Scheduled Tasks
# --------------- # ---------------
scheduler_events = { # scheduler_events = {
"hourly": [ # "daily": [
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals" # "erpnext.stock.reorder_item.reorder_item"
] # ]
} # }
fixtures = ["Custom Field", "Function", "Industry"] fixtures = ["Custom Field", "Function", "Industry"]
@@ -138,21 +138,15 @@ fixtures = ["Custom Field", "Function", "Industry"]
website_route_rules = [ website_route_rules = [
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"}, {"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
{"from_route": "/courses/<course>", "to_route": "courses/course"}, {"from_route": "/courses/<course>", "to_route": "courses/course"},
{"from_route": "/courses/<course>/edit", "to_route": "courses/create"},
{"from_route": "/courses/<course>/outline", "to_route": "courses/outline"},
{"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"}, {"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"}, {"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{ {
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>",
"to_route": "batch/learn", "to_route": "batch/learn",
}, },
{
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>/edit",
"to_route": "batch/edit",
},
{"from_route": "/quizzes", "to_route": "batch/quiz_list"}, {"from_route": "/quizzes", "to_route": "batch/quiz_list"},
{"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"}, {"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"},
{"from_route": "/batches/<batchname>", "to_route": "batches/batch"}, {"from_route": "/classes/<classname>", "to_route": "classes/class"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"}, {"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"}, {"from_route": "/courses/<course>/join", "to_route": "batch/join"},
{"from_route": "/courses/<course>/manage", "to_route": "cohorts"}, {"from_route": "/courses/<course>/manage", "to_route": "cohorts"},
@@ -176,36 +170,15 @@ website_route_rules = [
{"from_route": "/users", "to_route": "profiles/profile"}, {"from_route": "/users", "to_route": "profiles/profile"},
{"from_route": "/jobs/<job>", "to_route": "jobs/job"}, {"from_route": "/jobs/<job>", "to_route": "jobs/job"},
{ {
"from_route": "/batches/<batchname>/students/<username>", "from_route": "/classes/<classname>/students/<username>",
"to_route": "/batches/progress", "to_route": "/classes/progress",
}, },
{"from_route": "/assignments/<assignment>", "to_route": "assignments/assignment"}, {"from_route": "/assignments/<assignment>", "to_route": "assignments/assignment"},
{
"from_route": "/assignment-submission/<assignment>/<submission>",
"to_route": "assignment_submission/assignment_submission",
},
{
"from_route": "/quiz-submission/<quiz>/<submission>",
"to_route": "quiz_submission/quiz_submission",
},
{
"from_route": "/billing/<module>/<modulename>",
"to_route": "billing/billing",
},
{
"from_route": "/batches/details/<batchname>",
"to_route": "batches/batch_details",
},
{
"from_route": "/certified-participants",
"to_route": "certified_participants/certified_participants",
},
] ]
website_redirects = [ website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"}, {"source": "/update-profile", "target": "/edit-profile"},
{"source": "/dashboard", "target": "/courses"}, {"source": "/dashboard", "target": "/courses"},
{"source": "/community", "target": "/people"},
] ]
update_website_context = [ update_website_context = [
@@ -255,9 +228,6 @@ jinja = {
"lms.lms.utils.get_filtered_membership", "lms.lms.utils.get_filtered_membership",
"lms.lms.utils.show_start_learing_cta", "lms.lms.utils.show_start_learing_cta",
"lms.lms.utils.can_create_courses", "lms.lms.utils.can_create_courses",
"lms.lms.utils.get_telemetry_boot_info",
"lms.lms.utils.is_onboarding_complete",
"lms.www.utils.is_student",
], ],
"filters": [], "filters": [],
} }
@@ -304,9 +274,6 @@ lms_markdown_macro_renderers = {
"YouTubeVideo": "lms.plugins.youtube_video_renderer", "YouTubeVideo": "lms.plugins.youtube_video_renderer",
"Video": "lms.plugins.video_renderer", "Video": "lms.plugins.video_renderer",
"Assignment": "lms.plugins.assignment_renderer", "Assignment": "lms.plugins.assignment_renderer",
"Embed": "lms.plugins.embed_renderer",
"Audio": "lms.plugins.audio_renderer",
"PDF": "lms.plugins.pdf_renderer",
} }
# page_renderer to manage profile pages # page_renderer to manage profile pages
@@ -320,4 +287,6 @@ profile_url_prefix = "/users/"
signup_form_template = "lms.plugins.show_custom_signup" signup_form_template = "lms.plugins.show_custom_signup"
on_login = "lms.overrides.user.on_login"
on_session_creation = "lms.overrides.user.on_session_creation" on_session_creation = "lms.overrides.user.on_session_creation"

View File

@@ -9,7 +9,6 @@ def after_install():
def after_sync(): def after_sync():
create_lms_roles() create_lms_roles()
set_default_home() set_default_home()
set_default_certificate_print_format()
add_all_roles_to("Administrator") add_all_roles_to("Administrator")
@@ -17,7 +16,7 @@ def add_pages_to_nav():
pages = [ pages = [
{"label": "Explore", "idx": 1}, {"label": "Explore", "idx": 1},
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2}, {"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3}, {"label": "Classes", "url": "/classes", "parent": "Explore", "idx": 3},
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4}, {"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
{"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5}, {"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5},
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6}, {"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
@@ -45,27 +44,17 @@ def add_pages_to_nav():
).save() ).save()
def before_uninstall(): def after_uninstall():
delete_custom_fields() delete_custom_fields()
delete_lms_roles()
def create_lms_roles(): def create_lms_roles():
create_course_creator_role() create_course_creator_role()
create_moderator_role() create_moderator_role()
create_evaluator_role()
create_lms_student_role()
def delete_lms_roles():
roles = ["Course Creator", "Moderator"]
for role in roles:
if frappe.db.exists("Role", role):
frappe.db.delete("Role", role)
def set_default_home(): def set_default_home():
frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses") frappe.db.set_value("Portal Settings", None, "default_portal_home", "/courses")
def create_course_creator_role(): def create_course_creator_role():
@@ -78,7 +67,7 @@ def create_course_creator_role():
"desk_access": 0, "desk_access": 0,
} }
) )
role.save() role.save(ignore_permissions=True)
def create_moderator_role(): def create_moderator_role():
@@ -91,52 +80,7 @@ def create_moderator_role():
"desk_access": 0, "desk_access": 0,
} }
) )
role.save() role.save(ignore_permissions=True)
def create_evaluator_role():
if not frappe.db.exists("Role", "Class Evaluator"):
role = frappe.new_doc("Role")
role.update(
{
"role_name": "Class Evaluator",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def create_lms_student_role():
if not frappe.db.exists("Role", "LMS Student"):
role = frappe.new_doc("Role")
role.update(
{
"role_name": "LMS Student",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def set_default_certificate_print_format():
filters = {
"doc_type": "LMS Certificate",
"property": "default_print_format",
}
if not frappe.db.exists("Property Setter", filters):
filters.update(
{
"doctype_or_field": "DocType",
"property_type": "Data",
"value": "Certificate",
}
)
doc = frappe.new_doc("Property Setter")
doc.update(filters)
doc.save()
def delete_custom_fields(): def delete_custom_fields():
@@ -182,3 +126,4 @@ def delete_custom_fields():
for field in fields: for field in fields:
frappe.db.delete("Custom Field", {"fieldname": field}) frappe.db.delete("Custom Field", {"fieldname": field})
frappe.db.commit()

View File

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

View File

@@ -116,7 +116,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-09-29 17:03:30.825021", "modified": "2022-09-15 17:22:21.662675",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Job", "module": "Job",
"name": "Job Opportunity", "name": "Job Opportunity",
@@ -144,7 +144,7 @@
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS Student", "role": "All",
"select": 1, "select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1

View File

@@ -21,7 +21,7 @@ def submit_solution(exercise, code):
@exerecise: name of the exercise to submit @exerecise: name of the exercise to submit
@code: solution to the exercise @code: solution to the exercise
""" """
ex = frappe.get_doc("LMS Exercise", exercise) ex = frappe.get_doc("Exercise", exercise)
if not ex: if not ex:
return return
doc = ex.submit(code) doc = ex.submit(code)
@@ -32,15 +32,15 @@ def submit_solution(exercise, code):
def save_current_lesson(course_name, lesson_name): def save_current_lesson(course_name, lesson_name):
"""Saves the current lesson for a student/mentor.""" """Saves the current lesson for a student/mentor."""
name = frappe.get_value( name = frappe.get_value(
doctype="LMS Enrollment", doctype="LMS Batch Membership",
filters={"course": course_name, "member": frappe.session.user}, filters={"course": course_name, "member": frappe.session.user},
fieldname="name", fieldname="name",
) )
if not name: if not name:
return return
doc = frappe.get_doc("LMS Enrollment", name) doc = frappe.get_doc("LMS Batch Membership", name)
doc.current_lesson = lesson_name doc.current_lesson = lesson_name
doc.save() doc.save(ignore_permissions=True)
return {"current_lesson": doc.current_lesson} return {"current_lesson": doc.current_lesson}
@@ -66,7 +66,7 @@ def join_cohort(course, cohort, subgroup, invite_code):
return {"ok": True, "status": "record found"} return {"ok": True, "status": "record found"}
else: else:
doc = frappe.get_doc(data) doc = frappe.get_doc(data)
doc.insert() doc.insert(ignore_permissions=True)
return {"ok": True, "status": "record created"} return {"ok": True, "status": "record created"}
@@ -82,7 +82,7 @@ def approve_cohort_join_request(join_request):
return {"ok": False, "error": "Permission Deined"} return {"ok": False, "error": "Permission Deined"}
r.status = "Accepted" r.status = "Accepted"
r.save() r.save(ignore_permissions=True)
return {"ok": True} return {"ok": True}
@@ -98,7 +98,7 @@ def reject_cohort_join_request(join_request):
return {"ok": False, "error": "Permission Deined"} return {"ok": False, "error": "Permission Deined"}
r.status = "Rejected" r.status = "Rejected"
r.save() r.save(ignore_permissions=True)
return {"ok": True} return {"ok": True}
@@ -115,7 +115,7 @@ def undo_reject_cohort_join_request(join_request):
return {"ok": False, "error": "Permission Deined"} return {"ok": False, "error": "Permission Deined"}
r.status = "Pending" r.status = "Pending"
r.save() r.save(ignore_permissions=True)
return {"ok": True} return {"ok": True}

View File

@@ -7,7 +7,7 @@
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"xIsSeries\": 1}, \"lineOptions\": {\"regionFill\": 1}}", "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"xIsSeries\": 1}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"document_type": "LMS Enrollment", "document_type": "LMS Batch Membership",
"dynamic_filters_json": "[]", "dynamic_filters_json": "[]",
"filters_json": "[]", "filters_json": "[]",
"group_by_type": "Count", "group_by_type": "Count",
@@ -15,7 +15,7 @@
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"last_synced_on": "2022-10-20 10:46:56.859520", "last_synced_on": "2022-10-20 10:46:56.859520",
"modified": "2022-10-20 11:30:26.863009", "modified": "2022-10-20 11:30:26.863008",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Enrollments", "name": "Course Enrollments",

View File

@@ -7,8 +7,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"course", "course",
"title", "title"
"evaluator"
], ],
"fields": [ "fields": [
{ {
@@ -24,24 +23,17 @@
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Course Title", "label": "Title",
"read_only": 1 "read_only": 1
},
{
"fieldname": "evaluator",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Evaluator",
"options": "Course Evaluator"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-08-28 10:03:02.960844", "modified": "2022-11-11 15:51:45.560864",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Batch Course", "name": "Class Course",
"naming_rule": "Autoincrement", "naming_rule": "Autoincrement",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],

View File

@@ -5,5 +5,5 @@
from frappe.model.document import Document from frappe.model.document import Document
class BatchCourse(Document): class ClassCourse(Document):
pass pass

View File

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

View File

@@ -7,11 +7,7 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"student_details_section",
"student", "student",
"payment",
"confirmation_email_sent",
"column_break_oduu",
"student_name", "student_name",
"username" "username"
], ],
@@ -38,36 +34,15 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Username", "label": "Username",
"read_only": 1 "read_only": 1
},
{
"fieldname": "student_details_section",
"fieldtype": "Section Break",
"label": "Student Details"
},
{
"fieldname": "column_break_oduu",
"fieldtype": "Column Break"
},
{
"fieldname": "payment",
"fieldtype": "Link",
"label": "Payment",
"options": "LMS Payment"
},
{
"default": "0",
"fieldname": "confirmation_email_sent",
"fieldtype": "Check",
"label": "Confirmation Email Sent"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-09 17:09:50.481794", "modified": "2022-11-15 11:13:39.410578",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Batch Student", "name": "Class Student",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -5,5 +5,5 @@
from frappe.model.document import Document from frappe.model.document import Document
class BatchStudent(Document): class ClassStudent(Document):
pass pass

View File

@@ -5,5 +5,5 @@
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
class TestBatchStudent(FrappeTestCase): class TestClassStudent(FrappeTestCase):
pass pass

View File

@@ -16,7 +16,7 @@ class Cohort(Document):
if include_counts: if include_counts:
mentors = self._get_subgroup_counts("Cohort Mentor") mentors = self._get_subgroup_counts("Cohort Mentor")
students = self._get_subgroup_counts("LMS Enrollment") students = self._get_subgroup_counts("LMS Batch Membership")
join_requests = self._get_subgroup_counts("Cohort Join Request", status="Pending") join_requests = self._get_subgroup_counts("Cohort Join Request", status="Pending")
for s in subgroups: for s in subgroups:
s.num_mentors = mentors.get(s.name, 0) s.num_mentors = mentors.get(s.name, 0)
@@ -56,7 +56,7 @@ class Cohort(Document):
return { return {
"subgroups": self._get_count("Cohort Subgroup"), "subgroups": self._get_count("Cohort Subgroup"),
"mentors": self._get_count("Cohort Mentor"), "mentors": self._get_count("Cohort Mentor"),
"students": self._get_count("LMS Enrollment"), "students": self._get_count("LMS Batch Membership"),
"join_requests": self._get_count("Cohort Join Request", status="Pending"), "join_requests": self._get_count("Cohort Join Request", status="Pending"),
} }

View File

@@ -51,7 +51,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-29 17:08:18.950560", "modified": "2021-12-16 15:06:03.985221",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Cohort Join Request", "name": "Cohort Join Request",
@@ -68,21 +68,9 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1,
"write": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -13,7 +13,7 @@ class CohortJoinRequest(Document):
def ensure_student(self): def ensure_student(self):
# case 1 - user is already a member # case 1 - user is already a member
q = { q = {
"doctype": "LMS Enrollment", "doctype": "LMS Batch Membership",
"cohort": self.cohort, "cohort": self.cohort,
"subgroup": self.subgroup, "subgroup": self.subgroup,
"member": self.email, "member": self.email,
@@ -26,21 +26,21 @@ class CohortJoinRequest(Document):
cohort = frappe.get_doc("Cohort", self.cohort) cohort = frappe.get_doc("Cohort", self.cohort)
q = { q = {
"doctype": "LMS Enrollment", "doctype": "LMS Batch Membership",
"course": cohort.course, "course": cohort.course,
"member": self.email, "member": self.email,
"member_type": "Student", "member_type": "Student",
} }
name = frappe.db.exists(q) name = frappe.db.exists(q)
if name: if name:
doc = frappe.get_doc("LMS Enrollment", name) doc = frappe.get_doc("LMS Batch Membership", name)
doc.cohort = self.cohort doc.cohort = self.cohort
doc.subgroup = self.subgroup doc.subgroup = self.subgroup
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
else: else:
# case 3 - user has not signed up for this course yet # case 3 - user has not signed up for this course yet
data = { data = {
"doctype": "LMS Enrollment", "doctype": "LMS Batch Membership",
"course": cohort.course, "course": cohort.course,
"cohort": self.cohort, "cohort": self.cohort,
"subgroup": self.subgroup, "subgroup": self.subgroup,

View File

@@ -23,7 +23,7 @@ class CohortSubgroup(Document):
def has_student(self, email): def has_student(self, email):
"""Check if given user is a student of this subgroup.""" """Check if given user is a student of this subgroup."""
q = {"doctype": "LMS Enrollment", "subgroup": self.name, "member": email} q = {"doctype": "LMS Batch Membership", "subgroup": self.name, "member": email}
return frappe.db.exists(q) return frappe.db.exists(q)
def has_join_request(self, email): def has_join_request(self, email):
@@ -45,7 +45,7 @@ class CohortSubgroup(Document):
def get_students(self): def get_students(self):
emails = frappe.get_all( emails = frappe.get_all(
"LMS Enrollment", "LMS Batch Membership",
filters={"subgroup": self.name}, filters={"subgroup": self.name},
fields=["member"], fields=["member"],
pluck="member", pluck="member",

View File

@@ -59,7 +59,7 @@
"link_fieldname": "chapter" "link_fieldname": "chapter"
} }
], ],
"modified": "2023-09-29 17:03:58.013819", "modified": "2022-03-14 17:57:00.707416",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Chapter", "name": "Course Chapter",
@@ -86,7 +86,7 @@
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS Student", "role": "All",
"select": 1, "select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1

View File

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

View File

@@ -27,7 +27,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-07-13 11:30:22.641076", "modified": "2022-04-01 15:14:03.300260",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Evaluator", "name": "Course Evaluator",
@@ -45,30 +45,6 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Class Evaluator",
"share": 1,
"write": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -4,7 +4,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from lms.lms.utils import get_evaluator
class CourseEvaluator(Document): class CourseEvaluator(Document):
@@ -37,26 +36,21 @@ class CourseEvaluator(Document):
@frappe.whitelist() @frappe.whitelist()
def get_schedule(course, date, batch=None): def get_schedule(course, date):
evaluator = get_evaluator(course, batch) evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
all_slots = frappe.get_all( all_slots = frappe.get_all(
"Evaluator Schedule", "Evaluator Schedule",
filters={"parent": evaluator}, filters={"parent": evaluator},
fields=["day", "start_time", "end_time"], fields=["day", "start_time", "end_time"],
order_by="start_time",
) )
booked_slots = frappe.get_all( booked_slots = frappe.get_all(
"LMS Certificate Request", "LMS Certificate Request",
filters={"evaluator": evaluator, "date": date}, filters={"evaluator": evaluator, "date": date},
fields=["start_time", "day"], fields=["start_time"],
) )
for slot in booked_slots: for slot in booked_slots:
same_slot = list( same_slot = list(filter(lambda x: x.start_time == slot.start_time, all_slots))
filter(lambda x: x.start_time == slot.start_time and x.day == slot.day, all_slots)
)
if len(same_slot): if len(same_slot):
all_slots.remove(same_slot[0]) all_slots.remove(same_slot[0])

View File

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

View File

@@ -24,7 +24,6 @@
"file_type", "file_type",
"section_break_11", "section_break_11",
"body", "body",
"instructor_notes",
"help_section", "help_section",
"help" "help"
], ],
@@ -132,16 +131,11 @@
{ {
"fieldname": "column_break_15", "fieldname": "column_break_15",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "instructor_notes",
"fieldtype": "Markdown Editor",
"label": "Instructor Notes"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-29 17:04:19.252897", "modified": "2022-12-28 16:01:42.191123",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Lesson", "name": "Course Lesson",
@@ -169,7 +163,7 @@
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS Student", "role": "All",
"select": 1, "select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ from frappe.model.document import Document
from lms.lms.utils import get_membership from lms.lms.utils import get_membership
class LMSExercise(Document): class Exercise(Document):
def get_user_submission(self): def get_user_submission(self):
"""Returns the latest submission for this user.""" """Returns the latest submission for this user."""
user = frappe.session.user user = frappe.session.user
@@ -43,7 +43,7 @@ class LMSExercise(Document):
exercise_title=self.title, exercise_title=self.title,
course=self.course, course=self.course,
lesson=self.lesson, lesson=self.lesson,
batch=member.batch_old, batch=member.batch,
solution=code, solution=code,
member=member.name, member=member.name,
) )

View File

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

View File

@@ -7,7 +7,7 @@
"field_order": [ "field_order": [
"exercise", "exercise",
"status", "status",
"batch_old", "batch",
"column_break_4", "column_break_4",
"exercise_title", "exercise_title",
"course", "course",
@@ -30,7 +30,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Exercise", "label": "Exercise",
"options": "LMS Exercise", "options": "Exercise",
"search_index": 1 "search_index": 1
}, },
{ {
@@ -40,10 +40,10 @@
"options": "Correct\nIncorrect" "options": "Correct\nIncorrect"
}, },
{ {
"fieldname": "batch_old", "fieldname": "batch",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch Old", "label": "Batch",
"options": "LMS Batch Old" "options": "LMS Batch"
}, },
{ {
"fieldname": "column_break_4", "fieldname": "column_break_4",
@@ -110,7 +110,7 @@
"fieldname": "member", "fieldname": "member",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Member", "label": "Member",
"options": "LMS Enrollment" "options": "LMS Batch Membership"
}, },
{ {
"fetch_from": "member.member", "fetch_from": "member.member",
@@ -141,7 +141,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-12-08 22:58:46.312863", "modified": "2021-12-08 22:58:46.312861",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Exercise Latest Submission", "name": "Exercise Latest Submission",
@@ -163,4 +163,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -7,7 +7,7 @@
"field_order": [ "field_order": [
"exercise", "exercise",
"status", "status",
"batch_old", "batch",
"column_break_4", "column_break_4",
"exercise_title", "exercise_title",
"course", "course",
@@ -25,7 +25,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Exercise", "label": "Exercise",
"options": "LMS Exercise" "options": "Exercise"
}, },
{ {
"fetch_from": "exercise.title", "fetch_from": "exercise.title",
@@ -44,10 +44,10 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "batch_old", "fieldname": "batch",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch Old", "label": "Batch",
"options": "LMS Batch Old" "options": "LMS Batch"
}, },
{ {
"fetch_from": "exercise.lesson", "fetch_from": "exercise.lesson",
@@ -96,12 +96,12 @@
"fieldname": "member", "fieldname": "member",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Member", "label": "Member",
"options": "LMS Enrollment" "options": "LMS Batch Membership"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-12-08 22:25:05.809377", "modified": "2021-12-08 22:25:05.809376",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Exercise Submission", "name": "Exercise Submission",
@@ -123,4 +123,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -19,7 +19,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-29 17:04:58.167481", "modified": "2021-12-21 09:34:35.018280",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Function", "name": "Function",
@@ -44,12 +44,11 @@
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS Student", "role": "All",
"select": 1, "select": 1,
"share": 1 "share": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"states": []
} }

View File

@@ -19,7 +19,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-29 17:05:27.231982", "modified": "2021-12-21 09:35:20.443192",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Industry", "name": "Industry",
@@ -44,12 +44,11 @@
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS Student", "role": "All",
"select": 1, "select": 1,
"share": 1 "share": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"states": []
} }

View File

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

View File

@@ -0,0 +1,117 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-12-21 16:15:22.651658",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"assignment",
"lesson",
"course",
"status",
"column_break_3",
"member",
"member_name",
"comments"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Lesson",
"options": "Course Lesson",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "assignment",
"fieldtype": "Attach",
"label": "Assignment",
"reqd": 1
},
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fetch_from": "member.full_name",
"fieldname": "member_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Name",
"read_only": 1
},
{
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"read_only": 1
},
{
"default": "Not Graded",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Pass\nFail\nNot Graded"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
}
],
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2022-11-16 12:11:59.472025",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson Assignment",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [
{
"color": "Green",
"title": "Pass"
},
{
"color": "Orange",
"title": "Not Graded"
},
{
"color": "Red",
"title": "Fail"
}
],
"title_field": "lesson"
}

View File

@@ -0,0 +1,61 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class LessonAssignment(Document):
def validate(self):
self.validate_duplicates()
def validate_duplicates(self):
if frappe.db.exists(
"Lesson Assignment",
{"lesson": self.lesson, "member": self.member, "name": ["!=", self.name]},
):
lesson_title = frappe.db.get_value("Course Lesson", self.lesson, "title")
frappe.throw(
_("Assignment for Lesson {0} by {1} already exists.").format(
lesson_title, self.member_name
)
)
@frappe.whitelist()
def upload_assignment(assignment, lesson):
args = {
"doctype": "Lesson Assignment",
"lesson": lesson,
"member": frappe.session.user,
}
if frappe.db.exists(args):
del args["doctype"]
frappe.db.set_value("Lesson Assignment", args, "assignment", assignment)
else:
args.update({"assignment": assignment})
lesson_work = frappe.get_doc(args)
lesson_work.save(ignore_permissions=True)
@frappe.whitelist()
def get_assignment(lesson):
assignment = frappe.db.get_value(
"Lesson Assignment",
{"lesson": lesson, "member": frappe.session.user},
["lesson", "member", "assignment", "comments", "status"],
as_dict=True,
)
assignment.file_name = frappe.db.get_value(
"File", {"file_url": assignment.assignment}, "file_name"
)
return assignment
@frappe.whitelist()
def grade_assignment(name, result, comments):
doc = frappe.get_doc("Lesson Assignment", name)
doc.status = result
doc.comments = comments
doc.save()

View File

@@ -5,5 +5,5 @@
import unittest import unittest
class TestLMSAssignmentSubmission(unittest.TestCase): class TestLessonAssignment(unittest.TestCase):
pass pass

View File

@@ -1,40 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-05-29 14:50:07.910319",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"assessment_type",
"assessment_name"
],
"fields": [
{
"fieldname": "assessment_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Assessment Type",
"options": "DocType"
},
{
"fieldname": "assessment_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Assessment Name",
"options": "assessment_type"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-05-29 14:56:36.602399",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Assessment",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

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

View File

@@ -1,104 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format: ASG-{#####}",
"creation": "2023-05-26 19:41:26.025081",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"grade_assignment",
"question",
"column_break_hmwv",
"type",
"show_answer",
"answer"
],
"fields": [
{
"fieldname": "question",
"fieldtype": "Text Editor",
"label": "Question"
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
"options": "Document\nPDF\nURL\nImage\nText"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Title"
},
{
"fieldname": "column_break_hmwv",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.type == \"Text\"",
"fieldname": "show_answer",
"fieldtype": "Check",
"label": "Show Answer"
},
{
"depends_on": "show_answer",
"fieldname": "answer",
"fieldtype": "Text Editor",
"label": "Answer"
},
{
"default": "1",
"depends_on": "eval:doc.type == \"Text\"",
"fieldname": "grade_assignment",
"fieldtype": "Check",
"label": "Grade Assignment"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-06 12:08:46.898950",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Assignment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
}
],
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}

View File

@@ -1,25 +0,0 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from lms.lms.utils import has_course_moderator_role, has_course_instructor_role
class LMSAssignment(Document):
pass
@frappe.whitelist()
def save_assignment(assignment, title, type, question):
if not has_course_moderator_role() or not has_course_instructor_role():
return
if assignment:
doc = frappe.get_doc("LMS Assignment", assignment)
else:
doc = frappe.get_doc({"doctype": "LMS Assignment"})
doc.update({"title": title, "type": type, "question": question})
doc.save(ignore_permissions=True)
return doc.name

View File

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

View File

@@ -1,200 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format: ASG-SUB-{#####}",
"creation": "2021-12-21 16:15:22.651658",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"assignment",
"assignment_title",
"type",
"column_break_3",
"member",
"member_name",
"section_break_dlzh",
"question",
"column_break_zvis",
"assignment_attachment",
"answer",
"section_break_rqal",
"status",
"evaluator",
"column_break_esgd",
"comments",
"section_break_cwaw",
"lesson",
"course",
"column_break_ygdu"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "assignment",
"fieldtype": "Link",
"label": "Assignment",
"options": "LMS Assignment"
},
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fetch_from": "member.full_name",
"fieldname": "member_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Name",
"read_only": 1
},
{
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"default": "Not Graded",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Pass\nFail\nNot Graded\nNot Applicable"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fetch_from": "course.evaluator",
"fieldname": "evaluator",
"fieldtype": "Link",
"label": "Evaluator",
"options": "User",
"read_only": 1
},
{
"depends_on": "eval:!([\"URL\", \"Text\"]).includes(doc.type);",
"fieldname": "assignment_attachment",
"fieldtype": "Attach",
"label": "Assignment Attachment",
"mandatory_depends_on": "eval:doc.type != \"URL\";"
},
{
"fetch_from": "assignment.type",
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Document\nPDF\nURL\nImage\nText"
},
{
"fetch_from": "assignment.question",
"fieldname": "question",
"fieldtype": "Text Editor",
"label": "Question",
"read_only": 1
},
{
"fetch_from": "assignment.title",
"fieldname": "assignment_title",
"fieldtype": "Data",
"label": "Assignment Title"
},
{
"fieldname": "section_break_rqal",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_esgd",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_cwaw",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ygdu",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:([\"URL\", \"Text\"]).includes(doc.type);",
"fieldname": "answer",
"fieldtype": "Text Editor",
"label": "Answer",
"mandatory_depends_on": "eval:doc.type == \"URL\";"
},
{
"fieldname": "section_break_dlzh",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_zvis",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2023-10-06 15:14:55.984714",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Assignment Submission",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [
{
"color": "Green",
"title": "Pass"
},
{
"color": "Orange",
"title": "Not Graded"
},
{
"color": "Red",
"title": "Fail"
},
{
"color": "Blue",
"title": "Not Applicable"
}
],
"title_field": "assignment_title"
}

View File

@@ -1,100 +0,0 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_url
class LMSAssignmentSubmission(Document):
def validate(self):
self.validate_duplicates()
def validate_duplicates(self):
if frappe.db.exists(
"LMS Assignment Submission",
{"assignment": self.assignment, "member": self.member, "name": ["!=", self.name]},
):
lesson_title = frappe.db.get_value("Course Lesson", self.lesson, "title")
frappe.throw(
_("Assignment for Lesson {0} by {1} already exists.").format(
lesson_title, self.member_name
)
)
@frappe.whitelist()
def upload_assignment(
assignment_attachment=None,
answer=None,
assignment=None,
lesson=None,
status="Not Graded",
comments=None,
submission=None,
):
if frappe.session.user == "Guest":
return
assignment_details = frappe.db.get_value(
"LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1
)
assignment_type = assignment_details.type
if assignment_type in ["URL", "Text"] and not answer:
frappe.throw(_("Please enter the URL for assignment submission."))
if assignment_type == "File" and not assignment_attachment:
frappe.throw(_("Please upload the assignment file."))
if assignment_type == "URL" and not validate_url(answer):
frappe.throw(_("Please enter a valid URL."))
if submission:
doc = frappe.get_doc("LMS Assignment Submission", submission)
else:
doc = frappe.get_doc(
{
"doctype": "LMS Assignment Submission",
"assignment": assignment,
"lesson": lesson,
"member": frappe.session.user,
"type": assignment_type,
}
)
doc.update(
{
"assignment_attachment": assignment_attachment,
"status": "Not Applicable"
if assignment_type == "Text" and not assignment_details.grade_assignment
else status,
"comments": comments,
"answer": answer,
}
)
doc.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def get_assignment(lesson):
assignment = frappe.db.get_value(
"LMS Assignment Submission",
{"lesson": lesson, "member": frappe.session.user},
["name", "lesson", "member", "assignment_attachment", "comments", "status"],
as_dict=True,
)
assignment.file_name = frappe.db.get_value(
"File", {"file_url": assignment.assignment_attachment}, "file_name"
)
return assignment
@frappe.whitelist()
def grade_assignment(name, result, comments):
doc = frappe.get_doc("LMS Assignment Submission", name)
doc.status = result
doc.comments = comments
doc.save(ignore_permissions=True)

View File

@@ -1,123 +1,7 @@
// Copyright (c) 2022, Frappe and contributors // Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("LMS Batch", { frappe.ui.form.on("LMS Batch", {
onload: function (frm) { // refresh: function(frm) {
frm.set_query("student", "students", function (doc) { // }
return {
filters: {
ignore_user_type: 1,
},
};
});
frm.set_query("reference_doctype", "timetable", function () {
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
return {
filters: {
name: ["in", doctypes],
},
};
});
frm.set_query("reference_doctype", "timetable_legends", function () {
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
return {
filters: {
name: ["in", doctypes],
},
};
});
},
timetable_template: function (frm) {
set_timetable(frm);
},
}); });
const set_timetable = (frm) => {
if (frm.doc.timetable_template) {
frm.clear_table("timetable");
frm.refresh_fields();
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "LMS Batch Timetable",
parent: "LMS Timetable Template",
fields: [
"reference_doctype",
"reference_docname",
"day",
"start_time",
"end_time",
"duration",
],
filters: {
parent: frm.doc.timetable_template,
parenttype: "LMS Timetable Template",
},
order_by: "idx",
},
callback: (data) => {
add_timetable_rows(frm, data.message);
},
});
}
};
const add_timetable_rows = (frm, timetable) => {
timetable.forEach((row) => {
let child = frm.add_child("timetable");
child.reference_doctype = row.reference_doctype;
child.reference_docname = row.reference_docname;
child.date = frappe.datetime.add_days(frm.doc.start_date, row.day - 1);
child.start_time = row.start_time;
child.end_time = row.end_time
? row.end_time
: row.duration
? moment
.utc(row.start_time, "HH:mm")
.add(row.duration, "hour")
.format("HH:mm")
: null;
child.duration = row.duration;
});
frm.refresh_field("timetable");
set_legends(frm);
};
const set_legends = (frm) => {
if (frm.doc.timetable_template) {
frm.clear_table("timetable_legends");
frm.refresh_fields();
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "LMS Timetable Legend",
parent: "LMS Timetable Template",
fields: ["reference_doctype", "label", "color"],
filters: {
parent: frm.doc.timetable_template,
parenttype: "LMS Timetable Template",
},
order_by: "idx",
},
callback: (data) => {
add_legend_rows(frm, data.message);
},
});
}
};
const add_legend_rows = (frm, legends) => {
legends.forEach((row) => {
let child = frm.add_child("timetable_legends");
child.reference_doctype = row.reference_doctype;
child.label = row.label;
child.color = row.color;
});
frm.refresh_field("timetable_legends");
frm.save();
};

View File

@@ -1,291 +1,132 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "autoname": "format: BATCH-{#####}",
"autoname": "format: CLS-{#####}", "creation": "2021-03-18 19:37:34.614796",
"creation": "2022-11-09 16:14:05.876933",
"default_view": "List",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title", "course",
"start_date", "start_date",
"end_date",
"column_break_4",
"start_time", "start_time",
"column_break_3",
"title",
"sessions_on",
"end_time", "end_time",
"published", "section_break_5",
"section_break_rgfj",
"medium",
"category",
"column_break_flwy",
"seat_count",
"section_break_6",
"description", "description",
"batch_details_raw", "section_break_7",
"column_break_hlqw", "visibility",
"batch_details", "membership",
"meta_image", "column_break_9",
"section_break_jgji", "status",
"students", "stage"
"courses",
"assessment_tab",
"assessment",
"schedule_tab",
"timetable_template",
"column_break_anya",
"show_live_class",
"allow_future",
"section_break_ontp",
"timetable",
"timetable_legends",
"pricing_tab",
"section_break_gsac",
"paid_batch",
"column_break_iens",
"amount",
"currency",
"customisations_tab",
"section_break_ubxi",
"custom_component",
"column_break_pxgb",
"custom_script"
], ],
"fields": [ "fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{ {
"fieldname": "title", "fieldname": "title",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "Title", "label": "Title",
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "end_date", "fieldname": "description",
"fieldtype": "Date", "fieldtype": "Markdown Editor",
"in_list_view": 1, "label": "Description"
"label": "End Date",
"reqd": 1
}, },
{ {
"fieldname": "column_break_4", "default": "Public",
"fieldname": "visibility",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility",
"options": "Public\nUnlisted\nPrivate"
},
{
"fieldname": "membership",
"fieldtype": "Select",
"label": "Membership",
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
},
{
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Active\nInactive"
},
{
"default": "Ready",
"fieldname": "stage",
"fieldtype": "Select",
"label": "Stage",
"options": "Ready\nIn Progress\nCompleted\nCancelled"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "description", "fieldname": "section_break_5",
"fieldtype": "Small Text", "fieldtype": "Section Break",
"label": "Description", "label": "Batch Description"
"reqd": 1
}, },
{ {
"fieldname": "section_break_6", "fieldname": "column_break_9",
"fieldtype": "Section Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "students", "fieldname": "section_break_7",
"fieldtype": "Table", "fieldtype": "Section Break",
"label": "Students", "label": "Batch Settings"
"options": "Batch Student"
},
{
"fieldname": "courses",
"fieldtype": "Table",
"label": "Courses",
"options": "Batch Course"
}, },
{ {
"fieldname": "start_date", "fieldname": "start_date",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1, "in_list_view": 1,
"label": "Start Date", "label": "Start Date"
"reqd": 1
},
{
"fieldname": "custom_component",
"fieldtype": "Code",
"label": "Custom HTML",
"options": "HTML"
},
{
"default": "0",
"description": "Students will be enrolled in a paid batch once they complete the payment",
"fieldname": "paid_batch",
"fieldtype": "Check",
"label": "Paid Batch"
},
{
"fieldname": "seat_count",
"fieldtype": "Int",
"label": "Seat Count"
}, },
{ {
"fieldname": "start_time", "fieldname": "start_time",
"fieldtype": "Time", "fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time" "label": "Start Time"
}, },
{
"fieldname": "sessions_on",
"fieldtype": "Data",
"label": "Sessions On Days"
},
{ {
"fieldname": "end_time", "fieldname": "end_time",
"fieldtype": "Time", "fieldtype": "Time",
"in_list_view": 1,
"label": "End Time" "label": "End Time"
},
{
"fieldname": "assessment_tab",
"fieldtype": "Tab Break",
"label": "Assessment"
},
{
"fieldname": "assessment",
"fieldtype": "Table",
"label": "Assessment",
"options": "LMS Assessment"
},
{
"fieldname": "section_break_rgfj",
"fieldtype": "Section Break"
},
{
"default": "Online",
"fieldname": "medium",
"fieldtype": "Select",
"label": "Medium",
"options": "Online\nOffline"
},
{
"fieldname": "column_break_flwy",
"fieldtype": "Column Break"
},
{
"fieldname": "category",
"fieldtype": "Link",
"label": "Category",
"options": "LMS Category"
},
{
"description": "These customisations will work on the main batch page.",
"fieldname": "section_break_ubxi",
"fieldtype": "Section Break"
},
{
"fieldname": "schedule_tab",
"fieldtype": "Tab Break",
"label": "Timetable"
},
{
"fieldname": "section_break_gsac",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_iens",
"fieldtype": "Column Break"
},
{
"depends_on": "paid_batch",
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount"
},
{
"depends_on": "paid_batch",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "batch_details",
"fieldtype": "Text Editor",
"label": "Batch Details",
"reqd": 1
},
{
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"label": "Published"
},
{
"fieldname": "timetable",
"fieldtype": "Table",
"label": "Timetable",
"options": "LMS Batch Timetable"
},
{
"fieldname": "timetable_template",
"fieldtype": "Link",
"label": "Timetable Template",
"options": "LMS Timetable Template"
},
{
"fieldname": "column_break_anya",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "show_live_class",
"fieldtype": "Check",
"label": "Show live class"
},
{
"fieldname": "section_break_ontp",
"fieldtype": "Section Break"
},
{
"fieldname": "batch_details_raw",
"fieldtype": "HTML Editor",
"label": "Batch Details Raw"
},
{
"fieldname": "column_break_hlqw",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_jgji",
"fieldtype": "Section Break"
},
{
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Meta Image"
},
{
"fieldname": "column_break_pxgb",
"fieldtype": "Column Break"
},
{
"fieldname": "customisations_tab",
"fieldtype": "Tab Break",
"label": "Customisations"
},
{
"fieldname": "pricing_tab",
"fieldtype": "Tab Break",
"label": "Pricing"
},
{
"fieldname": "custom_script",
"fieldtype": "Code",
"label": "Custom Script (JavaScript)",
"options": "Javascript"
},
{
"fieldname": "timetable_legends",
"fieldtype": "Table",
"label": "Timetable Legends",
"options": "LMS Timetable Legend"
},
{
"default": "1",
"fieldname": "allow_future",
"fieldtype": "Check",
"label": "Allow accessing future dates"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [
"modified": "2023-10-12 12:53:37.351989", {
"group": "Members",
"link_doctype": "LMS Batch Membership",
"link_fieldname": "batch"
}
],
"modified": "2022-09-28 18:43:22.955907",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",
"naming_rule": "Expression (old style)", "naming_rule": "Expression",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -299,23 +140,11 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
} }
], ],
"show_title_field_in_link": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "title" "track_changes": 1
} }

View File

@@ -1,395 +1,93 @@
# Copyright (c) 2022, Frappe and contributors # Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
import requests
import base64
import json
from frappe import _ from frappe import _
from datetime import timedelta
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (
cint, from lms.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
format_date, from lms.lms.utils import is_mentor
format_datetime,
add_to_date,
getdate,
get_datetime,
)
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
from lms.www.utils import get_quiz_details, get_assignment_details
from frappe.email.doctype.email_template.email_template import get_email_template
class LMSBatch(Document): class LMSBatch(Document):
def validate(self): def validate(self):
if self.seat_count: pass
self.validate_seats_left() # self.validate_if_mentor()
self.validate_duplicate_courses()
self.validate_duplicate_students()
self.validate_duplicate_assessments()
self.validate_membership()
self.validate_timetable()
self.send_confirmation_mail()
def validate_duplicate_students(self): def validate_if_mentor(self):
students = [row.student for row in self.students] if not is_mentor(self.course, frappe.session.user):
duplicates = {student for student in students if students.count(student) > 1} course_title = frappe.db.get_value("LMS Course", self.course, "title")
if len(duplicates): frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
frappe.throw(
_("Student {0} has already been added to this batch.").format(
frappe.bold(next(iter(duplicates)))
)
)
def validate_duplicate_courses(self): def after_insert(self):
courses = [row.course for row in self.courses] create_membership(batch=self.name, course=self.course, member_type="Mentor")
duplicates = {course for course in courses if courses.count(course) > 1}
if len(duplicates):
title = frappe.db.get_value("LMS Course", next(iter(duplicates)), "title")
frappe.throw(
_("Course {0} has already been added to this batch.").format(frappe.bold(title))
)
def validate_duplicate_assessments(self): def is_member(self, email, member_type=None):
assessments = [row.assessment_name for row in self.assessment] """Checks if a person is part of a batch.
for assessment in self.assessment:
if assessments.count(assessment.assessment_name) > 1:
title = frappe.db.get_value(
assessment.assessment_type, assessment.assessment_name, "title"
)
frappe.throw(
_("Assessment {0} has already been added to this batch.").format(
frappe.bold(title)
)
)
def send_confirmation_mail(self): If member_type is specified, checks if the person is a Student/Mentor.
for student in self.students: """
if not student.confirmation_email_sent: filters = {"batch": self.name, "member": email}
self.send_mail(student) if member_type:
student.confirmation_email_sent = 1 filters["member_type"] = member_type
return frappe.db.exists("LMS Batch Membership", filters)
def send_mail(self, student): def get_membership(self, email):
subject = _("Enrollment Confirmation for the Next Training Batch") """Returns the membership document of given user."""
template = "batch_confirmation" name = frappe.get_value(
custom_template = frappe.db.get_single_value( doctype="LMS Batch Membership",
"LMS Settings", "batch_confirmation_template" filters={"batch": self.name, "member": email},
fieldname="name",
) )
return frappe.get_doc("LMS Batch Membership", name)
args = { def get_current_lesson(self, user):
"student_name": student.student_name, """Returns the name of the current lesson for the given user."""
"start_time": self.start_time, membership = self.get_membership(user)
"start_date": self.start_date, return membership and membership.current_lesson
"medium": self.medium,
"name": self.name,
}
if custom_template:
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
recipients=student.student,
subject=subject,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
retry=3,
)
def validate_membership(self):
for course in self.courses:
for student in self.students:
filters = {
"doctype": "LMS Enrollment",
"member": student.student,
"course": course.course,
}
if not frappe.db.exists(filters):
frappe.get_doc(filters).save()
def validate_seats_left(self):
if cint(self.seat_count) < len(self.students):
frappe.throw(_("There are no seats available in this batch."))
def validate_timetable(self):
for schedule in self.timetable:
if schedule.start_time and schedule.end_time:
if (
schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time
):
frappe.throw(
_("Row #{0} Start time cannot be greater than or equal to end time.").format(
schedule.idx
)
)
if schedule.start_time < self.start_time or schedule.start_time > self.end_time:
frappe.throw(
_("Row #{0} Start time cannot be outside the batch duration.").format(
schedule.idx
)
)
if schedule.end_time < self.start_time or schedule.end_time > self.end_time:
frappe.throw(
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
)
if schedule.date < self.start_date or schedule.date > self.end_date:
frappe.throw(
_("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx)
)
@frappe.whitelist() @frappe.whitelist()
def remove_student(student, batch_name): def save_message(message, batch):
frappe.only_for("Moderator") doc = frappe.get_doc(
frappe.db.delete("Batch Student", {"student": student, "parent": batch_name})
@frappe.whitelist()
def remove_course(course, parent):
frappe.only_for("Moderator")
frappe.db.delete("Batch Course", {"course": course, "parent": parent})
@frappe.whitelist()
def remove_assessment(assessment, parent):
frappe.only_for("Moderator")
frappe.db.delete("LMS Assessment", {"assessment_name": assessment, "parent": parent})
@frappe.whitelist()
def create_live_class(
batch_name, title, duration, date, time, timezone, auto_recording, description=None
):
date = format_date(date, "yyyy-mm-dd", True)
frappe.only_for("Moderator")
payload = {
"topic": title,
"start_time": format_datetime(f"{date} {time}", "yyyy-MM-ddTHH:mm:ssZ"),
"duration": duration,
"agenda": description,
"private_meeting": True,
"auto_recording": "none"
if auto_recording == "No Recording"
else auto_recording.lower(),
"timezone": timezone,
}
headers = {
"Authorization": "Bearer " + authenticate(),
"content-type": "application/json",
}
response = requests.post(
"https://api.zoom.us/v2/users/me/meetings", headers=headers, data=json.dumps(payload)
)
if response.status_code == 201:
data = json.loads(response.text)
payload.update(
{
"doctype": "LMS Live Class",
"start_url": data.get("start_url"),
"join_url": data.get("join_url"),
"title": title,
"host": frappe.session.user,
"date": date,
"time": time,
"batch_name": batch_name,
"password": data.get("password"),
"description": description,
"auto_recording": auto_recording,
}
)
class_details = frappe.get_doc(payload)
class_details.save()
return class_details
def authenticate():
zoom = frappe.get_single("Zoom Settings")
if not zoom.enable:
frappe.throw(_("Please enable Zoom Settings to use this feature."))
authenticate_url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={zoom.account_id}"
headers = {
"Authorization": "Basic "
+ base64.b64encode(
bytes(
zoom.client_id
+ ":"
+ zoom.get_password(fieldname="client_secret", raise_exception=False),
encoding="utf8",
)
).decode()
}
response = requests.request("POST", authenticate_url, headers=headers)
return response.json()["access_token"]
@frappe.whitelist()
def create_batch(
title,
start_date,
end_date,
description=None,
batch_details=None,
batch_details_raw=None,
meta_image=None,
seat_count=0,
start_time=None,
end_time=None,
medium="Online",
category=None,
paid_batch=0,
amount=0,
currency=None,
name=None,
published=0,
):
frappe.only_for("Moderator")
if name:
doc = frappe.get_doc("LMS Batch", name)
else:
doc = frappe.get_doc({"doctype": "LMS Batch"})
doc.update(
{ {
"title": title, "doctype": "LMS Message",
"start_date": start_date, "batch": batch,
"end_date": end_date, "author": frappe.session.user,
"description": description, "message": message,
"batch_details": batch_details,
"batch_details_raw": batch_details_raw,
"image": meta_image,
"seat_count": seat_count,
"start_time": start_time,
"end_time": end_time,
"medium": medium,
"category": category,
"paid_batch": paid_batch,
"amount": amount,
"currency": currency,
"published": published,
} }
) )
doc.save() doc.save(ignore_permissions=True)
return doc
@frappe.whitelist() def switch_batch(course_name, email, batch_name):
def fetch_lessons(courses): """Switches the user from the current batch of the course to a new batch."""
lessons = [] membership = frappe.get_last_doc(
courses = json.loads(courses) "LMS Batch Membership", filters={"course": course_name, "member": email}
for course in courses:
lessons.extend(get_lessons(course.get("course")))
return lessons
@frappe.whitelist()
def add_course(course, parent, name=None, evaluator=None):
frappe.only_for("Moderator")
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
frappe.throw(_("Course already added to the batch."))
if name:
doc = frappe.get_doc("Batch Course", name)
else:
doc = frappe.new_doc("Batch Course")
doc.update(
{
"course": course,
"evaluator": evaluator,
"parent": parent,
"parentfield": "courses",
"parenttype": "LMS Batch",
}
)
doc.save()
return doc.name
@frappe.whitelist()
def get_batch_timetable(batch):
timetable = frappe.get_all(
"LMS Batch Timetable",
filters={"parent": batch},
fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"],
order_by="date",
) )
show_live_class = frappe.db.get_value("LMS Batch", batch, "show_live_class") batch = frappe.get_doc("LMS Batch", batch_name)
if show_live_class: if not batch:
live_classes = get_live_classes(batch) raise ValueError(f"Invalid Batch: {batch_name}")
timetable.extend(live_classes)
timetable = get_timetable_details(timetable) if batch.course != course_name:
return timetable raise ValueError("Can not switch batches across courses")
if batch.is_member(email):
print(f"{email} is already a member of {batch.title}")
return
def get_live_classes(batch): old_batch = frappe.get_doc("LMS Batch", membership.batch)
live_classes = frappe.get_all(
"LMS Live Class",
{"batch_name": batch},
["name", "title", "date", "time as start_time", "duration", "join_url as url"],
order_by="date",
)
for class_ in live_classes:
class_.end_time = class_.start_time + timedelta(minutes=class_.duration)
class_.reference_doctype = "LMS Live Class"
class_.reference_docname = class_.name
class_.icon = "icon-call"
return live_classes print("updating membership", membership.name)
membership.batch = batch_name
membership.save()
# update exercise submissions
def get_timetable_details(timetable): filters = {"owner": email, "batch": old_batch.name}
for entry in timetable: for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
entry.title = frappe.db.get_value( doc = frappe.get_doc("Exercise Submission", name)
entry.reference_doctype, entry.reference_docname, "title" print("updating exercise submission", name)
) doc.batch = batch_name
assessment = frappe._dict({"assessment_name": entry.reference_docname}) doc.save()
if entry.reference_doctype == "Course Lesson":
entry.icon = "icon-list"
course = frappe.db.get_value(
entry.reference_doctype, entry.reference_docname, "course"
)
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
elif entry.reference_doctype == "LMS Quiz":
entry.icon = "icon-quiz"
entry.url = "/quizzes"
details = get_quiz_details(assessment, frappe.session.user)
entry.update(details)
elif entry.reference_doctype == "LMS Assignment":
entry.icon = "icon-quiz"
details = get_assignment_details(assessment, frappe.session.user)
entry.update(details)
timetable = sorted(timetable, key=lambda k: k["date"])
return timetable
@frappe.whitelist()
def send_email_to_students(batch, subject, reply_to, message):
frappe.only_for("Moderator")
students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student")
frappe.sendmail(
recipients=students,
subject=subject,
reply_to=reply_to,
message=message
)

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2022, Frappe and Contributors # Copyright (c) 2021, FOSS United and Contributors
# See license.txt # See license.txt
# import frappe # import frappe
from frappe.tests.utils import FrappeTestCase import unittest
class TestLMSClass(FrappeTestCase): class TestLMSBatch(unittest.TestCase):
pass pass

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2021, FOSS United and contributors // Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("LMS Enrollment", { frappe.ui.form.on("LMS Batch Membership", {
onload: function (frm) { onload: function (frm) {
frm.set_query("member", function (doc) { frm.set_query("member", function (doc) {
return { return {

View File

@@ -7,7 +7,7 @@
"field_order": [ "field_order": [
"course", "course",
"member_type", "member_type",
"payment", "batch",
"column_break_3", "column_break_3",
"member", "member",
"member_name", "member_name",
@@ -15,7 +15,6 @@
"section_break_8", "section_break_8",
"cohort", "cohort",
"subgroup", "subgroup",
"batch_old",
"column_break_12", "column_break_12",
"current_lesson", "current_lesson",
"progress", "progress",
@@ -23,10 +22,10 @@
], ],
"fields": [ "fields": [
{ {
"fieldname": "batch_old", "fieldname": "batch",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch Old", "label": "Batch",
"options": "LMS Batch Old" "options": "LMS Batch"
}, },
{ {
"fieldname": "member", "fieldname": "member",
@@ -113,20 +112,14 @@
{ {
"fieldname": "section_break_8", "fieldname": "section_break_8",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"fieldname": "payment",
"fieldtype": "Link",
"label": "Payment",
"options": "LMS Payment"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-10-02 12:41:25.139734", "modified": "2022-10-10 12:38:17.839526",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Enrollment", "name": "LMS Batch Membership",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -140,31 +133,6 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"select": 1,
"share": 1,
"write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
@@ -173,4 +141,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "member_name" "title_field": "member_name"
} }

View File

@@ -6,17 +6,17 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
class LMSEnrollment(Document): class LMSBatchMembership(Document):
def validate(self): def validate(self):
self.validate_membership_in_same_batch() self.validate_membership_in_same_batch()
self.validate_membership_in_different_batch_same_course() self.validate_membership_in_different_batch_same_course()
def validate_membership_in_same_batch(self): def validate_membership_in_same_batch(self):
filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]} filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]}
if self.batch_old: if self.batch:
filters["batch_old"] = self.batch_old filters["batch"] = self.batch
previous_membership = frappe.db.get_value( previous_membership = frappe.db.get_value(
"LMS Enrollment", filters, fieldname=["member_type", "member"], as_dict=1 "LMS Batch Membership", filters, fieldname=["member_type", "member"], as_dict=1
) )
if previous_membership: if previous_membership:
@@ -34,16 +34,16 @@ class LMSEnrollment(Document):
if self.member_type != "Student": if self.member_type != "Student":
return return
course = frappe.db.get_value("LMS Batch Old", self.batch_old, "course") course = frappe.db.get_value("LMS Batch", self.batch, "course")
memberships = frappe.get_all( memberships = frappe.get_all(
"LMS Enrollment", "LMS Batch Membership",
filters={ filters={
"member": self.member, "member": self.member,
"name": ["!=", self.name], "name": ["!=", self.name],
"member_type": "Student", "member_type": "Student",
"course": self.course, "course": self.course,
}, },
fields=["batch_old", "member_type", "name"], fields=["batch", "member_type", "name"],
) )
if memberships: if memberships:
@@ -51,7 +51,7 @@ class LMSEnrollment(Document):
member_name = frappe.db.get_value("User", self.member, "full_name") member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw( frappe.throw(
_("{0} is already a Student of {1} course through {2} batch").format( _("{0} is already a Student of {1} course through {2} batch").format(
member_name, course, membership.batch_old member_name, course, membership.batch
) )
) )
@@ -62,8 +62,8 @@ def create_membership(
): ):
frappe.get_doc( frappe.get_doc(
{ {
"doctype": "LMS Enrollment", "doctype": "LMS Batch Membership",
"batch_old": batch, "batch": batch,
"course": course, "course": course,
"role": role, "role": role,
"member_type": member_type, "member_type": member_type,
@@ -76,13 +76,15 @@ def create_membership(
@frappe.whitelist() @frappe.whitelist()
def update_current_membership(batch, course, member): def update_current_membership(batch, course, member):
all_memberships = frappe.get_all( all_memberships = frappe.get_all(
"LMS Enrollment", {"member": member, "course": course} "LMS Batch Membership", {"member": member, "course": course}
) )
for membership in all_memberships: for membership in all_memberships:
frappe.db.set_value("LMS Enrollment", membership.name, "is_current", 0) frappe.db.set_value("LMS Batch Membership", membership.name, "is_current", 0)
current_membership = frappe.get_all( current_membership = frappe.get_all(
"LMS Enrollment", {"batch_old": batch, "member": member} "LMS Batch Membership", {"batch": batch, "member": member}
) )
if len(current_membership): if len(current_membership):
frappe.db.set_value("LMS Enrollment", current_membership[0].name, "is_current", 1) frappe.db.set_value(
"LMS Batch Membership", current_membership[0].name, "is_current", 1
)

View File

@@ -8,12 +8,12 @@ import frappe
from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
class TestLMSEnrollment(unittest.TestCase): class TestLMSBatchMembership(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.db.delete("LMS Enrollment") frappe.db.sql("DELETE FROM `tabLMS Batch Membership`")
frappe.db.delete("LMS Batch Old") frappe.db.sql("DELETE FROM `tabLMS Batch`")
frappe.db.delete("LMS Course Mentor Mapping") frappe.db.sql("delete from `tabLMS Course Mentor Mapping`")
frappe.db.delete("User", {"email": ("like", "%@test.com")}) frappe.db.sql("DELETE FROM `tabUser` where email like '%@test.com'")
def new_course_batch(self): def new_course_batch(self):
course = new_course("Test Course") course = new_course("Test Course")
@@ -26,7 +26,7 @@ class TestLMSEnrollment(unittest.TestCase):
batch = frappe.get_doc( batch = frappe.get_doc(
{ {
"doctype": "LMS Batch Old", "doctype": "LMS Batch",
"name": "test-batch", "name": "test-batch",
"title": "Test Batch", "title": "Test Batch",
"course": course.name, "course": course.name,
@@ -37,14 +37,13 @@ class TestLMSEnrollment(unittest.TestCase):
frappe.session.user = "Administrator" frappe.session.user = "Administrator"
return course, batch return course, batch
def add_membership(self, batch_name, member_name, course, member_type="Student"): def add_membership(self, batch_name, member_name, member_type="Student"):
doc = frappe.get_doc( doc = frappe.get_doc(
{ {
"doctype": "LMS Enrollment", "doctype": "LMS Batch Membership",
"batch_old": batch_name, "batch": batch_name,
"member": member_name, "member": member_name,
"member_type": member_type, "member_type": member_type,
"course": course,
} }
) )
doc.insert() doc.insert()
@@ -53,7 +52,7 @@ class TestLMSEnrollment(unittest.TestCase):
def test_membership(self): def test_membership(self):
course, batch = self.new_course_batch() course, batch = self.new_course_batch()
member = new_user("Test", "test01@test.com") member = new_user("Test", "test01@test.com")
membership = self.add_membership(batch.name, member.name, course.name) membership = self.add_membership(batch.name, member.name)
assert membership.course == course.name assert membership.course == course.name
assert membership.member_name == member.full_name assert membership.member_name == member.full_name
@@ -61,7 +60,7 @@ class TestLMSEnrollment(unittest.TestCase):
def test_membership_change_role(self): def test_membership_change_role(self):
course, batch = self.new_course_batch() course, batch = self.new_course_batch()
member = new_user("Test", "test01@test.com") member = new_user("Test", "test01@test.com")
membership = self.add_membership(batch.name, member.name, course.name) membership = self.add_membership(batch.name, member.name)
# it should be possible to change role # it should be possible to change role
membership.role = "Admin" membership.role = "Admin"

View File

@@ -1,150 +0,0 @@
{
"actions": [],
"autoname": "format: BATCH-{#####}",
"creation": "2021-03-18 19:37:34.614796",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"start_date",
"start_time",
"column_break_3",
"title",
"sessions_on",
"end_time",
"section_break_5",
"description",
"section_break_7",
"visibility",
"membership",
"column_break_9",
"status",
"stage"
],
"fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"default": "Public",
"fieldname": "visibility",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility",
"options": "Public\nUnlisted\nPrivate"
},
{
"fieldname": "membership",
"fieldtype": "Select",
"label": "Membership",
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
},
{
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Active\nInactive"
},
{
"default": "Ready",
"fieldname": "stage",
"fieldtype": "Select",
"label": "Stage",
"options": "Ready\nIn Progress\nCompleted\nCancelled"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Batch Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Batch Settings"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
"fieldname": "sessions_on",
"fieldtype": "Data",
"label": "Sessions On Days"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Members",
"link_doctype": "LMS Enrollment",
"link_fieldname": "batch_old"
}
],
"modified": "2022-09-28 18:43:22.955907",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Old",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,92 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.doctype.lms_enrollment.lms_enrollment import create_membership
from lms.lms.utils import is_mentor
class LMSBatchOld(Document):
def validate(self):
pass
# self.validate_if_mentor()
def validate_if_mentor(self):
if not is_mentor(self.course, frappe.session.user):
course_title = frappe.db.get_value("LMS Course", self.course, "title")
frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
def after_insert(self):
create_membership(batch=self.name, course=self.course, member_type="Mentor")
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
If member_type is specified, checks if the person is a Student/Mentor.
"""
filters = {"batch_old": self.name, "member": email}
if member_type:
filters["member_type"] = member_type
return frappe.db.exists("LMS Enrollment", filters)
def get_membership(self, email):
"""Returns the membership document of given user."""
name = frappe.get_value(
doctype="LMS Enrollment",
filters={"batch_old": self.name, "member": email},
fieldname="name",
)
return frappe.get_doc("LMS Enrollment", name)
def get_current_lesson(self, user):
"""Returns the name of the current lesson for the given user."""
membership = self.get_membership(user)
return membership and membership.current_lesson
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc(
{
"doctype": "LMS Message",
"batch_old": batch,
"author": frappe.session.user,
"message": message,
}
)
doc.save(ignore_permissions=True)
def switch_batch(course_name, email, batch_name):
"""Switches the user from the current batch of the course to a new batch."""
membership = frappe.get_last_doc(
"LMS Enrollment", filters={"course": course_name, "member": email}
)
batch = frappe.get_doc("LMS Batch Old", batch_name)
if not batch:
raise ValueError(f"Invalid Batch: {batch_name}")
if batch.course != course_name:
raise ValueError("Can not switch batches across courses")
if batch.is_member(email):
print(f"{email} is already a member of {batch.title}")
return
old_batch = frappe.get_doc("LMS Batch Old", membership.batch_old)
membership.batch_old = batch_name
membership.save()
# update exercise submissions
filters = {"owner": email, "batch_old": old_batch.name}
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
doc = frappe.get_doc("Exercise Submission", name)
print("updating exercise submission", name)
doc.batch_old = batch_name
doc.save()

View File

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

View File

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

View File

@@ -1,87 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "hash",
"creation": "2023-09-14 12:44:51.098956",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"column_break_htdc",
"reference_doctype",
"reference_docname",
"date",
"day",
"column_break_merq",
"start_time",
"end_time",
"duration"
],
"fields": [
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference DocType",
"options": "DocType"
},
{
"fieldname": "reference_docname",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference DocName",
"options": "reference_doctype"
},
{
"fieldname": "column_break_merq",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.parenttype == \"LMS Batch\";",
"fieldname": "date",
"fieldtype": "Date",
"label": "Date"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "column_break_htdc",
"fieldtype": "Column Break"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
},
{
"depends_on": "eval: doc.parenttype == \"LMS Timetable Template\";",
"fieldname": "day",
"fieldtype": "Int",
"label": "Day"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-03 17:40:31.530181",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Timetable",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

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

View File

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

View File

@@ -1,59 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:category",
"creation": "2023-06-15 12:40:36.484165",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"category"
],
"fields": [
{
"fieldname": "category",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Category",
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-06-15 15:14:11.341961",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Category",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "category"
}

View File

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

View File

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

View File

@@ -8,11 +8,9 @@
"course", "course",
"member", "member",
"member_name", "member_name",
"published",
"column_break_3", "column_break_3",
"issue_date", "issue_date",
"expiry_date", "expiry_date"
"batch_name"
], ],
"fields": [ "fields": [
{ {
@@ -54,24 +52,11 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Member Name", "label": "Member Name",
"read_only": 1 "read_only": 1
},
{
"fieldname": "batch_name",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Batch",
"options": "LMS Batch"
},
{
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"label": "Publish on Participant Page"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-13 11:03:23.479255", "modified": "2022-04-06 11:49:36.077370",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate", "name": "LMS Certificate",
@@ -88,18 +73,6 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -5,17 +5,15 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_years, nowdate from frappe.utils import add_years, nowdate
from frappe.utils.pdf import get_pdf
from lms.lms.utils import is_certified from lms.lms.utils import is_certified
class LMSCertificate(Document): class LMSCertificate(Document):
def validate(self): def before_insert(self):
self.validate_duplicate_certificate()
def validate_duplicate_certificate(self):
certificates = frappe.get_all( certificates = frappe.get_all(
"LMS Certificate", "LMS Certificate", {"member": self.member, "course": self.course}
{"member": self.member, "course": self.course, "name": ["!=", self.name]},
) )
if len(certificates): if len(certificates):
full_name = frappe.db.get_value("User", self.member, "full_name") full_name = frappe.db.get_value("User", self.member, "full_name")
@@ -24,16 +22,6 @@ class LMSCertificate(Document):
_("{0} is already certified for the course {1}").format(full_name, course_name) _("{0} is already certified for the course {1}").format(full_name, course_name)
) )
def on_update(self):
frappe.share.add_docshare(
self.doctype,
self.name,
self.member,
write=1,
share=1,
flags={"ignore_share_permission": True},
)
@frappe.whitelist() @frappe.whitelist()
def create_certificate(course): def create_certificate(course):
@@ -59,3 +47,10 @@ def create_certificate(course):
) )
certificate.save(ignore_permissions=True) certificate.save(ignore_permissions=True)
return certificate return certificate
@frappe.whitelist()
def get_certificate_pdf(html):
frappe.local.response.filename = "certificate.pdf"
frappe.local.response.filecontent = get_pdf(html, {"orientation": "LandScape"})
frappe.local.response.type = "pdf"

View File

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

View File

@@ -8,16 +8,15 @@
"field_order": [ "field_order": [
"member", "member",
"member_name", "member_name",
"course",
"column_break_5", "column_break_5",
"course",
"status",
"section_break_6",
"date", "date",
"start_time", "start_time",
"end_time", "end_time",
"batch_name",
"section_break_6",
"rating",
"status",
"column_break_10", "column_break_10",
"rating",
"summary" "summary"
], ],
"fields": [ "fields": [
@@ -84,29 +83,21 @@
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"options": "Pending\nIn Progress\nPass\nFail", "options": "Pass\nFail",
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Evaluation Details"
}, },
{ {
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "batch_name",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Batch Name",
"options": "LMS Batch"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-26 19:44:43.594892", "modified": "2022-11-23 11:49:01.400292",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate Evaluation", "name": "LMS Certificate Evaluation",
@@ -123,39 +114,10 @@
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Class Evaluator",
"share": 1,
"write": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [ "states": [],
{
"color": "Green",
"title": "Pass"
},
{
"color": "Red",
"title": "Fail"
},
{
"color": "Blue",
"title": "Pending"
},
{
"color": "Orange",
"title": "In Progress"
}
],
"title_field": "member_name" "title_field": "member_name"
} }

View File

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

View File

@@ -7,16 +7,12 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"course", "course",
"evaluator",
"batch_name",
"column_break_4",
"member", "member",
"member_name", "member_name",
"section_break_lifi", "evaluator",
"column_break_4",
"date", "date",
"day", "day",
"google_meet_link",
"column_break_ddyh",
"start_time", "start_time",
"end_time" "end_time"
], ],
@@ -33,18 +29,17 @@
{ {
"fieldname": "member", "fieldname": "member",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_list_view": 1,
"label": "Member", "label": "Member",
"options": "User", "options": "User",
"reqd": 1 "reqd": 1
}, },
{ {
"fetch_from": "course.evaluator", "fetch_from": "course.evaluator",
"fetch_if_empty": 1,
"fieldname": "evaluator", "fieldname": "evaluator",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Evaluator", "label": "Evaluator",
"options": "User", "options": "Course Evaluator",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -71,6 +66,7 @@
{ {
"fieldname": "end_time", "fieldname": "end_time",
"fieldtype": "Time", "fieldtype": "Time",
"in_list_view": 1,
"label": "End Time", "label": "End Time",
"reqd": 1 "reqd": 1
}, },
@@ -84,32 +80,11 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Member Name", "label": "Member Name",
"read_only": 1 "read_only": 1
},
{
"fieldname": "section_break_lifi",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ddyh",
"fieldtype": "Column Break"
},
{
"fieldname": "google_meet_link",
"fieldtype": "Data",
"label": "Google Meet Link",
"read_only": 1
},
{
"fieldname": "batch_name",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Batch Name",
"options": "LMS Batch"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-08-23 14:50:37.618352", "modified": "2022-04-06 11:33:33.711545",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate Request", "name": "LMS Certificate Request",
@@ -124,31 +99,6 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Class Evaluator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1, "share": 1,
"write": 1 "write": 1
} }

View File

@@ -5,8 +5,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.utils import format_date, format_time, getdate, add_to_date, get_datetime from frappe.utils import format_date, format_time, getdate
from lms.lms.utils import get_evaluator
class LMSCertificateRequest(Document): class LMSCertificateRequest(Document):
@@ -16,13 +15,12 @@ class LMSCertificateRequest(Document):
def validate_if_existing_requests(self): def validate_if_existing_requests(self):
existing_requests = frappe.get_all( existing_requests = frappe.get_all(
"LMS Certificate Request", "LMS Certificate Request",
{"member": self.member, "course": self.course, "name": ["!=", self.name]}, {"member": self.member, "course": self.course},
["date", "start_time", "course"], ["date", "start_time", "course"],
) )
for req in existing_requests: for req in existing_requests:
if req.date == getdate(self.date) and getdate() <= getdate(self.date):
if req.date == getdate(self.date) or getdate() <= getdate(req.date):
course_title = frappe.db.get_value("LMS Course", req.course, "title") course_title = frappe.db.get_value("LMS Course", req.course, "title")
frappe.throw( frappe.throw(
_("You already have an evaluation on {0} at {1} for the course {2}.").format( _("You already have an evaluation on {0} at {1} for the course {2}.").format(
@@ -33,98 +31,26 @@ class LMSCertificateRequest(Document):
) )
def schedule_evals():
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
one_hour_ago = add_to_date(get_datetime(), hours=-1)
evals = frappe.get_all(
"LMS Certificate Request",
{"creation": [">=", one_hour_ago], "google_meet_link": ["is", "not set"]},
["name", "member", "member_name", "evaluator", "date", "start_time", "end_time"],
)
for eval in evals:
setup_calendar_event(eval)
def setup_calendar_event(eval):
calendar = frappe.db.get_value(
"Google Calendar", {"user": eval.evaluator, "enable": 1}, "name"
)
if calendar:
event = create_event(eval)
add_participants(eval, event)
update_meeting_details(eval, event, calendar)
def create_event(eval):
event = frappe.get_doc(
{
"doctype": "Event",
"subject": f"Evaluation of {eval.member_name}",
"starts_on": f"{eval.date} {eval.start_time}",
"ends_on": f"{eval.date} {eval.end_time}",
}
)
event.save()
return event
def add_participants(eval, event):
participants = [eval.member, eval.evaluator]
for participant in participants:
contact_name = frappe.db.get_value("Contact", {"email_id": participant}, "name")
frappe.get_doc(
{
"doctype": "Event Participants",
"reference_doctype": "Contact",
"reference_docname": contact_name,
"email": participant,
"parent": event.name,
"parenttype": "Event",
"parentfield": "event_participants",
}
).save()
def update_meeting_details(eval, event, calendar):
event.reload()
event.update(
{
"sync_with_google_calendar": 1,
"add_video_conferencing": 1,
"google_calendar": calendar,
}
)
event.save()
event.reload()
frappe.db.set_value(
"LMS Certificate Request", eval.name, "google_meet_link", event.google_meet_link
)
@frappe.whitelist() @frappe.whitelist()
def create_certificate_request(course, date, day, start_time, end_time, batch=None): def create_certificate_request(course, date, day, start_time, end_time):
is_member = frappe.db.exists( is_member = frappe.db.exists(
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user} {"doctype": "LMS Batch Membership", "course": course, "member": frappe.session.user}
) )
if not is_member: if not is_member:
return return
eval = frappe.new_doc("LMS Certificate Request")
eval.update( frappe.get_doc(
{ {
"doctype": "LMS Certificate Request",
"course": course, "course": course,
"evaluator": get_evaluator(course, batch),
"member": frappe.session.user, "member": frappe.session.user,
"date": date, "date": date,
"day": day, "day": day,
"start_time": start_time, "start_time": start_time,
"end_time": end_time, "end_time": end_time,
"batch": batch,
} }
) ).save(ignore_permissions=True)
eval.save(ignore_permissions=True)
@frappe.whitelist() @frappe.whitelist()

View File

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

View File

@@ -0,0 +1,111 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format: CLS-{#####}",
"creation": "2022-11-09 16:14:05.876933",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"start_date",
"end_date",
"column_break_4",
"description",
"section_break_6",
"students",
"courses",
"custom_component"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "students",
"fieldtype": "Table",
"label": "Students",
"options": "Class Student"
},
{
"fieldname": "courses",
"fieldtype": "Table",
"label": "Courses",
"options": "Class Course"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date",
"reqd": 1
},
{
"description": "The HTML code entered here will be displayed on the class details page.",
"fieldname": "custom_component",
"fieldtype": "Code",
"label": "Custom Component",
"options": "HTML"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-25 10:37:24.250557",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Class",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,66 @@
# Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import cint
class LMSClass(Document):
def validate(self):
validate_membership(self)
def validate_membership(self):
for course in self.courses:
for student in self.students:
filters = {
"doctype": "LMS Batch Membership",
"member": student.student,
"course": course.course,
}
if not frappe.db.exists(filters):
frappe.get_doc(filters).save()
@frappe.whitelist()
def add_student(email, class_name):
if not frappe.db.exists("User", email):
frappe.throw(_("There is no such user. Please create a user with this Email ID."))
frappe.get_doc(
{
"doctype": "Class Student",
"student": email,
"student_name": frappe.db.get_value("User", email, "full_name"),
"parent": class_name,
"parenttype": "LMS Class",
"parentfield": "students",
}
).save()
return True
@frappe.whitelist()
def remove_student(student, class_name):
frappe.db.delete("Class Student", {"student": student, "parent": class_name})
return True
@frappe.whitelist()
def update_course(class_name, course, value):
if cint(value):
doc = frappe.get_doc(
{
"doctype": "Class Course",
"parent": class_name,
"course": course,
"parenttype": "LMS Class",
"parentfield": "courses",
}
)
doc.save()
else:
frappe.db.delete("Class Course", {"parent": class_name, "course": course})
return True

View File

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

View File

@@ -11,6 +11,14 @@ frappe.ui.form.on("LMS Course", {
}; };
}); });
frm.set_query("instructor", "instructors", function () {
return {
filters: {
ignore_user_type: 1,
},
};
});
frm.set_query("course", "related_courses", function () { frm.set_query("course", "related_courses", function () {
return { return {
filters: { filters: {
@@ -19,14 +27,4 @@ frappe.ui.form.on("LMS Course", {
}; };
}); });
}, },
refresh: (frm) => {
frm.add_web_link(`/courses/${frm.doc.name}`, "See on Website");
if (!frm.doc.currency)
frappe.db
.get_single_value("LMS Settings", "default_currency")
.then((value) => {
frm.set_value("currency", value);
});
},
}); });

View File

@@ -32,18 +32,19 @@
"description", "description",
"chapters", "chapters",
"related_courses", "related_courses",
"pricing_section",
"paid_course",
"currency",
"course_price",
"certification_section", "certification_section",
"enable_certification", "enable_certification",
"expiry", "expiry",
"max_attempts", "section_break_23",
"column_break_rxww",
"grant_certificate_after", "grant_certificate_after",
"evaluator", "evaluator",
"duration" "column_break_26",
"max_attempts",
"duration",
"pricing_section",
"paid_certificate",
"currency",
"price_certificate"
], ],
"fields": [ "fields": [
{ {
@@ -52,11 +53,12 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Title", "label": "Title",
"reqd": 1, "reqd": 1,
"unique": 1,
"width": "200" "width": "200"
}, },
{ {
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Markdown Editor",
"label": "Description", "label": "Description",
"reqd": 1 "reqd": 1
}, },
@@ -169,6 +171,13 @@
"fieldname": "column_break_12", "fieldname": "column_break_12",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "0",
"depends_on": "enable_certification",
"fieldname": "paid_certificate",
"fieldtype": "Check",
"label": "Paid Certificate"
},
{ {
"depends_on": "enable_certification", "depends_on": "enable_certification",
"fieldname": "grant_certificate_after", "fieldname": "grant_certificate_after",
@@ -185,16 +194,24 @@
"options": "Course Evaluator" "options": "Course Evaluator"
}, },
{ {
"depends_on": "eval: doc.grant_certificate_after == \"Evaluation\"",
"fieldname": "pricing_section", "fieldname": "pricing_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Pricing" "label": "Pricing"
}, },
{ {
"depends_on": "paid_course", "depends_on": "paid_certificate",
"fieldname": "price_certificate",
"fieldtype": "Currency",
"label": "Certificate Price",
"mandatory_depends_on": "paid_certificate"
},
{
"depends_on": "paid_certificate",
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
"mandatory_depends_on": "paid_course", "mandatory_depends_on": "paid_certificate",
"options": "Currency" "options": "Currency"
}, },
{ {
@@ -212,21 +229,11 @@
"options": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12" "options": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12"
}, },
{ {
"default": "0", "fieldname": "section_break_23",
"fieldname": "paid_course", "fieldtype": "Section Break"
"fieldtype": "Check",
"label": "Paid Course"
}, },
{ {
"depends_on": "paid_course", "fieldname": "column_break_26",
"fieldname": "course_price",
"fieldtype": "Currency",
"label": "Course Price",
"option": "currency",
"mandatory_depends_on": "paid_course"
},
{
"fieldname": "column_break_rxww",
"fieldtype": "Column Break" "fieldtype": "Column Break"
} }
], ],
@@ -239,7 +246,7 @@
}, },
{ {
"group": "Batches", "group": "Batches",
"link_doctype": "LMS Batch Old", "link_doctype": "LMS Batch",
"link_fieldname": "course" "link_fieldname": "course"
}, },
{ {
@@ -254,7 +261,7 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-08-28 11:09:11.945066", "modified": "2022-09-14 13:26:53.153822",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",
@@ -269,18 +276,20 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
{ {
"create": 1, "create": 1,
"delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 1,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Course Creator", "role": "All",
"select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1
} }

View File

@@ -2,20 +2,19 @@
# For license information, please see license.txt # For license information, please see license.txt
import json import json
import random
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.telemetry import capture
from lms.lms.utils import get_chapters, can_create_courses from lms.lms.utils import get_chapters
from ...utils import generate_slug, validate_image from ...utils import generate_slug, validate_image
from frappe import _
class LMSCourse(Document): class LMSCourse(Document):
def validate(self): def validate(self):
self.validate_instructors() self.validate_instructors()
self.validate_video_link()
self.validate_status() self.validate_status()
self.image = validate_image(self.image) self.image = validate_image(self.image)
@@ -31,10 +30,6 @@ class LMSCourse(Document):
} }
).save(ignore_permissions=True) ).save(ignore_permissions=True)
def validate_video_link(self):
if self.video_link and "/" in self.video_link:
self.video_link = self.video_link.split("/")[-1]
def validate_status(self): def validate_status(self):
if self.published: if self.published:
self.status = "Approved" self.status = "Approved"
@@ -43,9 +38,6 @@ class LMSCourse(Document):
if not self.upcoming and self.has_value_changed("upcoming"): if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users() self.send_email_to_interested_users()
def after_insert(self):
capture("course_created", "lms")
def send_email_to_interested_users(self): def send_email_to_interested_users(self):
interested_users = frappe.get_all( interested_users = frappe.get_all(
"LMS Course Interest", {"course": self.name}, ["name", "user"] "LMS Course Interest", {"course": self.name}, ["name", "user"]
@@ -75,10 +67,7 @@ class LMSCourse(Document):
def autoname(self): def autoname(self):
if not self.name: if not self.name:
title = self.title self.name = generate_slug(self.title, "LMS Course")
if self.title == "New Course":
title = self.title + str(random.randint(0, 99))
self.name = generate_slug(title, "LMS Course")
def __repr__(self): def __repr__(self):
return f"<Course#{self.name}>" return f"<Course#{self.name}>"
@@ -118,18 +107,20 @@ class LMSCourse(Document):
return return
batch_name = frappe.get_value( batch_name = frappe.get_value(
doctype="LMS Enrollment", doctype="LMS Batch Membership",
filters={"course": self.name, "member_type": "Student", "member": email}, filters={"course": self.name, "member_type": "Student", "member": email},
fieldname="batch_old", fieldname="batch",
) )
return batch_name and frappe.get_doc("LMS Batch Old", batch_name) return batch_name and frappe.get_doc("LMS Batch", batch_name)
def get_batches(self, mentor=None): def get_batches(self, mentor=None):
batches = frappe.get_all("LMS Batch Old", {"course": self.name}) batches = frappe.get_all("LMS Batch", {"course": self.name})
if mentor: if mentor:
# TODO: optimize this # TODO: optimize this
memberships = frappe.db.get_all("LMS Enrollment", {"member": mentor}, ["batch_old"]) memberships = frappe.db.get_all(
batch_names = {m.batch_old for m in memberships} "LMS Batch Membership", {"member": mentor}, ["batch"]
)
batch_names = {m.batch for m in memberships}
return [b for b in batches if b.name in batch_names] return [b for b in batches if b.name in batch_names]
def get_cohorts(self): def get_cohorts(self):
@@ -159,12 +150,10 @@ class LMSCourse(Document):
def get_all_memberships(self, member): def get_all_memberships(self, member):
all_memberships = frappe.get_all( all_memberships = frappe.get_all(
"LMS Enrollment", {"member": member, "course": self.name}, ["batch_old"] "LMS Batch Membership", {"member": member, "course": self.name}, ["batch"]
) )
for membership in all_memberships: for membership in all_memberships:
membership.batch_title = frappe.db.get_value( membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
"LMS Batch Old", membership.batch_old, "title"
)
return all_memberships return all_memberships
@@ -212,13 +201,7 @@ def save_course(
published, published,
upcoming, upcoming,
image=None, image=None,
paid_course=False,
course_price=None,
currency=None,
): ):
if not can_create_courses(course):
return
if course: if course:
doc = frappe.get_doc("LMS Course", course) doc = frappe.get_doc("LMS Course", course)
else: else:
@@ -234,9 +217,6 @@ def save_course(
"tags": tags, "tags": tags,
"published": cint(published), "published": cint(published),
"upcoming": cint(upcoming), "upcoming": cint(upcoming),
"paid_course": cint(paid_course),
"course_price": course_price,
"currency": currency,
} }
) )
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
@@ -280,7 +260,6 @@ def save_lesson(
preview, preview,
idx, idx,
lesson, lesson,
instructor_notes=None,
youtube=None, youtube=None,
quiz_id=None, quiz_id=None,
question=None, question=None,
@@ -296,7 +275,6 @@ def save_lesson(
"chapter": chapter, "chapter": chapter,
"title": title, "title": title,
"body": body, "body": body,
"instructor_notes": instructor_notes,
"include_in_preview": preview, "include_in_preview": preview,
"youtube": youtube, "youtube": youtube,
"quiz_id": quiz_id, "quiz_id": quiz_id,
@@ -323,43 +301,3 @@ def save_lesson(
lesson_reference.save(ignore_permissions=True) lesson_reference.save(ignore_permissions=True)
return doc.name return doc.name
@frappe.whitelist()
def reorder_lesson(old_chapter, old_lesson_array, new_chapter, new_lesson_array):
if old_chapter == new_chapter:
sort_lessons(new_chapter, new_lesson_array)
else:
sort_lessons(old_chapter, old_lesson_array)
sort_lessons(new_chapter, new_lesson_array)
def sort_lessons(chapter, lesson_array):
lesson_array = json.loads(lesson_array)
for les in lesson_array:
ref = frappe.get_all("Lesson Reference", {"lesson": les}, ["name", "idx"])
if ref:
frappe.db.set_value(
"Lesson Reference",
ref[0].name,
{
"parent": chapter,
"idx": lesson_array.index(les) + 1,
},
)
@frappe.whitelist()
def reorder_chapter(chapter_array):
chapter_array = json.loads(chapter_array)
for chap in chapter_array:
ref = frappe.get_all("Chapter Reference", {"chapter": chap}, ["name", "idx"])
if ref:
frappe.db.set_value(
"Chapter Reference",
ref[0].name,
{
"idx": chapter_array.index(chap) + 1,
},
)

View File

@@ -12,6 +12,7 @@ class TestLMSCourse(unittest.TestCase):
def test_new_course(self): def test_new_course(self):
course = new_course("Test Course") course = new_course("Test Course")
assert course.title == "Test Course" assert course.title == "Test Course"
assert course.name == "test-course"
# disabled this test as it is failing # disabled this test as it is failing
def _test_add_mentors(self): def _test_add_mentors(self):
@@ -34,11 +35,9 @@ class TestLMSCourse(unittest.TestCase):
if frappe.db.exists("LMS Course", "test-course"): if frappe.db.exists("LMS Course", "test-course"):
frappe.db.delete("Exercise Submission", {"course": "test-course"}) frappe.db.delete("Exercise Submission", {"course": "test-course"})
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"}) frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
frappe.db.delete("LMS Exercise", {"course": "test-course"}) frappe.db.delete("Exercise", {"course": "test-course"})
frappe.db.delete("LMS Enrollment", {"course": "test-course"}) frappe.db.delete("LMS Batch Membership", {"course": "test-course"})
frappe.db.delete("Course Lesson", {"course": "test-course"}) frappe.db.delete("LMS Batch", {"course": "test-course"})
frappe.db.delete("Course Chapter", {"course": "test-course"})
frappe.db.delete("LMS Batch Old", {"course": "test-course"})
frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"}) frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"})
frappe.db.delete("Course Instructor", {"parent": "test-course"}) frappe.db.delete("Course Instructor", {"parent": "test-course"})
frappe.db.sql("delete from `tabCourse Instructor`") frappe.db.sql("delete from `tabCourse Instructor`")
@@ -51,14 +50,14 @@ def new_user(name, email):
return frappe.get_doc("User", user) return frappe.get_doc("User", user)
else: else:
filters = { filters = {
"doctype": "User",
"email": email, "email": email,
"first_name": name, "first_name": name,
"send_welcome_email": False, "send_welcome_email": False,
} }
doc = frappe.new_doc("User") doc = frappe.get_doc(filters)
doc.update(filters) doc.insert()
doc.save()
return doc return doc
@@ -69,6 +68,7 @@ def new_course(title, additional_filters=None):
else: else:
create_evaluator() create_evaluator()
filters = { filters = {
"doctype": "LMS Course",
"title": title, "title": title,
"short_introduction": title, "short_introduction": title,
"description": title, "description": title,
@@ -77,9 +77,8 @@ def new_course(title, additional_filters=None):
if additional_filters: if additional_filters:
filters.update(additional_filters) filters.update(additional_filters)
doc = frappe.new_doc("LMS Course") doc = frappe.get_doc(filters)
doc.update(filters) doc.insert(ignore_permissions=True)
doc.save()
return doc return doc

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