mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-20 22:00:27 -06:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b18f360daf | ||
|
|
20cc952982 | ||
|
|
cd39fd1dc2 | ||
|
|
0e59c15773 | ||
|
|
0a73032950 | ||
|
|
a7229b5b0b |
9
.coveragerc
Normal file
9
.coveragerc
Normal file
@@ -0,0 +1,9 @@
|
||||
[run]
|
||||
source = ./InvenTree
|
||||
omit =
|
||||
InvenTree/manage.py
|
||||
InvenTree/setup.py
|
||||
InvenTree/InvenTree/middleware.py
|
||||
InvenTree/InvenTree/utils.py
|
||||
InvenTree/InvenTree/wsgi.py
|
||||
InvenTree/users/apps.py
|
||||
@@ -1,25 +0,0 @@
|
||||
env:
|
||||
commonjs: false
|
||||
browser: true
|
||||
es2021: true
|
||||
jquery: true
|
||||
extends:
|
||||
- google
|
||||
parserOptions:
|
||||
ecmaVersion: 12
|
||||
rules:
|
||||
no-var: off
|
||||
guard-for-in: off
|
||||
no-trailing-spaces: off
|
||||
camelcase: off
|
||||
padded-blocks: off
|
||||
prefer-const: off
|
||||
max-len: off
|
||||
require-jsdoc: off
|
||||
valid-jsdoc: off
|
||||
no-multiple-empty-lines: off
|
||||
comma-dangle: off
|
||||
prefer-spread: off
|
||||
indent:
|
||||
- error
|
||||
- 4
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -8,4 +8,4 @@
|
||||
*.yaml text
|
||||
*.conf text
|
||||
*.sh text eol=lf
|
||||
*.js text
|
||||
*.js text
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
patreon: inventree
|
||||
ko_fi: inventree
|
||||
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Create a bug report to help us improve InvenTree!
|
||||
title: "[BUG] Enter bug description"
|
||||
labels: bug, question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!---
|
||||
Everything inside these brackets is hidden - please remove them where you fill out information.
|
||||
--->
|
||||
|
||||
|
||||
**Describe the bug**
|
||||
<!---
|
||||
A clear and concise description of what the bug is.
|
||||
--->
|
||||
|
||||
**Steps to Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
<!---
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
--->
|
||||
|
||||
**Expected behavior**
|
||||
<!---
|
||||
A clear and concise description of what you expected to happen.
|
||||
--->
|
||||
|
||||
<!---
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
--->
|
||||
|
||||
**Deployment Method**
|
||||
- [ ] Docker
|
||||
- [ ] Bare Metal
|
||||
|
||||
**Version Information**
|
||||
<!---
|
||||
You can get this by going to the "About InvenTree" section in the upper right corner and clicking on to the "copy version information"
|
||||
--->
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FR]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request the result of a bug?**
|
||||
Please link it here.
|
||||
|
||||
**Problem**
|
||||
A clear and concise description of what the problem is. e.g. I'm always frustrated when [...]
|
||||
|
||||
**Suggested solution**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Examples of other systems**
|
||||
Show how other software handles your FR if you have examples.
|
||||
|
||||
**Do you want to develop this?**
|
||||
If so please describe briefly how you would like to implement it (so we can give advice) and if you have experience in the needed technology (you do not need to be a pro - this is just as a information for us).
|
||||
37
.github/workflows/check_translations.yaml
vendored
37
.github/workflows/check_translations.yaml
vendored
@@ -1,37 +0,0 @@
|
||||
name: Check Translations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- l10
|
||||
pull_request:
|
||||
branches:
|
||||
- l10
|
||||
|
||||
jobs:
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Test Translations
|
||||
run: invoke translate
|
||||
- name: Check Migration Files
|
||||
run: python3 ci/check_migration_files.py
|
||||
60
.github/workflows/coverage.yaml
vendored
Normal file
60
.github/workflows/coverage.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
# Perform CI checks, and calculate code coverage
|
||||
|
||||
name: SQLite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
# Run tests on SQLite database
|
||||
# These tests are used for code coverage analysis
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke static
|
||||
- name: Coverage Tests
|
||||
run: |
|
||||
invoke coverage
|
||||
- name: Data Import Export
|
||||
run: |
|
||||
invoke migrate
|
||||
invoke import-fixtures
|
||||
invoke export-records -f data.json
|
||||
rm test_db.sqlite
|
||||
invoke migrate
|
||||
invoke import-records -f data.json
|
||||
invoke import-records -f data.json
|
||||
- name: Test Translations
|
||||
run: invoke translate
|
||||
- name: Check Migration Files
|
||||
run: python3 ci/check_migration_files.py
|
||||
- name: Upload Coverage Report
|
||||
run: coveralls
|
||||
@@ -1,12 +1,11 @@
|
||||
# Build and push docker image on push to 'stable' branch
|
||||
# Docker build will be uploaded to dockerhub with the 'inventree:stable' tag
|
||||
# Build and push latest docker image on push to master branch
|
||||
|
||||
name: Docker Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'stable'
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -16,9 +15,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check version number
|
||||
run: |
|
||||
python3 ci/check_version_number.py --release
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
@@ -35,8 +31,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
target: production
|
||||
build-args:
|
||||
branch=stable
|
||||
tags: inventree/inventree:stable
|
||||
repository: inventree/inventree
|
||||
tags: inventree/inventree:latest
|
||||
- name: Image Digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
51
.github/workflows/docker_latest.yaml
vendored
51
.github/workflows/docker_latest.yaml
vendored
@@ -1,51 +0,0 @@
|
||||
# Build and push latest docker image on push to master branch
|
||||
|
||||
name: Docker Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check version number
|
||||
run: |
|
||||
python3 ci/check_version_number.py --dev
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
cd docker
|
||||
docker-compose build
|
||||
docker-compose run inventree-dev-server invoke update
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
cd docker
|
||||
docker-compose up -d
|
||||
docker-compose run inventree-dev-server invoke wait
|
||||
docker-compose run inventree-dev-server invoke test
|
||||
docker-compose down
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to Dockerhub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./docker
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
target: production
|
||||
tags: inventree/inventree:latest
|
||||
- name: Image Digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
@@ -1,5 +1,4 @@
|
||||
# Publish docker images to dockerhub on a tagged release
|
||||
# Docker build will be uploaded to dockerhub with the 'invetree:<tag>' tag
|
||||
# Publish docker images to dockerhub
|
||||
|
||||
name: Docker Publish
|
||||
|
||||
@@ -14,9 +13,6 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Check Release tag
|
||||
run: |
|
||||
python3 ci/check_version_number.py --release --tag ${{ github.event.release.tag_name }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
@@ -33,6 +29,5 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
target: production
|
||||
build-args:
|
||||
tag=${{ github.event.release.tag_name }}
|
||||
repository: inventree/inventree
|
||||
tags: inventree/inventree:${{ github.event.release.tag_name }}
|
||||
28
.github/workflows/javascript.yaml
vendored
Normal file
28
.github/workflows/javascript.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Check javascript template files
|
||||
|
||||
name: Javascript Templates
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
javascript:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check Files
|
||||
run: |
|
||||
cd ci
|
||||
python check_js_templates.py
|
||||
|
||||
67
.github/workflows/mysql.yaml
vendored
Normal file
67
.github/workflows/mysql.yaml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# MySQL Unit Testing
|
||||
|
||||
name: MySQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
# Database backend configuration
|
||||
INVENTREE_DB_ENGINE: django.db.backends.mysql
|
||||
INVENTREE_DB_NAME: inventree
|
||||
INVENTREE_DB_USER: root
|
||||
INVENTREE_DB_PASSWORD: password
|
||||
INVENTREE_DB_HOST: '127.0.0.1'
|
||||
INVENTREE_DB_PORT: 3306
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:latest
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: inventree
|
||||
MYSQL_USER: inventree
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get install mysql-server libmysqlclient-dev
|
||||
pip3 install invoke
|
||||
pip3 install mysqlclient
|
||||
invoke install
|
||||
- name: Run Tests
|
||||
run: invoke test
|
||||
- name: Data Import Export
|
||||
run: |
|
||||
invoke migrate
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-fixtures
|
||||
invoke export-records -f data.json
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-records -f data.json
|
||||
invoke import-records -f data.json
|
||||
63
.github/workflows/postgresql.yaml
vendored
Normal file
63
.github/workflows/postgresql.yaml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
# PostgreSQL Unit Testing
|
||||
|
||||
name: PostgreSQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
# Database backend configuration
|
||||
INVENTREE_DB_ENGINE: django.db.backends.postgresql
|
||||
INVENTREE_DB_NAME: inventree
|
||||
INVENTREE_DB_USER: inventree
|
||||
INVENTREE_DB_PASSWORD: password
|
||||
INVENTREE_DB_HOST: '127.0.0.1'
|
||||
INVENTREE_DB_PORT: 5432
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: inventree
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get install libpq-dev
|
||||
pip3 install invoke
|
||||
pip3 install psycopg2
|
||||
invoke install
|
||||
- name: Run Tests
|
||||
run: invoke test
|
||||
- name: Data Import Export
|
||||
run: |
|
||||
invoke migrate
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-fixtures
|
||||
invoke export-records -f data.json
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-records -f data.json
|
||||
invoke import-records -f data.json
|
||||
49
.github/workflows/python.yaml
vendored
Normal file
49
.github/workflows/python.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# Run python library tests whenever code is pushed to master
|
||||
|
||||
name: Python Bindings
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
python:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||
INVENTREE_DB_ENGINE: 'sqlite3'
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install InvenTree
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3-dev python3-pip python3-venv
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke migrate
|
||||
- name: Download Python Code
|
||||
run: |
|
||||
git clone --depth 1 https://github.com/inventree/inventree-python ./inventree-python
|
||||
- name: Start Server
|
||||
run: |
|
||||
invoke import-records -f ./inventree-python/test/test_data.json
|
||||
invoke server -a 127.0.0.1:8000 &
|
||||
sleep 60
|
||||
- name: Run Tests
|
||||
run: |
|
||||
cd inventree-python
|
||||
invoke test
|
||||
|
||||
321
.github/workflows/qc_checks.yaml
vendored
321
.github/workflows/qc_checks.yaml
vendored
@@ -1,321 +0,0 @@
|
||||
# Checks for each PR / push
|
||||
|
||||
name: QC checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
env:
|
||||
python_version: 3.9
|
||||
node_version: 16
|
||||
|
||||
server_start_sleep: 60
|
||||
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_DB_ENGINE: sqlite3
|
||||
INVENTREE_DB_NAME: inventree
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
|
||||
jobs:
|
||||
pep_style:
|
||||
name: PEP style (python)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install flake8==3.8.3
|
||||
pip install pep8-naming==0.11.1
|
||||
- name: flake8
|
||||
run: |
|
||||
flake8 InvenTree
|
||||
|
||||
javascript:
|
||||
name: javascript template files
|
||||
needs: pep_style
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install node.js ${{ env.node_version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.node_version }}
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke static
|
||||
- name: Check Templated Files
|
||||
run: |
|
||||
cd ci
|
||||
python check_js_templates.py
|
||||
- name: Lint Javascript Files
|
||||
run: |
|
||||
invoke render-js-files
|
||||
npx eslint js_tmp/*.js
|
||||
|
||||
html:
|
||||
name: html template files
|
||||
needs: pep_style
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install node.js ${{ env.node_version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.node_version }}
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke static
|
||||
- name: Check HTML Files
|
||||
run: |
|
||||
npx markuplint InvenTree/build/templates/build/*.html
|
||||
npx markuplint InvenTree/company/templates/company/*.html
|
||||
npx markuplint InvenTree/order/templates/order/*.html
|
||||
npx markuplint InvenTree/part/templates/part/*.html
|
||||
npx markuplint InvenTree/stock/templates/stock/*.html
|
||||
npx markuplint InvenTree/templates/*.html
|
||||
npx markuplint InvenTree/templates/InvenTree/*.html
|
||||
npx markuplint InvenTree/templates/InvenTree/settings/*.html
|
||||
|
||||
python:
|
||||
name: python bindings
|
||||
needs: pep_style
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
wrapper_name: inventree-python
|
||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
|
||||
INVENTREE_MEDIA_ROOT: ../test_inventree_media
|
||||
INVENTREE_STATIC_ROOT: ../test_inventree_static
|
||||
INVENTREE_ADMIN_USER: testuser
|
||||
INVENTREE_ADMIN_PASSWORD: testpassword
|
||||
INVENTREE_ADMIN_EMAIL: test@test.com
|
||||
INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345
|
||||
INVENTREE_PYTHON_TEST_USERNAME: testuser
|
||||
INVENTREE_PYTHON_TEST_PASSWORD: testpassword
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install InvenTree
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3-dev python3-pip python3-venv
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke migrate
|
||||
- name: Download Python Code
|
||||
run: |
|
||||
git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
|
||||
- name: Start Server
|
||||
run: |
|
||||
invoke delete-data -f
|
||||
invoke import-fixtures
|
||||
invoke server -a 127.0.0.1:12345 &
|
||||
invoke wait
|
||||
- name: Run Tests
|
||||
run: |
|
||||
cd ${{ env.wrapper_name }}
|
||||
invoke check-server
|
||||
coverage run -m unittest discover -s test/
|
||||
|
||||
coverage:
|
||||
name: Sqlite / coverage
|
||||
needs: ['javascript', 'html']
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
INVENTREE_DB_NAME: ./inventree.sqlite
|
||||
INVENTREE_DB_ENGINE: sqlite3
|
||||
INVENTREE_PLUGINS_ENABLED: true
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
python -m pip install -U pip
|
||||
pip3 install invoke
|
||||
invoke update
|
||||
- name: Coverage Tests
|
||||
run: |
|
||||
invoke coverage
|
||||
- name: Data Import Export
|
||||
run: |
|
||||
invoke migrate
|
||||
invoke import-fixtures
|
||||
invoke export-records -f data.json
|
||||
rm inventree.sqlite
|
||||
invoke migrate
|
||||
invoke import-records -f data.json
|
||||
invoke import-records -f data.json
|
||||
- name: Test Translations
|
||||
run: invoke translate
|
||||
- name: Check Migration Files
|
||||
run: python3 ci/check_migration_files.py
|
||||
- name: Upload Coverage Report
|
||||
run: coveralls
|
||||
|
||||
postgres:
|
||||
name: Postgres
|
||||
needs: ['javascript', 'html']
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
env:
|
||||
INVENTREE_DB_ENGINE: django.db.backends.postgresql
|
||||
INVENTREE_DB_USER: inventree
|
||||
INVENTREE_DB_PASSWORD: password
|
||||
INVENTREE_DB_HOST: '127.0.0.1'
|
||||
INVENTREE_DB_PORT: 5432
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_CACHE_HOST: localhost
|
||||
INVENTREE_PLUGINS_ENABLED: true
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: inventree
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libpq-dev gettext
|
||||
python -m pip install -U pip
|
||||
pip3 install invoke
|
||||
pip3 install psycopg2
|
||||
pip3 install django-redis>=5.0.0
|
||||
invoke update
|
||||
- name: Run Tests
|
||||
run: invoke test
|
||||
- name: Data Import Export
|
||||
run: |
|
||||
invoke migrate
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-fixtures
|
||||
invoke export-records -f data.json
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-records -f data.json
|
||||
invoke import-records -f data.json
|
||||
|
||||
mysql:
|
||||
name: MySql
|
||||
needs: ['javascript', 'html']
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
env:
|
||||
# Database backend configuration
|
||||
INVENTREE_DB_ENGINE: django.db.backends.mysql
|
||||
INVENTREE_DB_USER: root
|
||||
INVENTREE_DB_PASSWORD: password
|
||||
INVENTREE_DB_HOST: '127.0.0.1'
|
||||
INVENTREE_DB_PORT: 3306
|
||||
INVENTREE_DEBUG: info
|
||||
INVENTREE_PLUGINS_ENABLED: true
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:latest
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: ${{ env.INVENTREE_DB_NAME }}
|
||||
MYSQL_USER: inventree
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python ${{ env.python_version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.python_version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libmysqlclient-dev gettext
|
||||
python -m pip install -U pip
|
||||
pip3 install invoke
|
||||
pip3 install mysqlclient
|
||||
invoke update
|
||||
- name: Run Tests
|
||||
run: invoke test
|
||||
- name: Data Import Export
|
||||
run: |
|
||||
invoke migrate
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-fixtures
|
||||
invoke export-records -f data.json
|
||||
python3 ./InvenTree/manage.py flush --noinput
|
||||
invoke import-records -f data.json
|
||||
invoke import-records -f data.json
|
||||
25
.github/workflows/stale.yml
vendored
25
.github/workflows/stale.yml
vendored
@@ -1,25 +0,0 @@
|
||||
# Marks all issues that do not receive activity stale starting 2022
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '24 11 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue seems stale. Please react to show this is still important.'
|
||||
stale-pr-message: 'This PR seems stale. Please react to show this is still important.'
|
||||
stale-issue-label: 'inactive'
|
||||
stale-pr-label: 'inactive'
|
||||
start-date: '2022-01-01'
|
||||
exempt-all-milestones: true
|
||||
34
.github/workflows/style.yaml
vendored
Normal file
34
.github/workflows/style.yaml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Style Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
style:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.7]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install flake8==3.8.3
|
||||
pip install pep8-naming==0.11.1
|
||||
- name: flake8
|
||||
run: |
|
||||
flake8 InvenTree
|
||||
67
.github/workflows/translations.yml
vendored
67
.github/workflows/translations.yml
vendored
@@ -19,31 +19,42 @@ jobs:
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Make Translations
|
||||
run: |
|
||||
invoke translate
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git checkout -b l10_local
|
||||
git add "*.po"
|
||||
git commit -m "updated translation base"
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: l10
|
||||
force: true
|
||||
- uses: actions/checkout@v2
|
||||
- name: Get Current Translations
|
||||
run: |
|
||||
git fetch
|
||||
git checkout origin/l10 -- `git ls-tree origin/l10 -r --name-only | grep ".po"`
|
||||
git reset
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Make Translations
|
||||
run: |
|
||||
invoke translate
|
||||
- name: stash changes
|
||||
run: |
|
||||
git stash
|
||||
- name: Checkout Translation Branch
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
ref: l10
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git checkout stash -- .
|
||||
git reset
|
||||
git add "*.po"
|
||||
git commit -m "updated translation base"
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: l10
|
||||
|
||||
21
.github/workflows/version.yml
vendored
21
.github/workflows/version.yml
vendored
@@ -1,21 +0,0 @@
|
||||
# Checks version number
|
||||
name: version number
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
check_version:
|
||||
name: version number
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check version number
|
||||
run: |
|
||||
python3 ci/check_version_number.py --branch ${{ github.base_ref }}
|
||||
25
.github/workflows/welcome.yml
vendored
25
.github/workflows/welcome.yml
vendored
@@ -1,25 +0,0 @@
|
||||
# welcome new contributers
|
||||
name: Welcome
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.
|
||||
If you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).
|
||||
pr-message: |
|
||||
This is your first PR, welcome!
|
||||
Please check [Contributing](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md) to make sure your submission fits our general code-style and workflow.
|
||||
Make sure to document why this PR is needed and to link connected issues so we can review it faster.
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -37,7 +37,6 @@ local_settings.py
|
||||
|
||||
# Files used for testing
|
||||
dummy_image.*
|
||||
_tmp.csv
|
||||
|
||||
# Sphinx files
|
||||
docs/_build
|
||||
@@ -49,7 +48,6 @@ static_i18n
|
||||
|
||||
# Local config file
|
||||
config.yaml
|
||||
plugins.txt
|
||||
|
||||
# Default data file
|
||||
data.json
|
||||
@@ -68,23 +66,5 @@ secret_key.txt
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Temporary javascript files (used for testing)
|
||||
js_tmp/
|
||||
|
||||
# Development files
|
||||
dev/
|
||||
|
||||
# Locale stats file
|
||||
locale_stats.json
|
||||
|
||||
# node.js
|
||||
node_modules/
|
||||
|
||||
# maintenance locker
|
||||
maintenance_mode_state.txt
|
||||
|
||||
# plugin dev directory
|
||||
plugins/
|
||||
|
||||
# Compiled translation files
|
||||
*.mo
|
||||
dev/
|
||||
47
.gitpod.yml
47
.gitpod.yml
@@ -1,47 +0,0 @@
|
||||
tasks:
|
||||
- name: Setup django
|
||||
before: |
|
||||
export INVENTREE_DB_ENGINE='sqlite3'
|
||||
export INVENTREE_DB_NAME='/workspace/InvenTree/dev/database.sqlite3'
|
||||
export INVENTREE_MEDIA_ROOT='/workspace/InvenTree/inventree-data/media'
|
||||
export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static'
|
||||
export PIP_USER='no'
|
||||
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install invoke
|
||||
inv install
|
||||
mkdir dev
|
||||
inv update
|
||||
gp sync-done setup_server
|
||||
|
||||
- name: Start server
|
||||
init: gp sync-await setup_server
|
||||
command: |
|
||||
gp sync-await setup_server
|
||||
export INVENTREE_DB_ENGINE='sqlite3'
|
||||
export INVENTREE_DB_NAME='/workspace/InvenTree/dev/database.sqlite3'
|
||||
export INVENTREE_MEDIA_ROOT='/workspace/InvenTree/inventree-data/media'
|
||||
export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static'
|
||||
|
||||
source venv/bin/activate
|
||||
rm /workspace/InvenTree/inventree-data -r
|
||||
git clone https://github.com/inventree/demo-dataset /workspace/InvenTree/inventree-data
|
||||
invoke delete-data -f
|
||||
invoke import-records -f /workspace/InvenTree/inventree-data/inventree_data.json
|
||||
|
||||
inv server
|
||||
|
||||
# List the ports to expose. Learn more https://www.gitpod.io/docs/config-ports/
|
||||
ports:
|
||||
- port: 8000
|
||||
onOpen: open-preview
|
||||
|
||||
github:
|
||||
prebuilds:
|
||||
master: true
|
||||
pullRequests: false
|
||||
pullRequestsFromForks: true
|
||||
addBadge: true
|
||||
addLabel: gitpod-ready
|
||||
addCheck: false
|
||||
@@ -1,19 +0,0 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: mixed-line-ending
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: '4.0.1'
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: '5.10.1'
|
||||
hooks:
|
||||
- id: isort
|
||||
163
CONTRIBUTING.md
163
CONTRIBUTING.md
@@ -1,168 +1,29 @@
|
||||
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
|
||||
Contributions to InvenTree are welcomed - please follow the guidelines below.
|
||||
|
||||
## Setup
|
||||
## Feature Branches
|
||||
|
||||
Please run `invoke setup_dev` in the root directory of your InvenTree code base to set up your development setup before starting to contribute. This will install and set up pre-commit to run some checks before each commit and help reduce the style errors.
|
||||
No pushing to master! New featues must be submitted in a separate branch (one branch per feature).
|
||||
|
||||
## Branches and Versioning
|
||||
|
||||
InvenTree roughly follow the [GitLab flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
|
||||
|
||||
### Version Numbering
|
||||
|
||||
InvenTree version numbering follows the [semantic versioning](https://semver.org/) specification.
|
||||
|
||||
### Master Branch
|
||||
|
||||
The HEAD of the "main" or "master" branch of InvenTree represents the current "latest" state of code development.
|
||||
|
||||
- All feature branches are merged into master
|
||||
- All bug fixes are merged into master
|
||||
|
||||
**No pushing to master:** New featues must be submitted as a pull request from a separate branch (one branch per feature).
|
||||
|
||||
### Feature Branches
|
||||
|
||||
Feature branches should be branched *from* the *master* branch.
|
||||
|
||||
- One major feature per branch / pull request
|
||||
- Feature pull requests are merged back *into* the master branch
|
||||
- Features *may* also be merged into a release candidate branch
|
||||
|
||||
### Stable Branch
|
||||
|
||||
The HEAD of the "stable" branch represents the latest stable release code.
|
||||
|
||||
- Versioned releases are merged into the "stable" branch
|
||||
- Bug fix branches are made *from* the "stable" branch
|
||||
|
||||
#### Release Candidate Branches
|
||||
|
||||
- Release candidate branches are made from master, and merged into stable.
|
||||
- RC branches are targetted at a major/minor version e.g. "0.5"
|
||||
- When a release candidate branch is merged into *stable*, the release is tagged
|
||||
|
||||
#### Bugfix Branches
|
||||
|
||||
- If a bug is discovered in a tagged release version of InvenTree, a "bugfix" or "hotfix" branch should be made *from* that tagged release
|
||||
- When approved, the branch is merged back *into* stable, with an incremented PATCH number (e.g. 0.4.1 -> 0.4.2)
|
||||
- The bugfix *must* also be cherry picked into the *master* branch.
|
||||
|
||||
## Environment
|
||||
### Target version
|
||||
We are currently targeting:
|
||||
| Name | Minimum version |
|
||||
|---|---|
|
||||
| Python | 3.9 |
|
||||
| Django | 3.2 |
|
||||
|
||||
### Auto creating updates
|
||||
The following tools can be used to auto-upgrade syntax that was depreciated in new versions:
|
||||
```bash
|
||||
pip install pyupgrade
|
||||
pip install django-upgrade
|
||||
```
|
||||
|
||||
To update the codebase run the following script.
|
||||
```bash
|
||||
pyupgrade `find . -name "*.py"`
|
||||
django-upgrade --target-version 3.2 `find . -name "*.py"`
|
||||
```
|
||||
|
||||
## Credits
|
||||
If you add any new dependencies / libraries, they need to be added to [the docs](https://github.com/inventree/inventree-docs/blob/master/docs/credits.md). Please try to do that as timely as possible.
|
||||
|
||||
|
||||
## Migration Files
|
||||
## Include Migration Files
|
||||
|
||||
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate` and commit the migration files before submitting the PR.
|
||||
|
||||
*Note: A github action checks for unstaged migration files and will reject the PR if it finds any!*
|
||||
## Update Translation Files
|
||||
|
||||
## Unit Testing
|
||||
Any PRs which update translatable strings (i.e. text strings that will appear in the web-front UI) must also update the translation (locale) files to include hooks for the translated strings.
|
||||
|
||||
Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage for any new features is insufficient, or the overall code coverage is decreased.
|
||||
*This does not mean that all translations must be provided, but that the translation files must include locations for the translated strings to be written.*
|
||||
|
||||
The InvenTree code base makes use of [GitHub actions](https://github.com/features/actions) to run a suite of automated tests against the code base every time a new pull request is received. These actions include (but are not limited to):
|
||||
To perform this step, simply run `invoke translate` from the top level directory before submitting the PR.
|
||||
|
||||
- Checking Python and Javascript code against standard style guides
|
||||
- Running unit test suite
|
||||
- Automated building and pushing of docker images
|
||||
- Generating translation files
|
||||
## Testing
|
||||
|
||||
The various github actions can be found in the `./github/workflows` directory
|
||||
|
||||
## Code Style
|
||||
|
||||
Sumbitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
|
||||
Please write docstrings for each function and class - we follow the [google doc-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for python. Docstrings for general javascript code is encouraged! Docstyles are checked by `invoke style`.
|
||||
Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage is decreased.
|
||||
|
||||
## Documentation
|
||||
|
||||
New features or updates to existing features should be accompanied by user documentation. A PR with associated documentation should link to the matching PR at https://github.com/inventree/inventree-docs/
|
||||
|
||||
## Translations
|
||||
## Code Style
|
||||
|
||||
Any user-facing strings *must* be passed through the translation engine.
|
||||
|
||||
- InvenTree code is written in English
|
||||
- User translatable strings are provided in English as the primary language
|
||||
- Secondary language translations are provided [via Crowdin](https://crowdin.com/project/inventree)
|
||||
|
||||
*Note: Translation files are updated via GitHub actions - you do not need to compile translations files before submitting a pull request!*
|
||||
|
||||
### Python Code
|
||||
|
||||
For strings exposed via Python code, use the following format:
|
||||
|
||||
```python
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
user_facing_string = _('This string will be exposed to the translation engine!')
|
||||
```
|
||||
|
||||
### Templated Strings
|
||||
|
||||
HTML and javascript files are passed through the django templating engine. Translatable strings are implemented as follows:
|
||||
|
||||
```html
|
||||
{% load i18n %}
|
||||
|
||||
<span>{% trans "This string will be translated" %} - this string will not!</span>
|
||||
```
|
||||
|
||||
## Github use
|
||||
### Tags
|
||||
The tags describe issues and PRs in multiple areas:
|
||||
| Area | Name | Description |
|
||||
|---|---|---|
|
||||
| Type Labels | | |
|
||||
| | bug | Identifies a bug which needs to be addressed |
|
||||
| | dependency | Relates to a project dependency |
|
||||
| | duplicate | Duplicate of another issue or PR |
|
||||
| | enhancement | This is an suggested enhancement or new feature |
|
||||
| | help wanted | Assistance required |
|
||||
| | invalid | This issue or PR is considered invalid |
|
||||
| | inactive | Indicates lack of activity |
|
||||
| | question | This is a question |
|
||||
| | roadmap | This is a roadmap feature with no immediate plans for implementation |
|
||||
| | security | Relates to a security issue |
|
||||
| | starter | Good issue for a developer new to the project |
|
||||
| | wontfix | No work will be done against this issue or PR |
|
||||
| Feature Labels | | |
|
||||
| | API | Relates to the API |
|
||||
| | barcode | Barcode scanning and integration |
|
||||
| | build | Build orders |
|
||||
| | importer | Data importing and processing |
|
||||
| | order | Purchase order and sales orders |
|
||||
| | part | Parts |
|
||||
| | plugin | Plugin ecosystem |
|
||||
| | pricing | Pricing functionality |
|
||||
| | report | Report generation |
|
||||
| | stock | Stock item management |
|
||||
| | user interface | User interface |
|
||||
| Ecosystem Labels | | |
|
||||
| | demo | Relates to the InvenTree demo server or dataset |
|
||||
| | docker | Docker / docker-compose |
|
||||
| | CI | CI / unit testing ecosystem |
|
||||
| | setup | Relates to the InvenTree setup / installation process |
|
||||
Sumbitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
|
||||
|
||||
@@ -2,17 +2,33 @@
|
||||
Main JSON interface views
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import filters, permissions
|
||||
from rest_framework import filters
|
||||
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .status import is_worker_running
|
||||
from .version import (inventreeApiVersion, inventreeInstanceName,
|
||||
inventreeVersion)
|
||||
from .views import AjaxView
|
||||
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
||||
from .status import is_worker_running
|
||||
|
||||
from plugins import plugins as inventree_plugins
|
||||
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
logger.info("Loading action plugins...")
|
||||
action_plugins = inventree_plugins.load_action_plugins()
|
||||
|
||||
|
||||
class InfoView(AjaxView):
|
||||
@@ -30,7 +46,6 @@ class InfoView(AjaxView):
|
||||
'instance': inventreeInstanceName(),
|
||||
'apiVersion': inventreeApiVersion(),
|
||||
'worker_running': is_worker_running(),
|
||||
'plugins_enabled': settings.PLUGINS_ENABLED,
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
@@ -53,44 +68,6 @@ class NotFoundView(AjaxView):
|
||||
return JsonResponse(data, status=404)
|
||||
|
||||
|
||||
class APIDownloadMixin:
|
||||
"""
|
||||
Mixin for enabling a LIST endpoint to be downloaded a file.
|
||||
|
||||
To download the data, add the ?export=<fmt> to the query string.
|
||||
|
||||
The implementing class must provided a download_queryset method,
|
||||
e.g.
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
dataset = StockItemResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
|
||||
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
|
||||
date=datetime.now().strftime("%d-%b-%Y"),
|
||||
fmt=export_format
|
||||
)
|
||||
|
||||
return DownloadFile(filedata, filename)
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
export_format = request.query_params.get('export', None)
|
||||
|
||||
if export_format and export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
return self.download_queryset(queryset, export_format)
|
||||
|
||||
else:
|
||||
# Default to the parent class implementation
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
raise NotImplementedError("download_queryset method not implemented!")
|
||||
|
||||
|
||||
class AttachmentMixin:
|
||||
"""
|
||||
Mixin for creating attachment objects,
|
||||
@@ -111,3 +88,39 @@ class AttachmentMixin:
|
||||
attachment = serializer.save()
|
||||
attachment.user = self.request.user
|
||||
attachment.save()
|
||||
|
||||
|
||||
class ActionPluginView(APIView):
|
||||
"""
|
||||
Endpoint for running custom action plugins.
|
||||
"""
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
action = request.data.get('action', None)
|
||||
|
||||
data = request.data.get('data', None)
|
||||
|
||||
if action is None:
|
||||
return Response({
|
||||
'error': _("No action specified")
|
||||
})
|
||||
|
||||
for plugin_class in action_plugins:
|
||||
if plugin_class.action_name() == action:
|
||||
|
||||
plugin = plugin_class(request.user, data=data)
|
||||
|
||||
plugin.perform_action()
|
||||
|
||||
return Response(plugin.get_response())
|
||||
|
||||
# If we got to here, no matching action was found
|
||||
return Response({
|
||||
'error': _("No matching action found"),
|
||||
"action": action,
|
||||
})
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
Helper functions for performing API unit tests
|
||||
"""
|
||||
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.http.response import StreamingHttpResponse
|
||||
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
class UserMixin:
|
||||
class InvenTreeAPITestCase(APITestCase):
|
||||
"""
|
||||
Base class for running InvenTree API tests
|
||||
"""
|
||||
|
||||
# User information
|
||||
username = 'testuser'
|
||||
@@ -49,50 +46,38 @@ class UserMixin:
|
||||
self.user.is_staff = True
|
||||
|
||||
self.user.save()
|
||||
|
||||
# Assign all roles if set
|
||||
if self.roles == 'all':
|
||||
self.assignRole(assign_all=True)
|
||||
# else filter the roles
|
||||
else:
|
||||
for role in self.roles:
|
||||
self.assignRole(role)
|
||||
|
||||
for role in self.roles:
|
||||
self.assignRole(role)
|
||||
|
||||
if self.auto_login:
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def assignRole(self, role=None, assign_all: bool = False):
|
||||
def assignRole(self, role):
|
||||
"""
|
||||
Set the user roles for the registered user
|
||||
"""
|
||||
|
||||
# role is of the format 'rule.permission' e.g. 'part.add'
|
||||
|
||||
if not assign_all and role:
|
||||
rule, perm = role.split('.')
|
||||
rule, perm = role.split('.')
|
||||
|
||||
for ruleset in self.group.rule_sets.all():
|
||||
|
||||
if assign_all or ruleset.name == rule:
|
||||
if ruleset.name == rule:
|
||||
|
||||
if assign_all or perm == 'view':
|
||||
if perm == 'view':
|
||||
ruleset.can_view = True
|
||||
elif assign_all or perm == 'change':
|
||||
elif perm == 'change':
|
||||
ruleset.can_change = True
|
||||
elif assign_all or perm == 'delete':
|
||||
elif perm == 'delete':
|
||||
ruleset.can_delete = True
|
||||
elif assign_all or perm == 'add':
|
||||
elif perm == 'add':
|
||||
ruleset.can_add = True
|
||||
|
||||
ruleset.save()
|
||||
break
|
||||
|
||||
|
||||
class InvenTreeAPITestCase(UserMixin, APITestCase):
|
||||
"""
|
||||
Base class for running InvenTree API tests
|
||||
"""
|
||||
|
||||
def getActions(self, url):
|
||||
"""
|
||||
Return a dict of the 'actions' available at a given endpoint.
|
||||
@@ -121,12 +106,12 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
|
||||
|
||||
return response
|
||||
|
||||
def post(self, url, data, expected_code=None, format='json'):
|
||||
def post(self, url, data, expected_code=None):
|
||||
"""
|
||||
Issue a POST request
|
||||
"""
|
||||
|
||||
response = self.client.post(url, data=data, format=format)
|
||||
response = self.client.post(url, data=data, format='json')
|
||||
|
||||
if expected_code is not None:
|
||||
self.assertEqual(response.status_code, expected_code)
|
||||
@@ -145,122 +130,14 @@ class InvenTreeAPITestCase(UserMixin, APITestCase):
|
||||
|
||||
return response
|
||||
|
||||
def patch(self, url, data, expected_code=None, format='json'):
|
||||
def patch(self, url, data, files=None, expected_code=None):
|
||||
"""
|
||||
Issue a PATCH request
|
||||
"""
|
||||
|
||||
response = self.client.patch(url, data=data, format=format)
|
||||
response = self.client.patch(url, data=data, files=files, format='json')
|
||||
|
||||
if expected_code is not None:
|
||||
self.assertEqual(response.status_code, expected_code)
|
||||
|
||||
return response
|
||||
|
||||
def put(self, url, data, expected_code=None, format='json'):
|
||||
"""
|
||||
Issue a PUT request
|
||||
"""
|
||||
|
||||
response = self.client.put(url, data=data, format=format)
|
||||
|
||||
if expected_code is not None:
|
||||
self.assertEqual(response.status_code, expected_code)
|
||||
|
||||
return response
|
||||
|
||||
def options(self, url, expected_code=None):
|
||||
"""
|
||||
Issue an OPTIONS request
|
||||
"""
|
||||
|
||||
response = self.client.options(url, format='json')
|
||||
|
||||
if expected_code is not None:
|
||||
self.assertEqual(response.status_code, expected_code)
|
||||
|
||||
return response
|
||||
|
||||
def download_file(self, url, data, expected_code=None, expected_fn=None, decode=True):
|
||||
"""
|
||||
Download a file from the server, and return an in-memory file
|
||||
"""
|
||||
|
||||
response = self.client.get(url, data=data, format='json')
|
||||
|
||||
if expected_code is not None:
|
||||
self.assertEqual(response.status_code, expected_code)
|
||||
|
||||
# Check that the response is of the correct type
|
||||
if not isinstance(response, StreamingHttpResponse):
|
||||
raise ValueError("Response is not a StreamingHttpResponse object as expected")
|
||||
|
||||
# Extract filename
|
||||
disposition = response.headers['Content-Disposition']
|
||||
|
||||
result = re.search(r'attachment; filename="([\w.]+)"', disposition)
|
||||
|
||||
fn = result.groups()[0]
|
||||
|
||||
if expected_fn is not None:
|
||||
self.assertEqual(expected_fn, fn)
|
||||
|
||||
if decode:
|
||||
# Decode data and return as StringIO file object
|
||||
fo = io.StringIO()
|
||||
fo.name = fo
|
||||
fo.write(response.getvalue().decode('UTF-8'))
|
||||
else:
|
||||
# Return a a BytesIO file object
|
||||
fo = io.BytesIO()
|
||||
fo.name = fn
|
||||
fo.write(response.getvalue())
|
||||
|
||||
fo.seek(0)
|
||||
|
||||
return fo
|
||||
|
||||
def process_csv(self, fo, delimiter=',', required_cols=None, excluded_cols=None, required_rows=None):
|
||||
"""
|
||||
Helper function to process and validate a downloaded csv file
|
||||
"""
|
||||
|
||||
# Check that the correct object type has been passed
|
||||
self.assertTrue(isinstance(fo, io.StringIO))
|
||||
|
||||
fo.seek(0)
|
||||
|
||||
reader = csv.reader(fo, delimiter=delimiter)
|
||||
|
||||
headers = []
|
||||
rows = []
|
||||
|
||||
for idx, row in enumerate(reader):
|
||||
if idx == 0:
|
||||
headers = row
|
||||
else:
|
||||
rows.append(row)
|
||||
|
||||
if required_cols is not None:
|
||||
for col in required_cols:
|
||||
self.assertIn(col, headers)
|
||||
|
||||
if excluded_cols is not None:
|
||||
for col in excluded_cols:
|
||||
self.assertNotIn(col, headers)
|
||||
|
||||
if required_rows is not None:
|
||||
self.assertEqual(len(rows), required_rows)
|
||||
|
||||
# Return the file data as a list of dict items, based on the headers
|
||||
data = []
|
||||
|
||||
for row in rows:
|
||||
entry = {}
|
||||
|
||||
for idx, col in enumerate(headers):
|
||||
entry[col] = row[idx]
|
||||
|
||||
data.append(entry)
|
||||
|
||||
return data
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
"""
|
||||
InvenTree API version information
|
||||
"""
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 50
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v50 -> 2022-05-18 : https://github.com/inventree/InvenTree/pull/2912
|
||||
- Implement Attachments for manufacturer parts
|
||||
|
||||
v49 -> 2022-05-09 : https://github.com/inventree/InvenTree/pull/2957
|
||||
- Allows filtering of plugin list by 'active' status
|
||||
- Allows filtering of plugin list by 'mixin' support
|
||||
- Adds endpoint to "identify" or "locate" stock items and locations (using plugins)
|
||||
|
||||
v48 -> 2022-05-12 : https://github.com/inventree/InvenTree/pull/2977
|
||||
- Adds "export to file" functionality for PurchaseOrder API endpoint
|
||||
- Adds "export to file" functionality for SalesOrder API endpoint
|
||||
- Adds "export to file" functionality for BuildOrder API endpoint
|
||||
|
||||
v47 -> 2022-05-10 : https://github.com/inventree/InvenTree/pull/2964
|
||||
- Fixes barcode API error response when scanning a StockItem which does not exist
|
||||
- Fixes barcode API error response when scanning a StockLocation which does not exist
|
||||
|
||||
v46 -> 2022-05-09
|
||||
- Fixes read permissions on settings API
|
||||
- Allows non-staff users to read global settings via the API
|
||||
|
||||
v45 -> 2022-05-08 : https://github.com/inventree/InvenTree/pull/2944
|
||||
- Settings are now accessed via the API using their unique key, not their PK
|
||||
- This allows the settings to be accessed without prior knowledge of the PK
|
||||
|
||||
v44 -> 2022-05-04 : https://github.com/inventree/InvenTree/pull/2931
|
||||
- Converting more server-side rendered forms to the API
|
||||
- Exposes more core functionality to API endpoints
|
||||
|
||||
v43 -> 2022-04-26 : https://github.com/inventree/InvenTree/pull/2875
|
||||
- Adds API detail endpoint for PartSalePrice model
|
||||
- Adds API detail endpoint for PartInternalPrice model
|
||||
|
||||
v42 -> 2022-04-26 : https://github.com/inventree/InvenTree/pull/2833
|
||||
- Adds variant stock information to the Part and BomItem serializers
|
||||
|
||||
v41 -> 2022-04-26
|
||||
- Fixes 'variant_of' filter for Part list endpoint
|
||||
|
||||
v40 -> 2022-04-19
|
||||
- Adds ability to filter StockItem list by "tracked" parameter
|
||||
- This checks the serial number or batch code fields
|
||||
|
||||
v39 -> 2022-04-18
|
||||
- Adds ability to filter StockItem list by "has_batch" parameter
|
||||
|
||||
v38 -> 2022-04-14 : https://github.com/inventree/InvenTree/pull/2828
|
||||
- Adds the ability to include stock test results for "installed items"
|
||||
|
||||
v37 -> 2022-04-07 : https://github.com/inventree/InvenTree/pull/2806
|
||||
- Adds extra stock availability information to the BomItem serializer
|
||||
|
||||
v36 -> 2022-04-03
|
||||
- Adds ability to filter part list endpoint by unallocated_stock argument
|
||||
|
||||
v35 -> 2022-04-01 : https://github.com/inventree/InvenTree/pull/2797
|
||||
- Adds stock allocation information to the Part API
|
||||
- Adds calculated field for "unallocated_quantity"
|
||||
|
||||
v34 -> 2022-03-25
|
||||
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
|
||||
|
||||
v33 -> 2022-03-24
|
||||
- Adds "plugins_enabled" information to root API endpoint
|
||||
|
||||
v32 -> 2022-03-19
|
||||
- Adds "parameters" detail to Part API endpoint (use ¶meters=true)
|
||||
- Adds ability to filter PartParameterTemplate API by Part instance
|
||||
- Adds ability to filter PartParameterTemplate API by PartCategory instance
|
||||
|
||||
v31 -> 2022-03-14
|
||||
- Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints
|
||||
|
||||
v30 -> 2022-03-09
|
||||
- Adds "exclude_location" field to BuildAutoAllocation API endpoint
|
||||
- Allows BuildItem API endpoint to be filtered by BomItem relation
|
||||
|
||||
v29 -> 2022-03-08
|
||||
- Adds "scheduling" endpoint for predicted stock scheduling information
|
||||
|
||||
v28 -> 2022-03-04
|
||||
- Adds an API endpoint for auto allocation of stock items against a build order
|
||||
- Ref: https://github.com/inventree/InvenTree/pull/2713
|
||||
|
||||
v27 -> 2022-02-28
|
||||
- Adds target_date field to individual line items for purchase orders and sales orders
|
||||
|
||||
v26 -> 2022-02-17
|
||||
- Adds API endpoint for uploading a BOM file and extracting data
|
||||
|
||||
v25 -> 2022-02-17
|
||||
- Adds ability to filter "part" list endpoint by "in_bom_for" argument
|
||||
|
||||
v24 -> 2022-02-10
|
||||
- Adds API endpoint for deleting (cancelling) build order outputs
|
||||
|
||||
v23 -> 2022-02-02
|
||||
- Adds API endpoints for managing plugin classes
|
||||
- Adds API endpoints for managing plugin settings
|
||||
|
||||
v22 -> 2021-12-20
|
||||
- Adds API endpoint to "merge" multiple stock items
|
||||
|
||||
v21 -> 2021-12-04
|
||||
- Adds support for multiple "Shipments" against a SalesOrder
|
||||
- Refactors process for stock allocation against a SalesOrder
|
||||
|
||||
v20 -> 2021-12-03
|
||||
- Adds ability to filter POLineItem endpoint by "base_part"
|
||||
- Adds optional "order_detail" to POLineItem list endpoint
|
||||
|
||||
v19 -> 2021-12-02
|
||||
- Adds the ability to filter the StockItem API by "part_tree"
|
||||
- Returns only stock items which match a particular part.tree_id field
|
||||
|
||||
v18 -> 2021-11-15
|
||||
- Adds the ability to filter BomItem API by "uses" field
|
||||
- This returns a list of all BomItems which "use" the specified part
|
||||
- Includes inherited BomItem objects
|
||||
|
||||
v17 -> 2021-11-09
|
||||
- Adds API endpoints for GLOBAL and USER settings objects
|
||||
- Ref: https://github.com/inventree/InvenTree/pull/2275
|
||||
|
||||
v16 -> 2021-10-17
|
||||
- Adds API endpoint for completing build order outputs
|
||||
|
||||
v15 -> 2021-10-06
|
||||
- Adds detail endpoint for SalesOrderAllocation model
|
||||
- Allows use of the API forms interface for adjusting SalesOrderAllocation objects
|
||||
|
||||
v14 -> 2021-10-05
|
||||
- Stock adjustment actions API is improved, using native DRF serializer support
|
||||
- However adjustment actions now only support 'pk' as a lookup field
|
||||
|
||||
v13 -> 2021-10-05
|
||||
- Adds API endpoint to allocate stock items against a BuildOrder
|
||||
- Updates StockItem API with improved filtering against BomItem data
|
||||
|
||||
v12 -> 2021-09-07
|
||||
- Adds API endpoint to receive stock items against a PurchaseOrder
|
||||
|
||||
v11 -> 2021-08-26
|
||||
- Adds "units" field to PartBriefSerializer
|
||||
- This allows units to be introspected from the "part_detail" field in the StockItem serializer
|
||||
|
||||
v10 -> 2021-08-23
|
||||
- Adds "purchase_price_currency" to StockItem serializer
|
||||
- Adds "purchase_price_string" to StockItem serializer
|
||||
- Purchase price is now writable for StockItem serializer
|
||||
|
||||
v9 -> 2021-08-09
|
||||
- Adds "price_string" to part pricing serializers
|
||||
|
||||
v8 -> 2021-07-19
|
||||
- Refactors the API interface for SupplierPart and ManufacturerPart models
|
||||
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
|
||||
|
||||
v7 -> 2021-07-03
|
||||
- Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716
|
||||
- API OPTIONS endpoints provide comprehensive field metedata
|
||||
- Multiple new API endpoints added for database models
|
||||
|
||||
v6 -> 2021-06-23
|
||||
- Part and Company images can now be directly uploaded via the REST API
|
||||
|
||||
v5 -> 2021-06-21
|
||||
- Adds API interface for manufacturer part parameters
|
||||
|
||||
v4 -> 2021-06-01
|
||||
- BOM items can now accept "variant stock" to be assigned against them
|
||||
- Many slight API tweaks were needed to get this to work properly!
|
||||
|
||||
v3 -> 2021-05-22:
|
||||
- The updated StockItem "history tracking" now uses a different interface
|
||||
|
||||
"""
|
||||
@@ -3,16 +3,11 @@
|
||||
import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from InvenTree.ready import isInTestMode, canAppAccessDatabase
|
||||
import InvenTree.tasks
|
||||
from InvenTree.ready import canAppAccessDatabase, isInTestMode
|
||||
|
||||
from .config import get_setting
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
@@ -23,84 +18,42 @@ class InvenTreeConfig(AppConfig):
|
||||
def ready(self):
|
||||
|
||||
if canAppAccessDatabase():
|
||||
|
||||
self.remove_obsolete_tasks()
|
||||
|
||||
self.start_background_tasks()
|
||||
|
||||
if not isInTestMode(): # pragma: no cover
|
||||
if not isInTestMode():
|
||||
self.update_exchange_rates()
|
||||
|
||||
self.collect_notification_methods()
|
||||
|
||||
if canAppAccessDatabase() or settings.TESTING_ENV:
|
||||
self.add_user_on_startup()
|
||||
|
||||
def remove_obsolete_tasks(self):
|
||||
"""
|
||||
Delete any obsolete scheduled tasks in the database
|
||||
"""
|
||||
|
||||
obsolete = [
|
||||
'InvenTree.tasks.delete_expired_sessions',
|
||||
'stock.tasks.delete_old_stock_items',
|
||||
]
|
||||
|
||||
try:
|
||||
from django_q.models import Schedule
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
return
|
||||
|
||||
# Remove any existing obsolete tasks
|
||||
Schedule.objects.filter(func__in=obsolete).delete()
|
||||
|
||||
def start_background_tasks(self):
|
||||
|
||||
try:
|
||||
from django_q.models import Schedule
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
except (AppRegistryNotReady):
|
||||
return
|
||||
|
||||
logger.info("Starting background tasks...")
|
||||
|
||||
# Remove successful task results from the database
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.delete_successful_tasks',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
# Check for InvenTree updates
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.check_for_updates',
|
||||
schedule_type=Schedule.DAILY
|
||||
)
|
||||
|
||||
# Heartbeat to let the server know the background worker is running
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.heartbeat',
|
||||
schedule_type=Schedule.MINUTES,
|
||||
minutes=15
|
||||
)
|
||||
|
||||
# Keep exchange rates up to date
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.update_exchange_rates',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
# Delete old error messages
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.delete_old_error_logs',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
# Delete old notification records
|
||||
InvenTree.tasks.schedule_task(
|
||||
'common.tasks.delete_old_notifications',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
def update_exchange_rates(self): # pragma: no cover
|
||||
def update_exchange_rates(self):
|
||||
"""
|
||||
Update exchange rates each time the server is started, *if*:
|
||||
|
||||
@@ -110,10 +63,10 @@ class InvenTreeConfig(AppConfig):
|
||||
|
||||
try:
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend
|
||||
|
||||
from common.settings import currency_code_default
|
||||
from datetime import datetime, timedelta
|
||||
from InvenTree.tasks import update_exchange_rates
|
||||
except AppRegistryNotReady: # pragma: no cover
|
||||
from common.settings import currency_code_default
|
||||
except AppRegistryNotReady:
|
||||
pass
|
||||
|
||||
base_currency = currency_code_default()
|
||||
@@ -125,18 +78,23 @@ class InvenTreeConfig(AppConfig):
|
||||
|
||||
last_update = backend.last_update
|
||||
|
||||
if last_update is None:
|
||||
if last_update is not None:
|
||||
delta = datetime.now().date() - last_update.date()
|
||||
if delta > timedelta(days=1):
|
||||
print(f"Last update was {last_update}")
|
||||
update = True
|
||||
else:
|
||||
# Never been updated
|
||||
logger.info("Exchange backend has never been updated")
|
||||
print("Exchange backend has never been updated")
|
||||
update = True
|
||||
|
||||
# Backend currency has changed?
|
||||
if base_currency != backend.base_currency:
|
||||
logger.info(f"Base currency changed from {backend.base_currency} to {base_currency}")
|
||||
if not base_currency == backend.base_currency:
|
||||
print(f"Base currency changed from {backend.base_currency} to {base_currency}")
|
||||
update = True
|
||||
|
||||
except (ExchangeBackend.DoesNotExist):
|
||||
logger.info("Exchange backend not found - updating")
|
||||
print("Exchange backend not found - updating")
|
||||
update = True
|
||||
|
||||
except:
|
||||
@@ -144,67 +102,4 @@ class InvenTreeConfig(AppConfig):
|
||||
return
|
||||
|
||||
if update:
|
||||
try:
|
||||
update_exchange_rates()
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating exchange rates: {e}")
|
||||
|
||||
def add_user_on_startup(self):
|
||||
"""Add a user on startup"""
|
||||
# stop if checks were already created
|
||||
if hasattr(settings, 'USER_ADDED') and settings.USER_ADDED:
|
||||
return
|
||||
|
||||
# get values
|
||||
add_user = get_setting(
|
||||
'INVENTREE_ADMIN_USER',
|
||||
settings.CONFIG.get('admin_user', False)
|
||||
)
|
||||
add_email = get_setting(
|
||||
'INVENTREE_ADMIN_EMAIL',
|
||||
settings.CONFIG.get('admin_email', False)
|
||||
)
|
||||
add_password = get_setting(
|
||||
'INVENTREE_ADMIN_PASSWORD',
|
||||
settings.CONFIG.get('admin_password', False)
|
||||
)
|
||||
|
||||
# check if all values are present
|
||||
set_variables = 0
|
||||
for tested_var in [add_user, add_email, add_password]:
|
||||
if tested_var:
|
||||
set_variables += 1
|
||||
|
||||
# no variable set -> do not try anything
|
||||
if set_variables == 0:
|
||||
settings.USER_ADDED = True
|
||||
return
|
||||
|
||||
# not all needed variables set
|
||||
if set_variables < 3:
|
||||
logger.warn('Not all required settings for adding a user on startup are present:\nINVENTREE_ADMIN_USER, INVENTREE_ADMIN_EMAIL, INVENTREE_ADMIN_PASSWORD')
|
||||
settings.USER_ADDED = True
|
||||
return
|
||||
|
||||
# good to go -> create user
|
||||
user = get_user_model()
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if user.objects.filter(username=add_user).exists():
|
||||
logger.info(f"User {add_user} already exists - skipping creation")
|
||||
else:
|
||||
new_user = user.objects.create_superuser(add_user, add_email, add_password)
|
||||
logger.info(f'User {str(new_user)} was created!')
|
||||
except IntegrityError as _e:
|
||||
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
|
||||
|
||||
# do not try again
|
||||
settings.USER_ADDED = True
|
||||
|
||||
def collect_notification_methods(self):
|
||||
"""
|
||||
Collect all notification methods
|
||||
"""
|
||||
from common.notifications import storage
|
||||
|
||||
storage.collect()
|
||||
update_exchange_rates()
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""
|
||||
Pull rendered copies of the templated
|
||||
only used for testing the js files! - This file is omited from coverage
|
||||
"""
|
||||
|
||||
import os # pragma: no cover
|
||||
import pathlib # pragma: no cover
|
||||
|
||||
from InvenTree.helpers import InvenTreeTestCase # pragma: no cover
|
||||
|
||||
|
||||
class RenderJavascriptFiles(InvenTreeTestCase): # pragma: no cover
|
||||
"""
|
||||
A unit test to "render" javascript files.
|
||||
|
||||
The server renders templated javascript files,
|
||||
we need the fully-rendered files for linting and static tests.
|
||||
"""
|
||||
|
||||
def download_file(self, filename, prefix):
|
||||
|
||||
url = os.path.join(prefix, filename)
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
output_dir = os.path.join(
|
||||
here,
|
||||
'..',
|
||||
'..',
|
||||
'js_tmp',
|
||||
)
|
||||
|
||||
output_dir = os.path.abspath(output_dir)
|
||||
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
|
||||
output_file = os.path.join(
|
||||
output_dir,
|
||||
filename,
|
||||
)
|
||||
|
||||
with open(output_file, 'wb') as output:
|
||||
output.write(response.content)
|
||||
|
||||
def download_files(self, subdir, prefix):
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
js_template_dir = os.path.join(
|
||||
here,
|
||||
'..',
|
||||
'templates',
|
||||
'js',
|
||||
)
|
||||
|
||||
directory = os.path.join(js_template_dir, subdir)
|
||||
|
||||
directory = os.path.abspath(directory)
|
||||
|
||||
js_files = pathlib.Path(directory).rglob('*.js')
|
||||
|
||||
n = 0
|
||||
|
||||
for f in js_files:
|
||||
js = os.path.basename(f)
|
||||
|
||||
self.download_file(js, prefix)
|
||||
|
||||
n += 1
|
||||
|
||||
return n
|
||||
|
||||
def test_render_files(self):
|
||||
"""
|
||||
Look for all javascript files
|
||||
"""
|
||||
|
||||
n = 0
|
||||
|
||||
print("Rendering javascript files...")
|
||||
|
||||
n += self.download_files('translated', '/js/i18n')
|
||||
n += self.download_files('dynamic', '/js/dynamic')
|
||||
|
||||
print(f"Rendered {n} javascript files.")
|
||||
@@ -1,89 +0,0 @@
|
||||
"""
|
||||
Helper functions for loading InvenTree configuration options
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def get_base_dir():
|
||||
""" Returns the base (top-level) InvenTree directory """
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def get_config_file():
|
||||
"""
|
||||
Returns the path of the InvenTree configuration file.
|
||||
|
||||
Note: It will be created it if does not already exist!
|
||||
"""
|
||||
|
||||
base_dir = get_base_dir()
|
||||
|
||||
cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
|
||||
|
||||
if cfg_filename:
|
||||
cfg_filename = cfg_filename.strip()
|
||||
cfg_filename = os.path.abspath(cfg_filename)
|
||||
else:
|
||||
# Config file is *not* specified - use the default
|
||||
cfg_filename = os.path.join(base_dir, 'config.yaml')
|
||||
|
||||
if not os.path.exists(cfg_filename):
|
||||
print("InvenTree configuration file 'config.yaml' not found - creating default file")
|
||||
|
||||
cfg_template = os.path.join(base_dir, "config_template.yaml")
|
||||
shutil.copyfile(cfg_template, cfg_filename)
|
||||
print(f"Created config file {cfg_filename}")
|
||||
|
||||
return cfg_filename
|
||||
|
||||
|
||||
def get_plugin_file():
|
||||
"""
|
||||
Returns the path of the InvenTree plugins specification file.
|
||||
|
||||
Note: It will be created if it does not already exist!
|
||||
"""
|
||||
# Check if the plugin.txt file (specifying required plugins) is specified
|
||||
PLUGIN_FILE = os.getenv('INVENTREE_PLUGIN_FILE')
|
||||
|
||||
if not PLUGIN_FILE:
|
||||
# If not specified, look in the same directory as the configuration file
|
||||
|
||||
config_dir = os.path.dirname(get_config_file())
|
||||
|
||||
PLUGIN_FILE = os.path.join(config_dir, 'plugins.txt')
|
||||
|
||||
if not os.path.exists(PLUGIN_FILE):
|
||||
logger.warning("Plugin configuration file does not exist")
|
||||
logger.info(f"Creating plugin file at '{PLUGIN_FILE}'")
|
||||
|
||||
# If opening the file fails (no write permission, for example), then this will throw an error
|
||||
with open(PLUGIN_FILE, 'w') as plugin_file:
|
||||
plugin_file.write("# InvenTree Plugins (uses PIP framework to install)\n\n")
|
||||
|
||||
return PLUGIN_FILE
|
||||
|
||||
|
||||
def get_setting(environment_var, backup_val, default_value=None):
|
||||
"""
|
||||
Helper function for retrieving a configuration setting value
|
||||
|
||||
- First preference is to look for the environment variable
|
||||
- Second preference is to look for the value of the settings file
|
||||
- Third preference is the default value
|
||||
"""
|
||||
|
||||
val = os.getenv(environment_var)
|
||||
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
if backup_val is not None:
|
||||
return backup_val
|
||||
|
||||
return default_value
|
||||
@@ -4,10 +4,12 @@
|
||||
Provides extra global data to all templates.
|
||||
"""
|
||||
|
||||
from InvenTree.status_codes import SalesOrderStatus, PurchaseOrderStatus
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||
from InvenTree.status_codes import StockHistoryCode
|
||||
|
||||
import InvenTree.status
|
||||
from InvenTree.status_codes import (BuildStatus, PurchaseOrderStatus,
|
||||
SalesOrderStatus, StockHistoryCode,
|
||||
StockStatus)
|
||||
|
||||
from users.models import RuleSet
|
||||
|
||||
|
||||
@@ -21,7 +23,7 @@ def health_status(request):
|
||||
|
||||
if request.path.endswith('.js'):
|
||||
# Do not provide to script requests
|
||||
return {} # pragma: no cover
|
||||
return {}
|
||||
|
||||
if hasattr(request, '_inventree_health_status'):
|
||||
# Do not duplicate efforts
|
||||
@@ -34,14 +36,9 @@ def health_status(request):
|
||||
'email_configured': InvenTree.status.is_email_configured(),
|
||||
}
|
||||
|
||||
# The following keys are required to denote system health
|
||||
health_keys = [
|
||||
'django_q_running',
|
||||
]
|
||||
|
||||
all_healthy = True
|
||||
|
||||
for k in health_keys:
|
||||
for k in status.keys():
|
||||
if status[k] is not True:
|
||||
all_healthy = False
|
||||
|
||||
@@ -93,7 +90,7 @@ def user_roles(request):
|
||||
}
|
||||
|
||||
if user.is_superuser:
|
||||
for ruleset in RuleSet.RULESET_MODELS.keys(): # pragma: no cover
|
||||
for ruleset in RuleSet.RULESET_MODELS.keys():
|
||||
roles[ruleset] = {
|
||||
'view': True,
|
||||
'add': True,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
"""
|
||||
Custom exception handling for the DRF API
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.debug import ExceptionReporter
|
||||
|
||||
import rest_framework.views as drfviews
|
||||
from error_report.models import Error
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def exception_handler(exc, context):
|
||||
"""
|
||||
Custom exception handler for DRF framework.
|
||||
Ref: https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
|
||||
|
||||
Catches any errors not natively handled by DRF, and re-throws as an error DRF can handle
|
||||
"""
|
||||
|
||||
response = None
|
||||
|
||||
# Catch any django validation error, and re-throw a DRF validation error
|
||||
if isinstance(exc, DjangoValidationError):
|
||||
exc = DRFValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
# Default to the built-in DRF exception handler
|
||||
response = drfviews.exception_handler(exc, context)
|
||||
|
||||
if response is None:
|
||||
# DRF handler did not provide a default response for this exception
|
||||
|
||||
if settings.DEBUG:
|
||||
error_detail = str(exc)
|
||||
else:
|
||||
error_detail = _("Error details can be found in the admin panel")
|
||||
|
||||
response_data = {
|
||||
'error': type(exc).__name__,
|
||||
'error_class': str(type(exc)),
|
||||
'detail': error_detail,
|
||||
'path': context['request'].path,
|
||||
'status_code': 500,
|
||||
}
|
||||
|
||||
response = Response(response_data, status=500)
|
||||
|
||||
# Log the exception to the database, too
|
||||
kind, info, data = sys.exc_info()
|
||||
|
||||
Error.objects.create(
|
||||
kind=kind.__name__,
|
||||
info=info,
|
||||
data='\n'.join(traceback.format_exception(kind, info, data)),
|
||||
path=context['request'].path,
|
||||
html=ExceptionReporter(context['request'], kind, info, data).get_traceback_html(),
|
||||
)
|
||||
|
||||
if response is not None:
|
||||
# Convert errors returned under the label '__all__' to 'non_field_errors'
|
||||
if '__all__' in response.data:
|
||||
response.data['non_field_errors'] = response.data['__all__']
|
||||
del response.data['__all__']
|
||||
|
||||
return response
|
||||
@@ -1,13 +1,7 @@
|
||||
import ssl
|
||||
from urllib.error import URLError
|
||||
from urllib.request import urlopen
|
||||
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
import certifi
|
||||
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
|
||||
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
|
||||
|
||||
|
||||
class InvenTreeExchange(SimpleExchangeBackend):
|
||||
@@ -29,22 +23,6 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
||||
return {
|
||||
}
|
||||
|
||||
def get_response(self, **kwargs):
|
||||
"""
|
||||
Custom code to get response from server.
|
||||
Note: Adds a 5-second timeout
|
||||
"""
|
||||
|
||||
url = self.get_url(**kwargs)
|
||||
|
||||
try:
|
||||
context = ssl.create_default_context(cafile=certifi.where())
|
||||
response = urlopen(url, timeout=5, context=context)
|
||||
return response.read()
|
||||
except:
|
||||
# Returning None here will raise an error upstream
|
||||
return None
|
||||
|
||||
def update_rates(self, base_currency=currency_code_default()):
|
||||
|
||||
symbols = ','.join(currency_codes())
|
||||
@@ -52,14 +30,5 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
||||
try:
|
||||
super().update_rates(base=base_currency, symbols=symbols)
|
||||
# catch connection errors
|
||||
except URLError:
|
||||
except (HTTPError, URLError):
|
||||
print('Encountered connection error while updating')
|
||||
except OperationalError as e:
|
||||
if 'SerializationFailure' in e.__cause__.__class__.__name__:
|
||||
print('Serialization Failure while updating exchange rates')
|
||||
# We are just going to swallow this exception because the
|
||||
# exchange rates will be updated later by the scheduled task
|
||||
else:
|
||||
# Other operational errors probably are still show stoppers
|
||||
# so reraise them so that the log contains the stacktrace
|
||||
raise
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
""" Custom fields used in InvenTree """
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import sys
|
||||
|
||||
from .validators import allowable_url_schemes
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.forms.fields import URLField as FormURLField
|
||||
from django.db import models as models
|
||||
from django.core import validators
|
||||
from django import forms
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
from django.db import models as models
|
||||
from django.forms.fields import URLField as FormURLField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from djmoney.forms.fields import MoneyField
|
||||
from djmoney.models.fields import MoneyField as ModelMoneyField
|
||||
from djmoney.forms.fields import MoneyField
|
||||
from djmoney.models.validators import MinMoneyValidator
|
||||
|
||||
import InvenTree.helpers
|
||||
|
||||
from .validators import allowable_url_schemes
|
||||
|
||||
|
||||
class InvenTreeURLFormField(FormURLField):
|
||||
""" Custom URL form field with custom scheme validators """
|
||||
@@ -37,7 +41,7 @@ class InvenTreeURLField(models.URLField):
|
||||
|
||||
def money_kwargs():
|
||||
""" returns the database settings for MoneyFields """
|
||||
from common.settings import currency_code_default, currency_code_mappings
|
||||
from common.settings import currency_code_mappings, currency_code_default
|
||||
|
||||
kwargs = {}
|
||||
kwargs['currency_choices'] = currency_code_mappings()
|
||||
@@ -49,7 +53,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
||||
"""
|
||||
Custom MoneyField for clean migrations while using dynamic currency settings
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# detect if creating migration
|
||||
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
|
||||
@@ -127,7 +131,7 @@ def round_decimal(value, places):
|
||||
|
||||
class RoundingDecimalFormField(forms.DecimalField):
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
value = super(RoundingDecimalFormField, self).to_python(value)
|
||||
value = round_decimal(value, self.decimal_places)
|
||||
return value
|
||||
|
||||
@@ -145,7 +149,7 @@ class RoundingDecimalFormField(forms.DecimalField):
|
||||
|
||||
class RoundingDecimalField(models.DecimalField):
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
value = super(RoundingDecimalField, self).to_python(value)
|
||||
return round_decimal(value, self.decimal_places)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
|
||||
class InvenTreeOrderingFilter(OrderingFilter):
|
||||
"""
|
||||
Custom OrderingFilter class which allows aliased filtering of related fields.
|
||||
|
||||
To use, simply specify this filter in the "filter_backends" section.
|
||||
|
||||
filter_backends = [
|
||||
InvenTreeOrderingFilter,
|
||||
]
|
||||
|
||||
Then, specify a ordering_field_aliases attribute:
|
||||
|
||||
ordering_field_alises = {
|
||||
'name': 'part__part__name',
|
||||
'SKU': 'part__SKU',
|
||||
}
|
||||
"""
|
||||
|
||||
def get_ordering(self, request, queryset, view):
|
||||
|
||||
ordering = super().get_ordering(request, queryset, view)
|
||||
|
||||
aliases = getattr(view, 'ordering_field_aliases', None)
|
||||
|
||||
# Attempt to map ordering fields based on provided aliases
|
||||
if ordering is not None and aliases is not None:
|
||||
"""
|
||||
Ordering fields should be mapped to separate fields
|
||||
"""
|
||||
|
||||
ordering_initial = ordering
|
||||
ordering = []
|
||||
|
||||
for field in ordering_initial:
|
||||
|
||||
reverse = field.startswith('-')
|
||||
|
||||
if reverse:
|
||||
field = field[1:]
|
||||
|
||||
# Are aliases defined for this field?
|
||||
if field in aliases:
|
||||
alias = aliases[field]
|
||||
else:
|
||||
alias = field
|
||||
|
||||
"""
|
||||
Potentially, a single field could be "aliased" to multiple field,
|
||||
|
||||
(For example to enforce a particular ordering sequence)
|
||||
|
||||
e.g. to filter first by the integer value...
|
||||
|
||||
ordering_field_aliases = {
|
||||
"reference": ["integer_ref", "reference"]
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
if type(alias) is str:
|
||||
alias = [alias]
|
||||
elif type(alias) in [list, tuple]:
|
||||
pass
|
||||
else:
|
||||
# Unsupported alias type
|
||||
continue
|
||||
|
||||
for a in alias:
|
||||
if reverse:
|
||||
a = '-' + a
|
||||
|
||||
ordering.append(a)
|
||||
|
||||
return ordering
|
||||
@@ -2,32 +2,19 @@
|
||||
Helper forms which subclass Django forms to provide additional functionality
|
||||
"""
|
||||
|
||||
import logging
|
||||
from urllib.parse import urlencode
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from allauth.account.adapter import DefaultAccountAdapter
|
||||
from allauth.account.forms import SignupForm, set_form_field_order
|
||||
from allauth.exceptions import ImmediateHttpResponse
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from allauth_2fa.adapter import OTPAdapter
|
||||
from allauth_2fa.utils import user_has_valid_totp_device
|
||||
from crispy_forms.bootstrap import (AppendedText, Div, PrependedAppendedText,
|
||||
PrependedText, StrictButton)
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Field, Layout
|
||||
from crispy_forms.layout import Layout, Field
|
||||
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from part.models import PartCategory
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class HelperForm(forms.ModelForm):
|
||||
""" Provides simple integration of crispy_forms extension. """
|
||||
@@ -56,7 +43,7 @@ class HelperForm(forms.ModelForm):
|
||||
|
||||
def is_valid(self):
|
||||
|
||||
valid = super().is_valid()
|
||||
valid = super(HelperForm, self).is_valid()
|
||||
|
||||
return valid
|
||||
|
||||
@@ -148,15 +135,16 @@ class DeleteForm(forms.Form):
|
||||
|
||||
|
||||
class EditUserForm(HelperForm):
|
||||
"""
|
||||
Form for editing user information
|
||||
""" Form for editing user information
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'username',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email'
|
||||
]
|
||||
|
||||
|
||||
@@ -216,112 +204,3 @@ class SettingCategorySelectForm(forms.ModelForm):
|
||||
css_class='row',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# override allauth
|
||||
class CustomSignupForm(SignupForm):
|
||||
"""
|
||||
Override to use dynamic settings
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# check for two mail fields
|
||||
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
|
||||
self.fields["email2"] = forms.EmailField(
|
||||
label=_("Email (again)"),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"type": "email",
|
||||
"placeholder": _("Email address confirmation"),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
# check for two password fields
|
||||
if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'):
|
||||
self.fields.pop("password2")
|
||||
|
||||
# reorder fields
|
||||
set_form_field_order(self, ["username", "email", "email2", "password1", "password2", ])
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# check for two mail fields
|
||||
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
|
||||
email = cleaned_data.get("email")
|
||||
email2 = cleaned_data.get("email2")
|
||||
if (email and email2) and email != email2:
|
||||
self.add_error("email2", _("You must type the same email each time."))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class RegistratonMixin:
|
||||
"""
|
||||
Mixin to check if registration should be enabled
|
||||
"""
|
||||
def is_open_for_signup(self, request, *args, **kwargs):
|
||||
if settings.EMAIL_HOST and InvenTreeSetting.get_setting('LOGIN_ENABLE_REG', True):
|
||||
return super().is_open_for_signup(request, *args, **kwargs)
|
||||
return False
|
||||
|
||||
def save_user(self, request, user, form, commit=True):
|
||||
user = super().save_user(request, user, form)
|
||||
start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP')
|
||||
if start_group:
|
||||
try:
|
||||
group = Group.objects.get(id=start_group)
|
||||
user.groups.add(group)
|
||||
except Group.DoesNotExist:
|
||||
logger.error('The setting `SIGNUP_GROUP` contains an non existant group', start_group)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class CustomAccountAdapter(RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
|
||||
"""
|
||||
Override of adapter to use dynamic settings
|
||||
"""
|
||||
def send_mail(self, template_prefix, email, context):
|
||||
"""only send mail if backend configured"""
|
||||
if settings.EMAIL_HOST:
|
||||
return super().send_mail(template_prefix, email, context)
|
||||
return False
|
||||
|
||||
|
||||
class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
|
||||
"""
|
||||
Override of adapter to use dynamic settings
|
||||
"""
|
||||
def is_auto_signup_allowed(self, request, sociallogin):
|
||||
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True):
|
||||
return super().is_auto_signup_allowed(request, sociallogin)
|
||||
return False
|
||||
|
||||
# from OTPAdapter
|
||||
def has_2fa_enabled(self, user):
|
||||
"""Returns True if the user has 2FA configured."""
|
||||
return user_has_valid_totp_device(user)
|
||||
|
||||
def login(self, request, user):
|
||||
# Require two-factor authentication if it has been configured.
|
||||
if self.has_2fa_enabled(user):
|
||||
# Cast to string for the case when this is not a JSON serializable
|
||||
# object, e.g. a UUID.
|
||||
request.session['allauth_2fa_user_id'] = str(user.id)
|
||||
|
||||
redirect_url = reverse('two-factor-authenticate')
|
||||
# Add GET parameters to the URL if they exist.
|
||||
if request.GET:
|
||||
redirect_url += '?' + urlencode(request.GET)
|
||||
|
||||
raise ImmediateHttpResponse(
|
||||
response=HttpResponseRedirect(redirect_url)
|
||||
)
|
||||
|
||||
# Otherwise defer to the original allauth adapter.
|
||||
return super().login(request, user)
|
||||
|
||||
@@ -3,27 +3,27 @@ Provides helper functions used throughout the InvenTree project
|
||||
"""
|
||||
|
||||
import io
|
||||
import re
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.test import TestCase
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from wsgiref.util import FileWrapper
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.core.exceptions import ValidationError, FieldError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
import InvenTree.version
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from .settings import MEDIA_URL, STATIC_URL
|
||||
from common.settings import currency_code_default
|
||||
|
||||
from .api_tester import UserMixin
|
||||
from .settings import MEDIA_URL, STATIC_URL
|
||||
from djmoney.money import Money
|
||||
|
||||
|
||||
def getSetting(key, backup_value=None):
|
||||
@@ -69,35 +69,6 @@ def getStaticUrl(filename):
|
||||
return os.path.join(STATIC_URL, str(filename))
|
||||
|
||||
|
||||
def construct_absolute_url(*arg):
|
||||
"""
|
||||
Construct (or attempt to construct) an absolute URL from a relative URL.
|
||||
|
||||
This is useful when (for example) sending an email to a user with a link
|
||||
to something in the InvenTree web framework.
|
||||
|
||||
This requires the BASE_URL configuration option to be set!
|
||||
"""
|
||||
|
||||
base = str(InvenTreeSetting.get_setting('INVENTREE_BASE_URL'))
|
||||
|
||||
url = '/'.join(arg)
|
||||
|
||||
if not base:
|
||||
return url
|
||||
|
||||
# Strip trailing slash from base url
|
||||
if base.endswith('/'):
|
||||
base = base[:-1]
|
||||
|
||||
if url.startswith('/'):
|
||||
url = url[1:]
|
||||
|
||||
url = f"{base}/{url}"
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def getBlankImage():
|
||||
"""
|
||||
Return the qualified path for the 'blank image' placeholder.
|
||||
@@ -224,7 +195,7 @@ def increment(n):
|
||||
groups = result.groups()
|
||||
|
||||
# If we cannot match the regex, then simply return the provided value
|
||||
if len(groups) != 2:
|
||||
if not len(groups) == 2:
|
||||
return value
|
||||
|
||||
prefix, number = groups
|
||||
@@ -315,7 +286,7 @@ def WrapWithQuotes(text, quote='"'):
|
||||
return text
|
||||
|
||||
|
||||
def MakeBarcode(object_name, object_pk, object_data=None, **kwargs):
|
||||
def MakeBarcode(object_name, object_pk, object_data={}, **kwargs):
|
||||
""" Generate a string for a barcode. Adds some global InvenTree parameters.
|
||||
|
||||
Args:
|
||||
@@ -327,8 +298,6 @@ def MakeBarcode(object_name, object_pk, object_data=None, **kwargs):
|
||||
Returns:
|
||||
json string of the supplied data plus some other data
|
||||
"""
|
||||
if object_data is None:
|
||||
object_data = {}
|
||||
|
||||
url = kwargs.get('url', False)
|
||||
brief = kwargs.get('brief', True)
|
||||
@@ -375,15 +344,13 @@ def GetExportFormats():
|
||||
]
|
||||
|
||||
|
||||
def DownloadFile(data, filename, content_type='application/text', inline=False):
|
||||
"""
|
||||
Create a dynamic file for the user to download.
|
||||
def DownloadFile(data, filename, content_type='application/text'):
|
||||
""" Create a dynamic file for the user to download.
|
||||
|
||||
Args:
|
||||
data: Raw file data (string or bytes)
|
||||
filename: Filename for the file download
|
||||
content_type: Content type for the download
|
||||
inline: Download "inline" or as attachment? (Default = attachment)
|
||||
|
||||
Return:
|
||||
A StreamingHttpResponse object wrapping the supplied data
|
||||
@@ -398,57 +365,37 @@ def DownloadFile(data, filename, content_type='application/text', inline=False):
|
||||
|
||||
response = StreamingHttpResponse(wrapper, content_type=content_type)
|
||||
response['Content-Length'] = len(data)
|
||||
|
||||
disposition = "inline" if inline else "attachment"
|
||||
|
||||
response['Content-Disposition'] = f'{disposition}; filename={filename}'
|
||||
response['Content-Disposition'] = 'attachment; filename={f}'.format(f=filename)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
"""
|
||||
Attempt to extract serial numbers from an input string:
|
||||
|
||||
Requirements:
|
||||
- Serial numbers can be either strings, or integers
|
||||
- Serial numbers can be split by whitespace / newline / commma chars
|
||||
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
||||
- Serial numbers can be defined as ~ for getting the next available serial number
|
||||
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
||||
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||
def extract_serial_numbers(serials, expected_quantity):
|
||||
""" Attempt to extract serial numbers from an input string.
|
||||
- Serial numbers must be integer values
|
||||
- Serial numbers must be positive
|
||||
- Serial numbers can be split by whitespace / newline / commma chars
|
||||
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
||||
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
||||
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||
|
||||
Args:
|
||||
serials: input string with patterns
|
||||
expected_quantity: The number of (unique) serial numbers we expect
|
||||
next_number(int): the next possible serial number
|
||||
"""
|
||||
|
||||
serials = serials.strip()
|
||||
|
||||
# fill in the next serial number into the serial
|
||||
while '~' in serials:
|
||||
serials = serials.replace('~', str(next_number), 1)
|
||||
next_number += 1
|
||||
|
||||
# Split input string by whitespace or comma (,) characters
|
||||
groups = re.split(r"[\s,]+", serials)
|
||||
groups = re.split("[\s,]+", serials)
|
||||
|
||||
numbers = []
|
||||
errors = []
|
||||
|
||||
# Helper function to check for duplicated numbers
|
||||
def add_sn(sn):
|
||||
# Attempt integer conversion first, so numerical strings are never stored
|
||||
try:
|
||||
sn = int(sn)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if sn in numbers:
|
||||
errors.append(_('Duplicate serial: {sn}').format(sn=sn))
|
||||
# helpers
|
||||
def number_add(n):
|
||||
if n in numbers:
|
||||
errors.append(_('Duplicate serial: {n}').format(n=n))
|
||||
else:
|
||||
numbers.append(sn)
|
||||
numbers.append(n)
|
||||
|
||||
try:
|
||||
expected_quantity = int(expected_quantity)
|
||||
@@ -458,25 +405,15 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
if len(serials) == 0:
|
||||
raise ValidationError([_("Empty serial number string")])
|
||||
|
||||
# If the user has supplied the correct number of serials, don't process them for groups
|
||||
# just add them so any duplicates (or future validations) are checked
|
||||
if len(groups) == expected_quantity:
|
||||
for group in groups:
|
||||
add_sn(group)
|
||||
|
||||
if len(errors) > 0:
|
||||
raise ValidationError(errors)
|
||||
|
||||
return numbers
|
||||
|
||||
for group in groups:
|
||||
|
||||
group = group.strip()
|
||||
|
||||
# Hyphen indicates a range of numbers
|
||||
if '-' in group:
|
||||
items = group.split('-')
|
||||
|
||||
if len(items) == 2 and all([i.isnumeric() for i in items]):
|
||||
if len(items) == 2:
|
||||
a = items[0].strip()
|
||||
b = items[1].strip()
|
||||
|
||||
@@ -486,16 +423,16 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
|
||||
if a < b:
|
||||
for n in range(a, b + 1):
|
||||
add_sn(n)
|
||||
number_add(n)
|
||||
else:
|
||||
errors.append(_("Invalid group range: {g}").format(g=group))
|
||||
errors.append(_("Invalid group: {g}").format(g=group))
|
||||
|
||||
except ValueError:
|
||||
errors.append(_("Invalid group: {g}").format(g=group))
|
||||
continue
|
||||
else:
|
||||
# More than 2 hyphens or non-numeric group so add without interpolating
|
||||
add_sn(group)
|
||||
errors.append(_("Invalid group: {g}").format(g=group))
|
||||
continue
|
||||
|
||||
# plus signals either
|
||||
# 1: 'start+': expected number of serials, starting at start
|
||||
@@ -513,21 +450,20 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
|
||||
# case 1
|
||||
else:
|
||||
end = start + (expected_quantity - len(numbers))
|
||||
end = start + expected_quantity
|
||||
|
||||
for n in range(start, end):
|
||||
add_sn(n)
|
||||
number_add(n)
|
||||
# no case
|
||||
else:
|
||||
errors.append(_("Invalid group sequence: {g}").format(g=group))
|
||||
errors.append(_("Invalid group: {g}").format(g=group))
|
||||
continue
|
||||
|
||||
# At this point, we assume that the "group" is just a single serial value
|
||||
elif group:
|
||||
add_sn(group)
|
||||
|
||||
# No valid input group detected
|
||||
else:
|
||||
raise ValidationError(_(f"Invalid/no group {group}"))
|
||||
if group in numbers:
|
||||
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
||||
else:
|
||||
numbers.append(group)
|
||||
|
||||
if len(errors) > 0:
|
||||
raise ValidationError(errors)
|
||||
@@ -536,8 +472,8 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
|
||||
raise ValidationError([_("No serial numbers found")])
|
||||
|
||||
# The number of extracted serial numbers must match the expected quantity
|
||||
if expected_quantity != len(numbers):
|
||||
raise ValidationError([_("Number of unique serial numbers ({s}) must match quantity ({q})").format(s=len(numbers), q=expected_quantity)])
|
||||
if not expected_quantity == len(numbers):
|
||||
raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})").format(s=len(numbers), q=expected_quantity)])
|
||||
|
||||
return numbers
|
||||
|
||||
@@ -575,7 +511,7 @@ def validateFilterString(value, model=None):
|
||||
|
||||
pair = group.split('=')
|
||||
|
||||
if len(pair) != 2:
|
||||
if not len(pair) == 2:
|
||||
raise ValidationError(
|
||||
"Invalid group: {g}".format(g=group)
|
||||
)
|
||||
@@ -726,62 +662,3 @@ def clean_decimal(number):
|
||||
return Decimal(0)
|
||||
|
||||
return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()
|
||||
|
||||
|
||||
def get_objectreference(obj, type_ref: str = 'content_type', object_ref: str = 'object_id'):
|
||||
"""lookup method for the GenericForeignKey fields
|
||||
|
||||
Attributes:
|
||||
- obj: object that will be resolved
|
||||
- type_ref: field name for the contenttype field in the model
|
||||
- object_ref: field name for the object id in the model
|
||||
|
||||
Example implementation in the serializer:
|
||||
```
|
||||
target = serializers.SerializerMethodField()
|
||||
def get_target(self, obj):
|
||||
return get_objectreference(obj, 'target_content_type', 'target_object_id')
|
||||
```
|
||||
|
||||
The method name must always be the name of the field prefixed by 'get_'
|
||||
"""
|
||||
model_cls = getattr(obj, type_ref)
|
||||
obj_id = getattr(obj, object_ref)
|
||||
|
||||
# check if references are set -> return nothing if not
|
||||
if model_cls is None or obj_id is None:
|
||||
return None
|
||||
|
||||
# resolve referenced data into objects
|
||||
model_cls = model_cls.model_class()
|
||||
item = model_cls.objects.get(id=obj_id)
|
||||
url_fnc = getattr(item, 'get_absolute_url', None)
|
||||
|
||||
# create output
|
||||
ret = {}
|
||||
if url_fnc:
|
||||
ret['link'] = url_fnc()
|
||||
return {
|
||||
'name': str(item),
|
||||
'model': str(model_cls._meta.verbose_name),
|
||||
**ret
|
||||
}
|
||||
|
||||
|
||||
def inheritors(cls):
|
||||
"""
|
||||
Return all classes that are subclasses from the supplied cls
|
||||
"""
|
||||
subcls = set()
|
||||
work = [cls]
|
||||
while work:
|
||||
parent = work.pop()
|
||||
for child in parent.__subclasses__():
|
||||
if child not in subcls:
|
||||
subcls.add(child)
|
||||
work.append(child)
|
||||
return subcls
|
||||
|
||||
|
||||
class InvenTreeTestCase(UserMixin, TestCase):
|
||||
pass
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
Custom management command to cleanup old settings that are not defined anymore
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
@@ -16,27 +12,27 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
||||
logger.info("Collecting settings")
|
||||
print("Collecting settings")
|
||||
from common.models import InvenTreeSetting, InvenTreeUserSetting
|
||||
|
||||
# general settings
|
||||
db_settings = InvenTreeSetting.objects.all()
|
||||
model_settings = InvenTreeSetting.SETTINGS
|
||||
model_settings = InvenTreeSetting.GLOBAL_SETTINGS
|
||||
|
||||
# check if key exist and delete if not
|
||||
for setting in db_settings:
|
||||
if setting.key not in model_settings:
|
||||
setting.delete()
|
||||
logger.info(f"deleted setting '{setting.key}'")
|
||||
print(f"deleted setting '{setting.key}'")
|
||||
|
||||
# user settings
|
||||
db_settings = InvenTreeUserSetting.objects.all()
|
||||
model_settings = InvenTreeUserSetting.SETTINGS
|
||||
model_settings = InvenTreeUserSetting.GLOBAL_SETTINGS
|
||||
|
||||
# check if key exist and delete if not
|
||||
for setting in db_settings:
|
||||
if setting.key not in model_settings:
|
||||
setting.delete()
|
||||
logger.info(f"deleted user setting '{setting.key}'")
|
||||
print(f"deleted user setting '{setting.key}'")
|
||||
|
||||
logger.info("checked all settings")
|
||||
print("checked all settings")
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
Custom management command to prerender files
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.http.request import HttpRequest
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.module_loading import import_string
|
||||
from django.http.request import HttpRequest
|
||||
from django.utils.translation import override as lang_over
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def render_file(file_name, source, target, locales, ctx):
|
||||
""" renders a file into all provided locales """
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
Custom management command to rebuild thumbnail images
|
||||
|
||||
- May be required after importing a new dataset, for example
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
from company.models import Company
|
||||
from part.models import Part
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Rebuild all thumbnail images
|
||||
"""
|
||||
|
||||
def rebuild_thumbnail(self, model):
|
||||
"""
|
||||
Rebuild the thumbnail specified by the "image" field of the provided model
|
||||
"""
|
||||
|
||||
if not model.image:
|
||||
return
|
||||
|
||||
img = model.image
|
||||
url = img.thumbnail.name
|
||||
loc = os.path.join(settings.MEDIA_ROOT, url)
|
||||
|
||||
if not os.path.exists(loc):
|
||||
logger.info(f"Generating thumbnail image for '{img}'")
|
||||
|
||||
try:
|
||||
model.image.render_variations(replace=False)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Warning: Image file '{img}' is missing")
|
||||
except UnidentifiedImageError:
|
||||
logger.warning(f"Warning: Image file '{img}' is not a valid image")
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
||||
logger.info("Rebuilding Part thumbnails")
|
||||
|
||||
for part in Part.objects.exclude(image=None):
|
||||
try:
|
||||
self.rebuild_thumbnail(part)
|
||||
except (OperationalError, ProgrammingError):
|
||||
logger.error("ERROR: Database read error.")
|
||||
break
|
||||
|
||||
logger.info("Rebuilding Company thumbnails")
|
||||
|
||||
for company in Company.objects.exclude(image=None):
|
||||
try:
|
||||
self.rebuild_thumbnail(company)
|
||||
except (OperationalError, ProgrammingError):
|
||||
logger.error("ERROR: abase read error.")
|
||||
break
|
||||
@@ -1,36 +0,0 @@
|
||||
"""
|
||||
Custom management command to remove MFA for a user
|
||||
"""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Remove MFA for a user
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('mail', type=str)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
||||
# general settings
|
||||
mail = kwargs.get('mail')
|
||||
if not mail:
|
||||
raise KeyError('A mail is required')
|
||||
user = get_user_model()
|
||||
mfa_user = [*set(user.objects.filter(email=mail) | user.objects.filter(emailaddress__email=mail))]
|
||||
|
||||
if len(mfa_user) == 0:
|
||||
print('No user with this mail associated')
|
||||
elif len(mfa_user) > 1:
|
||||
print('More than one user found with this mail')
|
||||
else:
|
||||
# and clean out all MFA methods
|
||||
# backup codes
|
||||
mfa_user[0].staticdevice_set.all().delete()
|
||||
# TOTP tokens
|
||||
mfa_user[0].totpdevice_set.all().delete()
|
||||
print(f'Removed all MFA methods for user {str(mfa_user[0])}')
|
||||
@@ -2,11 +2,12 @@
|
||||
Custom management command, wait for the database to be ready!
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from django.db import connection
|
||||
from django.db.utils import ImproperlyConfigured, OperationalError
|
||||
from django.db.utils import OperationalError, ImproperlyConfigured
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.metadata import SimpleMetadata
|
||||
from rest_framework.utils import model_meta
|
||||
from rest_framework.fields import empty
|
||||
|
||||
import users.models
|
||||
from InvenTree.helpers import str2bool
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
@@ -27,28 +31,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
"""
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
|
||||
|
||||
self.request = request
|
||||
self.view = view
|
||||
|
||||
metadata = super().determine_metadata(request, view)
|
||||
|
||||
"""
|
||||
Custom context information to pass through to the OPTIONS endpoint,
|
||||
if the "context=True" is supplied to the OPTIONS requst
|
||||
|
||||
Serializer class can supply context data by defining a get_context_data() method (no arguments)
|
||||
"""
|
||||
|
||||
context = {}
|
||||
|
||||
if str2bool(request.query_params.get('context', False)):
|
||||
|
||||
if hasattr(self.serializer, 'get_context_data'):
|
||||
context = self.serializer.get_context_data()
|
||||
|
||||
metadata['context'] = context
|
||||
|
||||
user = request.user
|
||||
|
||||
if user is None:
|
||||
@@ -84,10 +72,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
# Remove any HTTP methods that the user does not have permission for
|
||||
for method, permission in rolemap.items():
|
||||
|
||||
result = check(user, table, permission)
|
||||
|
||||
if method in actions and not result:
|
||||
if method in actions and not check(user, table, permission):
|
||||
del actions[method]
|
||||
|
||||
# Add a 'DELETE' action if we are allowed to delete
|
||||
@@ -111,52 +96,27 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
to any fields whose Meta.model specifies a default value
|
||||
"""
|
||||
|
||||
self.serializer = serializer
|
||||
|
||||
serializer_info = super().get_serializer_info(serializer)
|
||||
|
||||
model_class = None
|
||||
|
||||
try:
|
||||
model_class = serializer.Meta.model
|
||||
ModelClass = serializer.Meta.model
|
||||
|
||||
model_fields = model_meta.get_field_info(model_class)
|
||||
|
||||
model_default_func = getattr(model_class, 'api_defaults', None)
|
||||
|
||||
if model_default_func:
|
||||
model_default_values = model_class.api_defaults(self.request)
|
||||
else:
|
||||
model_default_values = {}
|
||||
model_fields = model_meta.get_field_info(ModelClass)
|
||||
|
||||
# Iterate through simple fields
|
||||
for name, field in model_fields.fields.items():
|
||||
|
||||
if name in serializer_info.keys():
|
||||
if field.has_default() and name in serializer_info.keys():
|
||||
|
||||
if field.has_default():
|
||||
default = field.default
|
||||
|
||||
default = field.default
|
||||
if callable(default):
|
||||
try:
|
||||
default = default()
|
||||
except:
|
||||
continue
|
||||
|
||||
if callable(default):
|
||||
try:
|
||||
default = default()
|
||||
except:
|
||||
continue
|
||||
|
||||
serializer_info[name]['default'] = default
|
||||
|
||||
elif name in model_default_values:
|
||||
serializer_info[name]['default'] = model_default_values[name]
|
||||
|
||||
# Attributes to copy from the model to the field (if they don't exist)
|
||||
attributes = ['help_text']
|
||||
|
||||
for attr in attributes:
|
||||
if attr not in serializer_info[name]:
|
||||
|
||||
if hasattr(field, attr):
|
||||
serializer_info[name][attr] = getattr(field, attr)
|
||||
serializer_info[name]['default'] = default
|
||||
|
||||
# Iterate through relations
|
||||
for name, relation in model_fields.relations.items():
|
||||
@@ -176,9 +136,6 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'):
|
||||
serializer_info[name]['help_text'] = relation.model_field.help_text
|
||||
|
||||
if name in model_default_values:
|
||||
serializer_info[name]['default'] = model_default_values[name]
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@@ -188,24 +145,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
# Extract extra information if an instance is available
|
||||
if hasattr(serializer, 'instance'):
|
||||
instance = serializer.instance
|
||||
|
||||
if instance is None and model_class is not None:
|
||||
# Attempt to find the instance based on kwargs lookup
|
||||
kwargs = getattr(self.view, 'kwargs', None)
|
||||
|
||||
if kwargs:
|
||||
pk = None
|
||||
|
||||
for field in ['pk', 'id', 'PK', 'ID']:
|
||||
if field in kwargs:
|
||||
pk = kwargs[field]
|
||||
break
|
||||
|
||||
if pk is not None:
|
||||
try:
|
||||
instance = model_class.objects.get(pk=pk)
|
||||
except (ValueError, model_class.DoesNotExist):
|
||||
pass
|
||||
|
||||
if instance is None:
|
||||
try:
|
||||
instance = self.view.get_object()
|
||||
except:
|
||||
pass
|
||||
|
||||
if instance is not None:
|
||||
"""
|
||||
@@ -244,7 +189,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
field_info = super().get_field_info(field)
|
||||
|
||||
# If a default value is specified for the serializer field, add it!
|
||||
if 'default' not in field_info and field.default != empty:
|
||||
if 'default' not in field_info and not field.default == empty:
|
||||
field_info['default'] = field.get_default()
|
||||
|
||||
# Force non-nullable fields to read as "required"
|
||||
@@ -254,7 +199,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
# Introspect writable related fields
|
||||
if field_info['type'] == 'field' and not field_info['read_only']:
|
||||
|
||||
|
||||
# If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset
|
||||
if isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||
model = field.queryset.model
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.middleware import PersistentRemoteUserMiddleware
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.db import connection
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import Resolver404, include, re_path, reverse_lazy
|
||||
import logging
|
||||
import time
|
||||
import operator
|
||||
|
||||
from allauth_2fa.middleware import (AllauthTwoFactorMiddleware,
|
||||
BaseRequire2FAMiddleware)
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.urls import frontendpatterns
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
@@ -28,15 +21,28 @@ class AuthRequiredMiddleware(object):
|
||||
|
||||
assert hasattr(request, 'user')
|
||||
|
||||
# API requests are handled by the DRF library
|
||||
if request.path_info.startswith('/api/'):
|
||||
return self.get_response(request)
|
||||
response = self.get_response(request)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
"""
|
||||
Normally, a web-based session would use csrftoken based authentication.
|
||||
However when running an external application (e.g. the InvenTree app or Python library),
|
||||
we must validate the user token manually.
|
||||
However when running an external application (e.g. the InvenTree app),
|
||||
we wish to use token-based auth to grab media files.
|
||||
|
||||
So, we will allow token-based authentication but ONLY for the /media/ directory.
|
||||
|
||||
What problem is this solving?
|
||||
- The InvenTree mobile app does not use csrf token auth
|
||||
- Token auth is used by the Django REST framework, but that is under the /api/ endpoint
|
||||
- Media files (e.g. Part images) are required to be served to the app
|
||||
- We do not want to make /media/ files accessible without login!
|
||||
|
||||
There is PROBABLY a better way of going about this?
|
||||
a) Allow token-based authentication against a user?
|
||||
b) Serve /media/ files in a duplicate location e.g. /api/media/ ?
|
||||
c) Is there a "standard" way of solving this problem?
|
||||
|
||||
My [google|stackoverflow]-fu has failed me. So this hack has been created.
|
||||
"""
|
||||
|
||||
authorized = False
|
||||
@@ -50,90 +56,104 @@ class AuthRequiredMiddleware(object):
|
||||
elif request.path_info.startswith('/accounts/'):
|
||||
authorized = True
|
||||
|
||||
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
|
||||
auth = request.headers.get('Authorization', request.headers.get('authorization')).strip()
|
||||
elif 'Authorization' in request.headers.keys():
|
||||
auth = request.headers['Authorization'].strip()
|
||||
|
||||
if auth.lower().startswith('token') and len(auth.split()) == 2:
|
||||
token_key = auth.split()[1]
|
||||
if auth.startswith('Token') and len(auth.split()) == 2:
|
||||
token = auth.split()[1]
|
||||
|
||||
# Does the provided token match a valid user?
|
||||
try:
|
||||
token = Token.objects.get(key=token_key)
|
||||
if Token.objects.filter(key=token).exists():
|
||||
|
||||
# Provide the user information to the request
|
||||
request.user = token.user
|
||||
authorized = True
|
||||
allowed = ['/api/', '/media/']
|
||||
|
||||
except Token.DoesNotExist:
|
||||
logger.warning(f"Access denied for unknown token {token_key}")
|
||||
# Only allow token-auth for /media/ or /static/ dirs!
|
||||
if any([request.path_info.startswith(a) for a in allowed]):
|
||||
authorized = True
|
||||
|
||||
# No authorization was found for the request
|
||||
if not authorized:
|
||||
# A logout request will redirect the user to the login screen
|
||||
if request.path_info == reverse_lazy('logout'):
|
||||
return HttpResponseRedirect(reverse_lazy('login'))
|
||||
|
||||
path = request.path_info
|
||||
|
||||
# List of URL endpoints we *do not* want to redirect to
|
||||
urls = [
|
||||
reverse_lazy('account_login'),
|
||||
reverse_lazy('account_logout'),
|
||||
reverse_lazy('login'),
|
||||
reverse_lazy('logout'),
|
||||
reverse_lazy('admin:login'),
|
||||
reverse_lazy('admin:logout'),
|
||||
]
|
||||
|
||||
# Do not redirect requests to any of these paths
|
||||
paths_ignore = [
|
||||
'/api/',
|
||||
'/js/',
|
||||
'/media/',
|
||||
'/static/',
|
||||
]
|
||||
|
||||
if path not in urls and not any([path.startswith(p) for p in paths_ignore]):
|
||||
if path not in urls and not path.startswith('/api/'):
|
||||
# Save the 'next' parameter to pass through to the login view
|
||||
|
||||
return redirect('{}?next={}'.format(reverse_lazy('account_login'), request.path))
|
||||
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
|
||||
|
||||
else:
|
||||
# Return a 401 (Unauthorized) response code for this request
|
||||
return HttpResponse('Unauthorized', status=401)
|
||||
|
||||
response = self.get_response(request)
|
||||
# Code to be executed for each request/response after
|
||||
# the view is called.
|
||||
|
||||
return response
|
||||
|
||||
|
||||
url_matcher = re_path('', include(frontendpatterns))
|
||||
|
||||
|
||||
class Check2FAMiddleware(BaseRequire2FAMiddleware):
|
||||
"""check if user is required to have MFA enabled"""
|
||||
def require_2fa(self, request):
|
||||
# Superusers are require to have 2FA.
|
||||
try:
|
||||
if url_matcher.resolve(request.path[1:]):
|
||||
return InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA')
|
||||
except Resolver404:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
class CustomAllauthTwoFactorMiddleware(AllauthTwoFactorMiddleware):
|
||||
"""This function ensures only frontend code triggers the MFA auth cycle"""
|
||||
def process_request(self, request):
|
||||
try:
|
||||
if not url_matcher.resolve(request.path[1:]):
|
||||
super().process_request(request)
|
||||
except Resolver404:
|
||||
pass
|
||||
|
||||
|
||||
class InvenTreeRemoteUserMiddleware(PersistentRemoteUserMiddleware):
|
||||
class QueryCountMiddleware(object):
|
||||
"""
|
||||
Middleware to check if HTTP-header based auth is enabled and to set it up
|
||||
This middleware will log the number of queries run
|
||||
and the total time taken for each request (with a
|
||||
status code of 200). It does not currently support
|
||||
multi-db setups.
|
||||
|
||||
To enable this middleware, set 'log_queries: True' in the local InvenTree config file.
|
||||
|
||||
Reference: https://www.dabapps.com/blog/logging-sql-queries-django-13/
|
||||
|
||||
Note: 2020-08-15 - This is no longer used, instead we now rely on the django-debug-toolbar addon
|
||||
"""
|
||||
header = settings.REMOTE_LOGIN_HEADER
|
||||
|
||||
def process_request(self, request):
|
||||
if not settings.REMOTE_LOGIN:
|
||||
return
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
return super().process_request(request)
|
||||
def __call__(self, request):
|
||||
|
||||
t_start = time.time()
|
||||
response = self.get_response(request)
|
||||
t_stop = time.time()
|
||||
|
||||
if response.status_code == 200:
|
||||
total_time = 0
|
||||
|
||||
if len(connection.queries) > 0:
|
||||
|
||||
queries = {}
|
||||
|
||||
for query in connection.queries:
|
||||
query_time = query.get('time')
|
||||
|
||||
sql = query.get('sql').split('.')[0]
|
||||
|
||||
if sql in queries:
|
||||
queries[sql] += 1
|
||||
else:
|
||||
queries[sql] = 1
|
||||
|
||||
if query_time is None:
|
||||
# django-debug-toolbar monkeypatches the connection
|
||||
# cursor wrapper and adds extra information in each
|
||||
# item in connection.queries. The query time is stored
|
||||
# under the key "duration" rather than "time" and is
|
||||
# in milliseconds, not seconds.
|
||||
query_time = float(query.get('duration', 0))
|
||||
|
||||
total_time += float(query_time)
|
||||
|
||||
logger.debug('{n} queries run, {a:.3f}s / {b:.3f}s'.format(
|
||||
n=len(connection.queries),
|
||||
a=total_time,
|
||||
b=(t_stop - t_start)))
|
||||
|
||||
for x in sorted(queries.items(), key=operator.itemgetter(1), reverse=True):
|
||||
print(x[0], ':', x[1])
|
||||
|
||||
return response
|
||||
|
||||
@@ -2,26 +2,23 @@
|
||||
Generic models which provide extra functionality over base Django model types.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from mptt.exceptions import InvalidMove
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from mptt.exceptions import InvalidMove
|
||||
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
from InvenTree.validators import validate_tree_name
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
from .validators import validate_tree_name
|
||||
|
||||
|
||||
def rename_attachment(instance, filename):
|
||||
@@ -41,131 +38,15 @@ def rename_attachment(instance, filename):
|
||||
return os.path.join(instance.getSubdir(), filename)
|
||||
|
||||
|
||||
class DataImportMixin(object):
|
||||
"""
|
||||
Model mixin class which provides support for 'data import' functionality.
|
||||
|
||||
Models which implement this mixin should provide information on the fields available for import
|
||||
"""
|
||||
|
||||
# Define a map of fields avaialble for import
|
||||
IMPORT_FIELDS = {}
|
||||
|
||||
@classmethod
|
||||
def get_import_fields(cls):
|
||||
"""
|
||||
Return all available import fields
|
||||
|
||||
Where information on a particular field is not explicitly provided,
|
||||
introspect the base model to (attempt to) find that information.
|
||||
|
||||
"""
|
||||
fields = cls.IMPORT_FIELDS
|
||||
|
||||
for name, field in fields.items():
|
||||
|
||||
# Attempt to extract base field information from the model
|
||||
base_field = None
|
||||
|
||||
for f in cls._meta.fields:
|
||||
if f.name == name:
|
||||
base_field = f
|
||||
break
|
||||
|
||||
if base_field:
|
||||
if 'label' not in field:
|
||||
field['label'] = base_field.verbose_name
|
||||
|
||||
if 'help_text' not in field:
|
||||
field['help_text'] = base_field.help_text
|
||||
|
||||
fields[name] = field
|
||||
|
||||
return fields
|
||||
|
||||
@classmethod
|
||||
def get_required_import_fields(cls):
|
||||
""" Return all *required* import fields """
|
||||
fields = {}
|
||||
|
||||
for name, field in cls.get_import_fields().items():
|
||||
required = field.get('required', False)
|
||||
|
||||
if required:
|
||||
fields[name] = field
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
class ReferenceIndexingMixin(models.Model):
|
||||
"""
|
||||
A mixin for keeping track of numerical copies of the "reference" field.
|
||||
|
||||
!!DANGER!! always add `ReferenceIndexingSerializerMixin`to all your models serializers to
|
||||
ensure the reference field is not too big
|
||||
|
||||
Here, we attempt to convert a "reference" field value (char) to an integer,
|
||||
for performing fast natural sorting.
|
||||
|
||||
This requires extra database space (due to the extra table column),
|
||||
but is required as not all supported database backends provide equivalent casting.
|
||||
|
||||
This mixin adds a field named 'reference_int'.
|
||||
|
||||
- If the 'reference' field can be cast to an integer, it is stored here
|
||||
- If the 'reference' field *starts* with an integer, it is stored here
|
||||
- Otherwise, we store zero
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def rebuild_reference_field(self):
|
||||
|
||||
reference = getattr(self, 'reference', '')
|
||||
|
||||
self.reference_int = extract_int(reference)
|
||||
|
||||
reference_int = models.BigIntegerField(default=0)
|
||||
|
||||
|
||||
def extract_int(reference, clip=0x7fffffff):
|
||||
# Default value if we cannot convert to an integer
|
||||
ref_int = 0
|
||||
|
||||
# Look at the start of the string - can it be "integerized"?
|
||||
result = re.match(r"^(\d+)", reference)
|
||||
|
||||
if result and len(result.groups()) == 1:
|
||||
ref = result.groups()[0]
|
||||
try:
|
||||
ref_int = int(ref)
|
||||
except:
|
||||
ref_int = 0
|
||||
|
||||
# Ensure that the returned values are within the range that can be stored in an IntegerField
|
||||
# Note: This will result in large values being "clipped"
|
||||
if clip is not None:
|
||||
if ref_int > clip:
|
||||
ref_int = clip
|
||||
elif ref_int < -clip:
|
||||
ref_int = -clip
|
||||
|
||||
return ref_int
|
||||
|
||||
|
||||
class InvenTreeAttachment(models.Model):
|
||||
""" Provides an abstracted class for managing file attachments.
|
||||
|
||||
An attachment can be either an uploaded file, or an external URL
|
||||
|
||||
Attributes:
|
||||
attachment: File
|
||||
comment: String descriptor for the attachment
|
||||
user: User associated with file upload
|
||||
upload_date: Date the file was uploaded
|
||||
"""
|
||||
|
||||
def getSubdir(self):
|
||||
"""
|
||||
Return the subdirectory under which attachments should be stored.
|
||||
@@ -174,32 +55,11 @@ class InvenTreeAttachment(models.Model):
|
||||
|
||||
return "attachments"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Either 'attachment' or 'link' must be specified!
|
||||
if not self.attachment and not self.link:
|
||||
raise ValidationError({
|
||||
'attachment': _('Missing file'),
|
||||
'link': _('Missing external link'),
|
||||
})
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
if self.attachment is not None:
|
||||
return os.path.basename(self.attachment.name)
|
||||
else:
|
||||
return str(self.link)
|
||||
return os.path.basename(self.attachment.name)
|
||||
|
||||
attachment = models.FileField(upload_to=rename_attachment, verbose_name=_('Attachment'),
|
||||
help_text=_('Select file to attach'),
|
||||
blank=True, null=True
|
||||
)
|
||||
|
||||
link = InvenTreeURLField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Link'),
|
||||
help_text=_('Link to external URL')
|
||||
)
|
||||
help_text=_('Select file to attach'))
|
||||
|
||||
comment = models.CharField(blank=True, max_length=100, verbose_name=_('Comment'), help_text=_('File comment'))
|
||||
|
||||
@@ -215,76 +75,7 @@ class InvenTreeAttachment(models.Model):
|
||||
|
||||
@property
|
||||
def basename(self):
|
||||
if self.attachment:
|
||||
return os.path.basename(self.attachment.name)
|
||||
else:
|
||||
return None
|
||||
|
||||
@basename.setter
|
||||
def basename(self, fn):
|
||||
"""
|
||||
Function to rename the attachment file.
|
||||
|
||||
- Filename cannot be empty
|
||||
- Filename cannot contain illegal characters
|
||||
- Filename must specify an extension
|
||||
- Filename cannot match an existing file
|
||||
"""
|
||||
|
||||
fn = fn.strip()
|
||||
|
||||
if len(fn) == 0:
|
||||
raise ValidationError(_('Filename must not be empty'))
|
||||
|
||||
attachment_dir = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
self.getSubdir()
|
||||
)
|
||||
|
||||
old_file = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
self.attachment.name
|
||||
)
|
||||
|
||||
new_file = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
self.getSubdir(),
|
||||
fn
|
||||
)
|
||||
|
||||
new_file = os.path.abspath(new_file)
|
||||
|
||||
# Check that there are no directory tricks going on...
|
||||
if os.path.dirname(new_file) != attachment_dir:
|
||||
logger.error(f"Attempted to rename attachment outside valid directory: '{new_file}'")
|
||||
raise ValidationError(_("Invalid attachment directory"))
|
||||
|
||||
# Ignore further checks if the filename is not actually being renamed
|
||||
if new_file == old_file:
|
||||
return
|
||||
|
||||
forbidden = ["'", '"', "#", "@", "!", "&", "^", "<", ">", ":", ";", "/", "\\", "|", "?", "*", "%", "~", "`"]
|
||||
|
||||
for c in forbidden:
|
||||
if c in fn:
|
||||
raise ValidationError(_(f"Filename contains illegal character '{c}'"))
|
||||
|
||||
if len(fn.split('.')) < 2:
|
||||
raise ValidationError(_("Filename missing extension"))
|
||||
|
||||
if not os.path.exists(old_file):
|
||||
logger.error(f"Trying to rename attachment '{old_file}' which does not exist")
|
||||
return
|
||||
|
||||
if os.path.exists(new_file):
|
||||
raise ValidationError(_("Attachment with this filename already exists"))
|
||||
|
||||
try:
|
||||
os.rename(old_file, new_file)
|
||||
self.attachment.name = os.path.join(self.getSubdir(), fn)
|
||||
self.save()
|
||||
except:
|
||||
raise ValidationError(_("Error renaming file"))
|
||||
return os.path.basename(self.attachment.name)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
import users.models
|
||||
|
||||
43
InvenTree/InvenTree/plugins.py
Normal file
43
InvenTree/InvenTree/plugins.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import inspect
|
||||
import importlib
|
||||
import pkgutil
|
||||
|
||||
|
||||
def iter_namespace(pkg):
|
||||
|
||||
return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".")
|
||||
|
||||
|
||||
def get_modules(pkg):
|
||||
# Return all modules in a given package
|
||||
return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)]
|
||||
|
||||
|
||||
def get_classes(module):
|
||||
# Return all classes in a given module
|
||||
return inspect.getmembers(module, inspect.isclass)
|
||||
|
||||
|
||||
def get_plugins(pkg, baseclass):
|
||||
"""
|
||||
Return a list of all modules under a given package.
|
||||
|
||||
- Modules must be a subclass of the provided 'baseclass'
|
||||
- Modules must have a non-empty PLUGIN_NAME parameter
|
||||
"""
|
||||
|
||||
plugins = []
|
||||
|
||||
modules = get_modules(pkg)
|
||||
|
||||
# Iterate through each module in the package
|
||||
for mod in modules:
|
||||
# Iterate through each class in the module
|
||||
for item in get_classes(mod):
|
||||
plugin = item[1]
|
||||
if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME:
|
||||
plugins.append(plugin)
|
||||
|
||||
return plugins
|
||||
@@ -6,16 +6,10 @@ def isInTestMode():
|
||||
Returns True if the database is in testing mode
|
||||
"""
|
||||
|
||||
return 'test' in sys.argv
|
||||
if 'test' in sys.argv:
|
||||
return True
|
||||
|
||||
|
||||
def isImportingData():
|
||||
"""
|
||||
Returns True if the database is currently importing data,
|
||||
e.g. 'loaddata' command is performed
|
||||
"""
|
||||
|
||||
return 'loaddata' in sys.argv
|
||||
return False
|
||||
|
||||
|
||||
def canAppAccessDatabase(allow_test=False):
|
||||
@@ -39,8 +33,7 @@ def canAppAccessDatabase(allow_test=False):
|
||||
'createsuperuser',
|
||||
'wait_for_db',
|
||||
'prerender',
|
||||
'rebuild_models',
|
||||
'rebuild_thumbnails',
|
||||
'rebuild',
|
||||
'collectstatic',
|
||||
'makemessages',
|
||||
'compilemessages',
|
||||
|
||||
@@ -2,27 +2,28 @@
|
||||
Serializers used in various InvenTree apps
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import tablib
|
||||
from djmoney.contrib.django_rest_framework.fields import MoneyField
|
||||
from djmoney.money import Money
|
||||
from djmoney.utils import MONEY_CLASSES, get_currency_field_name
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.serializers import DecimalField
|
||||
from rest_framework.utils import model_meta
|
||||
|
||||
from .models import extract_int
|
||||
from rest_framework import serializers
|
||||
from rest_framework.utils import model_meta
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.serializers import DecimalField
|
||||
|
||||
|
||||
class InvenTreeMoneySerializer(MoneyField):
|
||||
@@ -33,13 +34,6 @@ class InvenTreeMoneySerializer(MoneyField):
|
||||
Ref: https://github.com/django-money/django-money/blob/master/djmoney/contrib/django_rest_framework/fields.py
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
kwargs["max_digits"] = kwargs.get("max_digits", 19)
|
||||
kwargs["decimal_places"] = kwargs.get("decimal_places", 4)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_value(self, data):
|
||||
"""
|
||||
Test that the returned amount is a valid Decimal
|
||||
@@ -52,18 +46,16 @@ class InvenTreeMoneySerializer(MoneyField):
|
||||
amount = None
|
||||
|
||||
try:
|
||||
if amount is not None and amount is not empty:
|
||||
if amount is not None:
|
||||
amount = Decimal(amount)
|
||||
except:
|
||||
raise ValidationError({
|
||||
self.field_name: [_("Must be a valid number")],
|
||||
})
|
||||
raise ValidationError(_("Must be a valid number"))
|
||||
|
||||
currency = data.get(get_currency_field_name(self.field_name), self.default_currency)
|
||||
|
||||
if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty:
|
||||
return Money(amount, currency)
|
||||
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
@@ -93,21 +85,14 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
"""
|
||||
Custom __init__ routine to ensure that *default* values (as specified in the ORM)
|
||||
are used by the DRF serializers, *if* the values are not provided by the user.
|
||||
"""
|
||||
|
||||
# self.instance = instance
|
||||
|
||||
# If instance is None, we are creating a new instance
|
||||
if instance is None and data is not empty:
|
||||
|
||||
if data is None:
|
||||
data = OrderedDict()
|
||||
else:
|
||||
new_data = OrderedDict()
|
||||
new_data.update(data)
|
||||
|
||||
data = new_data
|
||||
|
||||
# Required to side-step immutability of a QueryDict
|
||||
data = data.copy()
|
||||
|
||||
# Add missing fields which have default values
|
||||
ModelClass = self.Meta.model
|
||||
@@ -180,18 +165,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
return self.instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
Catch any django ValidationError, and re-throw as a DRF ValidationError
|
||||
"""
|
||||
|
||||
try:
|
||||
instance = super().update(instance, validated_data)
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
return instance
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
"""
|
||||
Perform serializer validation.
|
||||
@@ -213,40 +186,18 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Update instance fields
|
||||
for attr, value in data.items():
|
||||
try:
|
||||
setattr(instance, attr, value)
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
setattr(instance, attr, value)
|
||||
|
||||
# Run a 'full_clean' on the model.
|
||||
# Note that by default, DRF does *not* perform full model validation!
|
||||
try:
|
||||
instance.full_clean()
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
|
||||
data = exc.message_dict
|
||||
|
||||
# Change '__all__' key (django style) to 'non_field_errors' (DRF style)
|
||||
if '__all__' in data:
|
||||
data['non_field_errors'] = data['__all__']
|
||||
del data['__all__']
|
||||
|
||||
raise ValidationError(data)
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ReferenceIndexingSerializerMixin():
|
||||
"""
|
||||
This serializer mixin ensures the the reference is not to big / small
|
||||
for the BigIntegerField
|
||||
"""
|
||||
def validate_reference(self, value):
|
||||
if extract_int(value) > models.BigIntegerField.MAX_BIGINT:
|
||||
raise serializers.ValidationError('reference is to to big')
|
||||
return value
|
||||
|
||||
|
||||
class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||
"""
|
||||
Override the DRF native FileField serializer,
|
||||
@@ -276,27 +227,6 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||
return os.path.join(str(settings.MEDIA_URL), str(value))
|
||||
|
||||
|
||||
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
|
||||
|
||||
The only real addition here is that we support "renaming" of the attachment file.
|
||||
"""
|
||||
|
||||
attachment = InvenTreeAttachmentSerializerField(
|
||||
required=False,
|
||||
allow_null=False,
|
||||
)
|
||||
|
||||
# The 'filename' field must be present in the serializer
|
||||
filename = serializers.CharField(
|
||||
label=_('Filename'),
|
||||
required=False,
|
||||
source='basename',
|
||||
allow_blank=False,
|
||||
)
|
||||
|
||||
|
||||
class InvenTreeImageSerializerField(serializers.ImageField):
|
||||
"""
|
||||
Custom image serializer.
|
||||
@@ -309,329 +239,3 @@ class InvenTreeImageSerializerField(serializers.ImageField):
|
||||
return None
|
||||
|
||||
return os.path.join(str(settings.MEDIA_URL), str(value))
|
||||
|
||||
|
||||
class InvenTreeDecimalField(serializers.FloatField):
|
||||
"""
|
||||
Custom serializer for decimal fields. Solves the following issues:
|
||||
|
||||
- The normal DRF DecimalField renders values with trailing zeros
|
||||
- Using a FloatField can result in rounding issues: https://code.djangoproject.com/ticket/30290
|
||||
"""
|
||||
|
||||
def to_internal_value(self, data):
|
||||
|
||||
# Convert the value to a string, and then a decimal
|
||||
try:
|
||||
return Decimal(str(data))
|
||||
except:
|
||||
raise serializers.ValidationError(_("Invalid value"))
|
||||
|
||||
|
||||
class DataFileUploadSerializer(serializers.Serializer):
|
||||
"""
|
||||
Generic serializer for uploading a data file, and extracting a dataset.
|
||||
|
||||
- Validates uploaded file
|
||||
- Extracts column names
|
||||
- Extracts data rows
|
||||
"""
|
||||
|
||||
# Implementing class should register a target model (database model) to be used for import
|
||||
TARGET_MODEL = None
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
'data_file',
|
||||
]
|
||||
|
||||
data_file = serializers.FileField(
|
||||
label=_("Data File"),
|
||||
help_text=_("Select data file for upload"),
|
||||
required=True,
|
||||
allow_empty_file=False,
|
||||
)
|
||||
|
||||
def validate_data_file(self, data_file):
|
||||
"""
|
||||
Perform validation checks on the uploaded data file.
|
||||
"""
|
||||
|
||||
self.filename = data_file.name
|
||||
|
||||
name, ext = os.path.splitext(data_file.name)
|
||||
|
||||
# Remove the leading . from the extension
|
||||
ext = ext[1:]
|
||||
|
||||
accepted_file_types = [
|
||||
'xls', 'xlsx',
|
||||
'csv', 'tsv',
|
||||
'xml',
|
||||
]
|
||||
|
||||
if ext not in accepted_file_types:
|
||||
raise serializers.ValidationError(_("Unsupported file type"))
|
||||
|
||||
# Impose a 50MB limit on uploaded BOM files
|
||||
max_upload_file_size = 50 * 1024 * 1024
|
||||
|
||||
if data_file.size > max_upload_file_size:
|
||||
raise serializers.ValidationError(_("File is too large"))
|
||||
|
||||
# Read file data into memory (bytes object)
|
||||
try:
|
||||
data = data_file.read()
|
||||
except Exception as e:
|
||||
raise serializers.ValidationError(str(e))
|
||||
|
||||
if ext in ['csv', 'tsv', 'xml']:
|
||||
try:
|
||||
data = data.decode()
|
||||
except Exception as e:
|
||||
raise serializers.ValidationError(str(e))
|
||||
|
||||
# Convert to a tablib dataset (we expect headers)
|
||||
try:
|
||||
self.dataset = tablib.Dataset().load(data, ext, headers=True)
|
||||
except Exception as e:
|
||||
raise serializers.ValidationError(str(e))
|
||||
|
||||
if len(self.dataset.headers) == 0:
|
||||
raise serializers.ValidationError(_("No columns found in file"))
|
||||
|
||||
if len(self.dataset) == 0:
|
||||
raise serializers.ValidationError(_("No data rows found in file"))
|
||||
|
||||
return data_file
|
||||
|
||||
def match_column(self, column_name, field_names, exact=False):
|
||||
"""
|
||||
Attempt to match a column name (from the file) to a field (defined in the model)
|
||||
|
||||
Order of matching is:
|
||||
- Direct match
|
||||
- Case insensitive match
|
||||
- Fuzzy match
|
||||
"""
|
||||
|
||||
if not column_name:
|
||||
return None
|
||||
|
||||
column_name = str(column_name).strip()
|
||||
|
||||
column_name_lower = column_name.lower()
|
||||
|
||||
if column_name in field_names:
|
||||
return column_name
|
||||
|
||||
for field_name in field_names:
|
||||
if field_name.lower() == column_name_lower:
|
||||
return field_name
|
||||
|
||||
if exact:
|
||||
# Finished available 'exact' matches
|
||||
return None
|
||||
|
||||
# TODO: Fuzzy pattern matching for column names
|
||||
|
||||
# No matches found
|
||||
return None
|
||||
|
||||
def extract_data(self):
|
||||
"""
|
||||
Returns dataset extracted from the file
|
||||
"""
|
||||
|
||||
# Provide a dict of available import fields for the model
|
||||
model_fields = {}
|
||||
|
||||
# Keep track of columns we have already extracted
|
||||
matched_columns = set()
|
||||
|
||||
if self.TARGET_MODEL:
|
||||
try:
|
||||
model_fields = self.TARGET_MODEL.get_import_fields()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Extract a list of valid model field names
|
||||
model_field_names = [key for key in model_fields.keys()]
|
||||
|
||||
# Provide a dict of available columns from the dataset
|
||||
file_columns = {}
|
||||
|
||||
for header in self.dataset.headers:
|
||||
column = {}
|
||||
|
||||
# Attempt to "match" file columns to model fields
|
||||
match = self.match_column(header, model_field_names, exact=True)
|
||||
|
||||
if match is not None and match not in matched_columns:
|
||||
matched_columns.add(match)
|
||||
column['value'] = match
|
||||
else:
|
||||
column['value'] = None
|
||||
|
||||
file_columns[header] = column
|
||||
|
||||
return {
|
||||
'file_fields': file_columns,
|
||||
'model_fields': model_fields,
|
||||
'rows': [row.values() for row in self.dataset.dict],
|
||||
'filename': self.filename,
|
||||
}
|
||||
|
||||
def save(self):
|
||||
...
|
||||
|
||||
|
||||
class DataFileExtractSerializer(serializers.Serializer):
|
||||
"""
|
||||
Generic serializer for extracting data from an imported dataset.
|
||||
|
||||
- User provides an array of matched headers
|
||||
- User provides an array of raw data rows
|
||||
"""
|
||||
|
||||
# Implementing class should register a target model (database model) to be used for import
|
||||
TARGET_MODEL = None
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
'columns',
|
||||
'rows',
|
||||
]
|
||||
|
||||
# Mapping of columns
|
||||
columns = serializers.ListField(
|
||||
child=serializers.CharField(
|
||||
allow_blank=True,
|
||||
),
|
||||
)
|
||||
|
||||
rows = serializers.ListField(
|
||||
child=serializers.ListField(
|
||||
child=serializers.CharField(
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
self.columns = data.get('columns', [])
|
||||
self.rows = data.get('rows', [])
|
||||
|
||||
if len(self.rows) == 0:
|
||||
raise serializers.ValidationError(_("No data rows provided"))
|
||||
|
||||
if len(self.columns) == 0:
|
||||
raise serializers.ValidationError(_("No data columns supplied"))
|
||||
|
||||
self.validate_extracted_columns()
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
|
||||
if self.TARGET_MODEL:
|
||||
try:
|
||||
model_fields = self.TARGET_MODEL.get_import_fields()
|
||||
except:
|
||||
model_fields = {}
|
||||
|
||||
rows = []
|
||||
|
||||
for row in self.rows:
|
||||
"""
|
||||
Optionally pre-process each row, before sending back to the client
|
||||
"""
|
||||
|
||||
processed_row = self.process_row(self.row_to_dict(row))
|
||||
|
||||
if processed_row:
|
||||
rows.append({
|
||||
"original": row,
|
||||
"data": processed_row,
|
||||
})
|
||||
|
||||
return {
|
||||
'fields': model_fields,
|
||||
'columns': self.columns,
|
||||
'rows': rows,
|
||||
}
|
||||
|
||||
def process_row(self, row):
|
||||
"""
|
||||
Process a 'row' of data, which is a mapped column:value dict
|
||||
|
||||
Returns either a mapped column:value dict, or None.
|
||||
|
||||
If the function returns None, the column is ignored!
|
||||
"""
|
||||
|
||||
# Default implementation simply returns the original row data
|
||||
return row
|
||||
|
||||
def row_to_dict(self, row):
|
||||
"""
|
||||
Convert a "row" to a named data dict
|
||||
"""
|
||||
|
||||
row_dict = {
|
||||
'errors': {},
|
||||
}
|
||||
|
||||
for idx, value in enumerate(row):
|
||||
|
||||
if idx < len(self.columns):
|
||||
col = self.columns[idx]
|
||||
|
||||
if col:
|
||||
row_dict[col] = value
|
||||
|
||||
return row_dict
|
||||
|
||||
def validate_extracted_columns(self):
|
||||
"""
|
||||
Perform custom validation of header mapping.
|
||||
"""
|
||||
|
||||
if self.TARGET_MODEL:
|
||||
try:
|
||||
model_fields = self.TARGET_MODEL.get_import_fields()
|
||||
except:
|
||||
model_fields = {}
|
||||
|
||||
cols_seen = set()
|
||||
|
||||
for name, field in model_fields.items():
|
||||
|
||||
required = field.get('required', False)
|
||||
|
||||
# Check for missing required columns
|
||||
if required:
|
||||
if name not in self.columns:
|
||||
raise serializers.ValidationError(_(f"Missing required column: '{name}'"))
|
||||
|
||||
for col in self.columns:
|
||||
|
||||
if not col:
|
||||
continue
|
||||
|
||||
# Check for duplicated columns
|
||||
if col in cols_seen:
|
||||
raise serializers.ValidationError(_(f"Duplicate column: '{col}'"))
|
||||
|
||||
cols_seen.add(col)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
No "save" action for this serializer
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -12,48 +12,78 @@ database setup in this file.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
import string
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import django.conf.locale
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import moneyed
|
||||
import yaml
|
||||
|
||||
from .config import get_base_dir, get_config_file, get_plugin_file, get_setting
|
||||
import yaml
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.messages import constants as messages
|
||||
|
||||
|
||||
def _is_true(x):
|
||||
# Shortcut function to determine if a value "looks" like a boolean
|
||||
return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true']
|
||||
return str(x).lower() in ['1', 'y', 'yes', 't', 'true']
|
||||
|
||||
|
||||
def get_setting(environment_var, backup_val, default_value=None):
|
||||
"""
|
||||
Helper function for retrieving a configuration setting value
|
||||
|
||||
- First preference is to look for the environment variable
|
||||
- Second preference is to look for the value of the settings file
|
||||
- Third preference is the default value
|
||||
"""
|
||||
|
||||
val = os.getenv(environment_var)
|
||||
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
if backup_val is not None:
|
||||
return backup_val
|
||||
|
||||
return default_value
|
||||
|
||||
|
||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||
TESTING = 'test' in sys.argv
|
||||
# Are enviroment variables manipulated by tests? Needs to be set by testing code
|
||||
TESTING_ENV = False
|
||||
|
||||
# New requirement for django 3.2+
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = get_base_dir()
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
cfg_filename = get_config_file()
|
||||
# Specify where the "config file" is located.
|
||||
# By default, this is 'config.yaml'
|
||||
|
||||
cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
|
||||
|
||||
if cfg_filename:
|
||||
cfg_filename = cfg_filename.strip()
|
||||
cfg_filename = os.path.abspath(cfg_filename)
|
||||
|
||||
else:
|
||||
# Config file is *not* specified - use the default
|
||||
cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
|
||||
|
||||
if not os.path.exists(cfg_filename):
|
||||
print("InvenTree configuration file 'config.yaml' not found - creating default file")
|
||||
|
||||
cfg_template = os.path.join(BASE_DIR, "config_template.yaml")
|
||||
shutil.copyfile(cfg_template, cfg_filename)
|
||||
print(f"Created config file {cfg_filename}")
|
||||
|
||||
with open(cfg_filename, 'r') as cfg:
|
||||
CONFIG = yaml.safe_load(cfg)
|
||||
|
||||
# We will place any config files in the same directory as the config file
|
||||
config_dir = os.path.dirname(cfg_filename)
|
||||
|
||||
# Default action is to run the system in Debug mode
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = _is_true(get_setting(
|
||||
@@ -78,7 +108,7 @@ logging.basicConfig(
|
||||
)
|
||||
|
||||
if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
log_level = 'WARNING' # pragma: no cover
|
||||
log_level = 'WARNING'
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
@@ -92,11 +122,6 @@ LOGGING = {
|
||||
'handlers': ['console'],
|
||||
'level': log_level,
|
||||
},
|
||||
'filters': {
|
||||
'require_not_maintenance_mode_503': {
|
||||
'()': 'maintenance_mode.logging.RequireNotMaintenanceMode503',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Get a logger instance for this setup file
|
||||
@@ -115,20 +140,20 @@ d) Create "secret_key.txt" if it does not exist
|
||||
|
||||
if os.getenv("INVENTREE_SECRET_KEY"):
|
||||
# Secret key passed in directly
|
||||
SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip() # pragma: no cover
|
||||
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover
|
||||
SECRET_KEY = os.getenv("INVENTREE_SECRET_KEY").strip()
|
||||
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY")
|
||||
else:
|
||||
# Secret key passed in by file location
|
||||
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
||||
|
||||
if key_file:
|
||||
key_file = os.path.abspath(key_file) # pragma: no cover
|
||||
key_file = os.path.abspath(key_file)
|
||||
else:
|
||||
# default secret key location
|
||||
key_file = os.path.join(BASE_DIR, "secret_key.txt")
|
||||
key_file = os.path.abspath(key_file)
|
||||
|
||||
if not os.path.exists(key_file): # pragma: no cover
|
||||
if not os.path.exists(key_file):
|
||||
logger.info(f"Generating random key file at '{key_file}'")
|
||||
# Create a random key file
|
||||
with open(key_file, 'w') as f:
|
||||
@@ -140,34 +165,10 @@ else:
|
||||
|
||||
try:
|
||||
SECRET_KEY = open(key_file, "r").read().strip()
|
||||
except Exception: # pragma: no cover
|
||||
except Exception:
|
||||
logger.exception(f"Couldn't load keyfile {key_file}")
|
||||
sys.exit(-1)
|
||||
|
||||
# The filesystem location for served static files
|
||||
STATIC_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_STATIC_ROOT',
|
||||
CONFIG.get('static_root', None)
|
||||
)
|
||||
)
|
||||
|
||||
if STATIC_ROOT is None: # pragma: no cover
|
||||
print("ERROR: INVENTREE_STATIC_ROOT directory not defined")
|
||||
sys.exit(1)
|
||||
|
||||
# The filesystem location for served static files
|
||||
MEDIA_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_MEDIA_ROOT',
|
||||
CONFIG.get('media_root', None)
|
||||
)
|
||||
)
|
||||
|
||||
if MEDIA_ROOT is None: # pragma: no cover
|
||||
print("ERROR: INVENTREE_MEDIA_ROOT directory is not defined")
|
||||
sys.exit(1)
|
||||
|
||||
# List of allowed hosts (default = allow all)
|
||||
ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
|
||||
|
||||
@@ -183,17 +184,27 @@ if cors_opt:
|
||||
CORS_ORIGIN_ALLOW_ALL = cors_opt.get('allow_all', False)
|
||||
|
||||
if not CORS_ORIGIN_ALLOW_ALL:
|
||||
CORS_ORIGIN_WHITELIST = cors_opt.get('whitelist', []) # pragma: no cover
|
||||
CORS_ORIGIN_WHITELIST = cors_opt.get('whitelist', [])
|
||||
|
||||
# Web URL endpoint for served static files
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = []
|
||||
# The filesystem location for served static files
|
||||
STATIC_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_STATIC_ROOT',
|
||||
CONFIG.get('static_root', '/home/inventree/data/static')
|
||||
)
|
||||
)
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'InvenTree', 'static'),
|
||||
]
|
||||
|
||||
# Translated Template settings
|
||||
STATICFILES_I18_PREFIX = 'i18n'
|
||||
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js', 'translated')
|
||||
STATICFILES_I18_TRG = os.path.join(BASE_DIR, 'InvenTree', 'static_i18n')
|
||||
STATICFILES_I18_TRG = STATICFILES_DIRS[0] + '_' + STATICFILES_I18_PREFIX
|
||||
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
|
||||
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX)
|
||||
|
||||
@@ -207,11 +218,19 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
|
||||
# Web URL endpoint for served media files
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
if DEBUG:
|
||||
logger.info("InvenTree running with DEBUG enabled")
|
||||
# The filesystem location for served static files
|
||||
MEDIA_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_MEDIA_ROOT',
|
||||
CONFIG.get('media_root', '/home/inventree/data/media')
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
||||
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||
if DEBUG:
|
||||
logger.info("InvenTree running in DEBUG mode")
|
||||
|
||||
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
||||
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||
|
||||
# Application definition
|
||||
|
||||
@@ -221,13 +240,9 @@ INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'user_sessions', # db user sessions
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
|
||||
# Maintenance
|
||||
'maintenance_mode',
|
||||
|
||||
# InvenTree apps
|
||||
'build.apps.BuildConfig',
|
||||
@@ -239,7 +254,6 @@ INSTALLED_APPS = [
|
||||
'report.apps.ReportConfig',
|
||||
'stock.apps.StockConfig',
|
||||
'users.apps.UsersConfig',
|
||||
'plugin.apps.PluginAppConfig',
|
||||
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||
|
||||
# Third part add-ons
|
||||
@@ -259,64 +273,34 @@ INSTALLED_APPS = [
|
||||
'error_report', # Error reporting in the admin interface
|
||||
'django_q',
|
||||
'formtools', # Form wizard tools
|
||||
|
||||
'allauth', # Base app for SSO
|
||||
'allauth.account', # Extend user with accounts
|
||||
'allauth.socialaccount', # Use 'social' providers
|
||||
|
||||
'django_otp', # OTP is needed for MFA - base package
|
||||
'django_otp.plugins.otp_totp', # Time based OTP
|
||||
'django_otp.plugins.otp_static', # Backup codes
|
||||
|
||||
'allauth_2fa', # MFA flow for allauth
|
||||
]
|
||||
|
||||
MIDDLEWARE = CONFIG.get('middleware', [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'x_forwarded_for.middleware.XForwardedForMiddleware',
|
||||
'user_sessions.middleware.SessionMiddleware', # db user sessions
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'InvenTree.middleware.InvenTreeRemoteUserMiddleware', # Remote / proxy auth
|
||||
'django_otp.middleware.OTPMiddleware', # MFA support
|
||||
'InvenTree.middleware.CustomAllauthTwoFactorMiddleware', # Flow control for allauth
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'InvenTree.middleware.AuthRequiredMiddleware',
|
||||
'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
|
||||
'maintenance_mode.middleware.MaintenanceModeMiddleware',
|
||||
'InvenTree.middleware.AuthRequiredMiddleware'
|
||||
])
|
||||
|
||||
# Error reporting middleware
|
||||
MIDDLEWARE.append('error_report.middleware.ExceptionProcessor')
|
||||
|
||||
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
|
||||
'django.contrib.auth.backends.RemoteUserBackend', # proxy login
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers
|
||||
'django.contrib.auth.backends.ModelBackend'
|
||||
])
|
||||
|
||||
# If the debug toolbar is enabled, add the modules
|
||||
if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover
|
||||
if DEBUG and CONFIG.get('debug_toolbar', False):
|
||||
logger.info("Running with DEBUG_TOOLBAR enabled")
|
||||
INSTALLED_APPS.append('debug_toolbar')
|
||||
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
|
||||
|
||||
# Allow secure http developer server in debug mode
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append('sslserver')
|
||||
|
||||
# InvenTree URL configuration
|
||||
|
||||
# Base URL for admin pages (default="admin")
|
||||
INVENTREE_ADMIN_URL = get_setting(
|
||||
'INVENTREE_ADMIN_URL',
|
||||
CONFIG.get('admin_url', 'admin'),
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'InvenTree.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
@@ -328,6 +312,7 @@ TEMPLATES = [
|
||||
os.path.join(MEDIA_ROOT, 'report'),
|
||||
os.path.join(MEDIA_ROOT, 'label'),
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
@@ -335,24 +320,16 @@ TEMPLATES = [
|
||||
'django.template.context_processors.i18n',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
# Custom InvenTree context processors
|
||||
'InvenTree.context.health_status',
|
||||
'InvenTree.context.status_codes',
|
||||
'InvenTree.context.user_roles',
|
||||
],
|
||||
'loaders': [(
|
||||
'django.template.loaders.cached.Loader', [
|
||||
'plugin.template.PluginTemplateLoader',
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
])
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
|
||||
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
|
||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
@@ -371,6 +348,63 @@ REST_FRAMEWORK = {
|
||||
|
||||
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
||||
|
||||
background_workers = os.environ.get('INVENTREE_BACKGROUND_WORKERS', None)
|
||||
|
||||
if background_workers is not None:
|
||||
try:
|
||||
background_workers = int(background_workers)
|
||||
except ValueError:
|
||||
background_workers = None
|
||||
|
||||
if background_workers is None:
|
||||
# Sensible default?
|
||||
background_workers = 4
|
||||
|
||||
# django-q configuration
|
||||
Q_CLUSTER = {
|
||||
'name': 'InvenTree',
|
||||
'workers': background_workers,
|
||||
'timeout': 90,
|
||||
'retry': 120,
|
||||
'queue_limit': 50,
|
||||
'bulk': 10,
|
||||
'orm': 'default',
|
||||
'sync': False,
|
||||
}
|
||||
|
||||
# Markdownx configuration
|
||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
||||
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
|
||||
|
||||
# Markdownify configuration
|
||||
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
|
||||
|
||||
MARKDOWNIFY_WHITELIST_TAGS = [
|
||||
'a',
|
||||
'abbr',
|
||||
'b',
|
||||
'blockquote',
|
||||
'em',
|
||||
'h1', 'h2', 'h3',
|
||||
'i',
|
||||
'img',
|
||||
'li',
|
||||
'ol',
|
||||
'p',
|
||||
'strong',
|
||||
'ul'
|
||||
]
|
||||
|
||||
MARKDOWNIFY_WHITELIST_ATTRS = [
|
||||
'href',
|
||||
'src',
|
||||
'alt',
|
||||
]
|
||||
|
||||
MARKDOWNIFY_BLEACH = False
|
||||
|
||||
DATABASES = {}
|
||||
|
||||
"""
|
||||
Configure the database backend based on the user-specified values.
|
||||
|
||||
@@ -379,7 +413,7 @@ Configure the database backend based on the user-specified values.
|
||||
- The following code lets the user "mix and match" database configuration
|
||||
"""
|
||||
|
||||
logger.debug("Configuring database backend:")
|
||||
logger.info("Configuring database backend:")
|
||||
|
||||
# Extract database configuration from the config.yaml file
|
||||
db_config = CONFIG.get('database', {})
|
||||
@@ -404,7 +438,7 @@ for key in db_keys:
|
||||
reqiured_keys = ['ENGINE', 'NAME']
|
||||
|
||||
for key in reqiured_keys:
|
||||
if key not in db_config: # pragma: no cover
|
||||
if key not in db_config:
|
||||
error_msg = f'Missing required database configuration value {key}'
|
||||
logger.error(error_msg)
|
||||
|
||||
@@ -423,7 +457,7 @@ db_engine = db_config['ENGINE'].lower()
|
||||
|
||||
# Correct common misspelling
|
||||
if db_engine == 'sqlite':
|
||||
db_engine = 'sqlite3' # pragma: no cover
|
||||
db_engine = 'sqlite3'
|
||||
|
||||
if db_engine in ['sqlite3', 'postgresql', 'mysql']:
|
||||
# Prepend the required python module string
|
||||
@@ -433,205 +467,20 @@ if db_engine in ['sqlite3', 'postgresql', 'mysql']:
|
||||
db_name = db_config['NAME']
|
||||
db_host = db_config.get('HOST', "''")
|
||||
|
||||
logger.info(f"DB_ENGINE: {db_engine}")
|
||||
logger.info(f"DB_NAME: {db_name}")
|
||||
logger.info(f"DB_HOST: {db_host}")
|
||||
print("InvenTree Database Configuration")
|
||||
print("================================")
|
||||
print(f"ENGINE: {db_engine}")
|
||||
print(f"NAME: {db_name}")
|
||||
print(f"HOST: {db_host}")
|
||||
|
||||
"""
|
||||
In addition to base-level database configuration, we may wish to specify specific options to the database backend
|
||||
Ref: https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-OPTIONS
|
||||
"""
|
||||
DATABASES['default'] = db_config
|
||||
|
||||
# 'OPTIONS' or 'options' can be specified in config.yaml
|
||||
# Set useful sensible timeouts for a transactional webserver to communicate
|
||||
# with its database server, that is, if the webserver is having issues
|
||||
# connecting to the database server (such as a replica failover) don't sit and
|
||||
# wait for possibly an hour or more, just tell the client something went wrong
|
||||
# and let the client retry when they want to.
|
||||
db_options = db_config.get("OPTIONS", db_config.get("options", {}))
|
||||
|
||||
# Specific options for postgres backend
|
||||
if "postgres" in db_engine: # pragma: no cover
|
||||
from psycopg2.extensions import (ISOLATION_LEVEL_READ_COMMITTED,
|
||||
ISOLATION_LEVEL_SERIALIZABLE)
|
||||
|
||||
# Connection timeout
|
||||
if "connect_timeout" not in db_options:
|
||||
# The DB server is in the same data center, it should not take very
|
||||
# long to connect to the database server
|
||||
# # seconds, 2 is minium allowed by libpq
|
||||
db_options["connect_timeout"] = int(
|
||||
os.getenv("INVENTREE_DB_TIMEOUT", 2)
|
||||
)
|
||||
|
||||
# Setup TCP keepalive
|
||||
# DB server is in the same DC, it should not become unresponsive for
|
||||
# very long. With the defaults below we wait 5 seconds for the network
|
||||
# issue to resolve itself. It it that doesn't happen whatever happened
|
||||
# is probably fatal and no amount of waiting is going to fix it.
|
||||
# # 0 - TCP Keepalives disabled; 1 - enabled
|
||||
if "keepalives" not in db_options:
|
||||
db_options["keepalives"] = int(
|
||||
os.getenv("INVENTREE_DB_TCP_KEEPALIVES", "1")
|
||||
)
|
||||
# # Seconds after connection is idle to send keep alive
|
||||
if "keepalives_idle" not in db_options:
|
||||
db_options["keepalives_idle"] = int(
|
||||
os.getenv("INVENTREE_DB_TCP_KEEPALIVES_IDLE", "1")
|
||||
)
|
||||
# # Seconds after missing ACK to send another keep alive
|
||||
if "keepalives_interval" not in db_options:
|
||||
db_options["keepalives_interval"] = int(
|
||||
os.getenv("INVENTREE_DB_TCP_KEEPALIVES_INTERVAL", "1")
|
||||
)
|
||||
# # Number of missing ACKs before we close the connection
|
||||
if "keepalives_count" not in db_options:
|
||||
db_options["keepalives_count"] = int(
|
||||
os.getenv("INVENTREE_DB_TCP_KEEPALIVES_COUNT", "5")
|
||||
)
|
||||
# # Milliseconds for how long pending data should remain unacked
|
||||
# by the remote server
|
||||
# TODO: Supported starting in PSQL 11
|
||||
# "tcp_user_timeout": int(os.getenv("PGTCP_USER_TIMEOUT", "1000"),
|
||||
|
||||
# Postgres's default isolation level is Read Committed which is
|
||||
# normally fine, but most developers think the database server is
|
||||
# actually going to do Serializable type checks on the queries to
|
||||
# protect against simultaneous changes.
|
||||
# https://www.postgresql.org/docs/devel/transaction-iso.html
|
||||
# https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level
|
||||
if "isolation_level" not in db_options:
|
||||
serializable = _is_true(
|
||||
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true")
|
||||
)
|
||||
db_options["isolation_level"] = (
|
||||
ISOLATION_LEVEL_SERIALIZABLE
|
||||
if serializable
|
||||
else ISOLATION_LEVEL_READ_COMMITTED
|
||||
)
|
||||
|
||||
# Specific options for MySql / MariaDB backend
|
||||
if "mysql" in db_engine: # pragma: no cover
|
||||
# TODO TCP time outs and keepalives
|
||||
|
||||
# MariaDB's default isolation level is Repeatable Read which is
|
||||
# normally fine, but most developers think the database server is
|
||||
# actually going to Serializable type checks on the queries to
|
||||
# protect against siumltaneous changes.
|
||||
# https://mariadb.com/kb/en/mariadb-transactions-and-isolation-levels-for-sql-server-users/#changing-the-isolation-level
|
||||
# https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level
|
||||
if "isolation_level" not in db_options:
|
||||
serializable = _is_true(
|
||||
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true")
|
||||
)
|
||||
db_options["isolation_level"] = (
|
||||
"serializable" if serializable else "read committed"
|
||||
)
|
||||
|
||||
# Specific options for sqlite backend
|
||||
if "sqlite" in db_engine:
|
||||
# TODO: Verify timeouts are not an issue because no network is involved for SQLite
|
||||
|
||||
# SQLite's default isolation level is Serializable due to SQLite's
|
||||
# single writer implementation. Presumably as a result of this, it is
|
||||
# not possible to implement any lower isolation levels in SQLite.
|
||||
# https://www.sqlite.org/isolation.html
|
||||
pass
|
||||
|
||||
# Provide OPTIONS dict back to the database configuration dict
|
||||
db_config['OPTIONS'] = db_options
|
||||
|
||||
# Set testing options for the database
|
||||
db_config['TEST'] = {
|
||||
'CHARSET': 'utf8',
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
},
|
||||
}
|
||||
|
||||
# Set collation option for mysql test database
|
||||
if 'mysql' in db_engine:
|
||||
db_config['TEST']['COLLATION'] = 'utf8_general_ci'
|
||||
|
||||
DATABASES = {
|
||||
'default': db_config
|
||||
}
|
||||
|
||||
_cache_config = CONFIG.get("cache", {})
|
||||
_cache_host = _cache_config.get("host", os.getenv("INVENTREE_CACHE_HOST"))
|
||||
_cache_port = _cache_config.get(
|
||||
"port", os.getenv("INVENTREE_CACHE_PORT", "6379")
|
||||
)
|
||||
|
||||
if _cache_host: # pragma: no cover
|
||||
# We are going to rely upon a possibly non-localhost for our cache,
|
||||
# so don't wait too long for the cache as nothing in the cache should be
|
||||
# irreplacable.
|
||||
_cache_options = {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
|
||||
"SOCKET_TIMEOUT": int(os.getenv("CACHE_SOCKET_TIMEOUT", "2")),
|
||||
"CONNECTION_POOL_KWARGS": {
|
||||
"socket_keepalive": _is_true(
|
||||
os.getenv("CACHE_TCP_KEEPALIVE", "1")
|
||||
),
|
||||
"socket_keepalive_options": {
|
||||
socket.TCP_KEEPCNT: int(
|
||||
os.getenv("CACHE_KEEPALIVES_COUNT", "5")
|
||||
),
|
||||
socket.TCP_KEEPIDLE: int(
|
||||
os.getenv("CACHE_KEEPALIVES_IDLE", "1")
|
||||
),
|
||||
socket.TCP_KEEPINTVL: int(
|
||||
os.getenv("CACHE_KEEPALIVES_INTERVAL", "1")
|
||||
),
|
||||
socket.TCP_USER_TIMEOUT: int(
|
||||
os.getenv("CACHE_TCP_USER_TIMEOUT", "1000")
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": f"redis://{_cache_host}:{_cache_port}/0",
|
||||
"OPTIONS": _cache_options,
|
||||
},
|
||||
}
|
||||
else:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
# 4 background workers seems like a sensible default
|
||||
background_workers = int(os.environ.get('INVENTREE_BACKGROUND_WORKERS', 4))
|
||||
except ValueError: # pragma: no cover
|
||||
background_workers = 4
|
||||
|
||||
# django-q configuration
|
||||
Q_CLUSTER = {
|
||||
'name': 'InvenTree',
|
||||
'workers': background_workers,
|
||||
'timeout': 90,
|
||||
'retry': 120,
|
||||
'queue_limit': 50,
|
||||
'bulk': 10,
|
||||
'orm': 'default',
|
||||
'sync': False,
|
||||
}
|
||||
|
||||
if _cache_host: # pragma: no cover
|
||||
# If using external redis cache, make the cache the broker for Django Q
|
||||
# as well
|
||||
Q_CLUSTER["django_redis"] = "worker"
|
||||
|
||||
# database user sessions
|
||||
SESSION_ENGINE = 'user_sessions.backends.db'
|
||||
LOGOUT_REDIRECT_URL = 'index'
|
||||
SILENCED_SYSTEM_CHECKS = [
|
||||
'admin.E410',
|
||||
]
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
||||
|
||||
@@ -655,7 +504,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
EXTRA_URL_SCHEMES = CONFIG.get('extra_url_schemes', [])
|
||||
|
||||
if type(EXTRA_URL_SCHEMES) not in [list]: # pragma: no cover
|
||||
if not type(EXTRA_URL_SCHEMES) in [list]:
|
||||
logger.warning("extra_url_schemes not correctly formatted")
|
||||
EXTRA_URL_SCHEMES = []
|
||||
|
||||
@@ -666,24 +515,18 @@ LANGUAGE_CODE = CONFIG.get('language', 'en-us')
|
||||
|
||||
# If a new language translation is supported, it must be added here
|
||||
LANGUAGES = [
|
||||
('cs', _('Czech')),
|
||||
('de', _('German')),
|
||||
('el', _('Greek')),
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('es-mx', _('Spanish (Mexican)')),
|
||||
('fa', _('Farsi / Persian')),
|
||||
('fr', _('French')),
|
||||
('he', _('Hebrew')),
|
||||
('hu', _('Hungarian')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
('nl', _('Dutch')),
|
||||
('no', _('Norwegian')),
|
||||
('pl', _('Polish')),
|
||||
('pt', _('Portuguese')),
|
||||
('pt-BR', _('Portuguese (Brazilian)')),
|
||||
('ru', _('Russian')),
|
||||
('sv', _('Swedish')),
|
||||
('th', _('Thai')),
|
||||
@@ -692,25 +535,6 @@ LANGUAGES = [
|
||||
('zh-cn', _('Chinese')),
|
||||
]
|
||||
|
||||
# Testing interface translations
|
||||
if get_setting('TEST_TRANSLATIONS', False): # pragma: no cover
|
||||
# Set default language
|
||||
LANGUAGE_CODE = 'xx'
|
||||
|
||||
# Add to language catalog
|
||||
LANGUAGES.append(('xx', 'Test'))
|
||||
|
||||
# Add custom languages not provided by Django
|
||||
EXTRA_LANG_INFO = {
|
||||
'xx': {
|
||||
'code': 'xx',
|
||||
'name': 'Test',
|
||||
'name_local': 'Test'
|
||||
},
|
||||
}
|
||||
LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
|
||||
django.conf.locale.LANG_INFO = LANG_INFO
|
||||
|
||||
# Currencies available for use
|
||||
CURRENCIES = CONFIG.get(
|
||||
'currencies',
|
||||
@@ -721,7 +545,7 @@ CURRENCIES = CONFIG.get(
|
||||
|
||||
# Check that each provided currency is supported
|
||||
for currency in CURRENCIES:
|
||||
if currency not in moneyed.CURRENCIES: # pragma: no cover
|
||||
if currency not in moneyed.CURRENCIES:
|
||||
print(f"Currency code '{currency}' is not supported")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -795,14 +619,14 @@ USE_L10N = True
|
||||
# Do not use native timezone support in "test" mode
|
||||
# It generates a *lot* of cruft in the logs
|
||||
if not TESTING:
|
||||
USE_TZ = True # pragma: no cover
|
||||
USE_TZ = True
|
||||
|
||||
DATE_INPUT_FORMATS = [
|
||||
"%Y-%m-%d",
|
||||
]
|
||||
|
||||
# crispy forms use the bootstrap templates
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap4'
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap3'
|
||||
|
||||
# Use database transactions when importing / exporting data
|
||||
IMPORT_EXPORT_USE_TRANSACTIONS = True
|
||||
@@ -817,116 +641,3 @@ MESSAGE_TAGS = {
|
||||
messages.ERROR: 'alert alert-block alert-danger',
|
||||
messages.INFO: 'alert alert-block alert-info',
|
||||
}
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# Load the allauth social backends
|
||||
SOCIAL_BACKENDS = CONFIG.get('social_backends', [])
|
||||
for app in SOCIAL_BACKENDS:
|
||||
INSTALLED_APPS.append(app) # pragma: no cover
|
||||
|
||||
SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', [])
|
||||
|
||||
# settings for allauth
|
||||
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', CONFIG.get('login_confirm_days', 3))
|
||||
|
||||
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', CONFIG.get('login_attempts', 5))
|
||||
|
||||
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
|
||||
|
||||
# override forms / adapters
|
||||
ACCOUNT_FORMS = {
|
||||
'login': 'allauth.account.forms.LoginForm',
|
||||
'signup': 'InvenTree.forms.CustomSignupForm',
|
||||
'add_email': 'allauth.account.forms.AddEmailForm',
|
||||
'change_password': 'allauth.account.forms.ChangePasswordForm',
|
||||
'set_password': 'allauth.account.forms.SetPasswordForm',
|
||||
'reset_password': 'allauth.account.forms.ResetPasswordForm',
|
||||
'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm',
|
||||
'disconnect': 'allauth.socialaccount.forms.DisconnectForm',
|
||||
}
|
||||
|
||||
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
|
||||
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
|
||||
|
||||
# login settings
|
||||
REMOTE_LOGIN = get_setting('INVENTREE_REMOTE_LOGIN', CONFIG.get('remote_login', False))
|
||||
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', CONFIG.get('remote_login_header', 'REMOTE_USER'))
|
||||
|
||||
# Markdownx configuration
|
||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
||||
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
|
||||
|
||||
# Markdownify configuration
|
||||
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
|
||||
|
||||
MARKDOWNIFY_WHITELIST_TAGS = [
|
||||
'a',
|
||||
'abbr',
|
||||
'b',
|
||||
'blockquote',
|
||||
'em',
|
||||
'h1', 'h2', 'h3',
|
||||
'i',
|
||||
'img',
|
||||
'li',
|
||||
'ol',
|
||||
'p',
|
||||
'strong',
|
||||
'ul'
|
||||
]
|
||||
|
||||
MARKDOWNIFY_WHITELIST_ATTRS = [
|
||||
'href',
|
||||
'src',
|
||||
'alt',
|
||||
]
|
||||
|
||||
MARKDOWNIFY_BLEACH = False
|
||||
|
||||
# Maintenance mode
|
||||
MAINTENANCE_MODE_RETRY_AFTER = 60
|
||||
MAINTENANCE_MODE_STATE_BACKEND = 'maintenance_mode.backends.DefaultStorageBackend'
|
||||
|
||||
# Are plugins enabled?
|
||||
PLUGINS_ENABLED = _is_true(get_setting(
|
||||
'INVENTREE_PLUGINS_ENABLED',
|
||||
CONFIG.get('plugins_enabled', False),
|
||||
))
|
||||
|
||||
PLUGIN_FILE = get_plugin_file()
|
||||
|
||||
# Plugin Directories (local plugins will be loaded from these directories)
|
||||
PLUGIN_DIRS = ['plugin.builtin', ]
|
||||
|
||||
if not TESTING:
|
||||
# load local deploy directory in prod
|
||||
PLUGIN_DIRS.append('plugins') # pragma: no cover
|
||||
|
||||
if DEBUG or TESTING:
|
||||
# load samples in debug mode
|
||||
PLUGIN_DIRS.append('plugin.samples')
|
||||
|
||||
# Plugin test settings
|
||||
PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested?
|
||||
PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing?
|
||||
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
|
||||
PLUGIN_RETRY = get_setting('PLUGIN_RETRY', 5) # how often should plugin loading be tried?
|
||||
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
|
||||
|
||||
# User interface customization values
|
||||
CUSTOMIZE = get_setting(
|
||||
'INVENTREE_CUSTOMIZE',
|
||||
CONFIG.get('customize', {}),
|
||||
{}
|
||||
)
|
||||
|
||||
CUSTOM_LOGO = get_setting(
|
||||
'INVENTREE_CUSTOM_LOGO',
|
||||
CUSTOMIZE.get('logo', False)
|
||||
)
|
||||
|
||||
# check that the logo-file exsists in media
|
||||
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO):
|
||||
CUSTOM_LOGO = False
|
||||
logger.warning("The custom logo file could not be found in the default media storage")
|
||||
|
||||
10
InvenTree/InvenTree/static/bootstrap-table/bootstrap-table-en-US.min.js
vendored
Normal file
10
InvenTree/InvenTree/static/bootstrap-table/bootstrap-table-en-US.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @author: Dennis Hernández
|
||||
* @webSite: http://djhvscf.github.io/Blog
|
||||
* @update: zhixin wen <wenzhixin2010@gmail.com>
|
||||
*/
|
||||
|
||||
!($ => {
|
||||
const diacriticsMap = {}
|
||||
const defaultAccentsDiacritics = [
|
||||
{base: 'A', letters: '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F'},
|
||||
{base: 'AA',letters: '\uA732'},
|
||||
{base: 'AE',letters: '\u00C6\u01FC\u01E2'},
|
||||
{base: 'AO',letters: '\uA734'},
|
||||
{base: 'AU',letters: '\uA736'},
|
||||
{base: 'AV',letters: '\uA738\uA73A'},
|
||||
{base: 'AY',letters: '\uA73C'},
|
||||
{base: 'B', letters: '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181'},
|
||||
{base: 'C', letters: '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E'},
|
||||
{base: 'D', letters: '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779'},
|
||||
{base: 'DZ',letters: '\u01F1\u01C4'},
|
||||
{base: 'Dz',letters: '\u01F2\u01C5'},
|
||||
{base: 'E', letters: '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E'},
|
||||
{base: 'F', letters: '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B'},
|
||||
{base: 'G', letters: '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E'},
|
||||
{base: 'H', letters: '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D'},
|
||||
{base: 'I', letters: '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197'},
|
||||
{base: 'J', letters: '\u004A\u24BF\uFF2A\u0134\u0248'},
|
||||
{base: 'K', letters: '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2'},
|
||||
{base: 'L', letters: '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780'},
|
||||
{base: 'LJ',letters: '\u01C7'},
|
||||
{base: 'Lj',letters: '\u01C8'},
|
||||
{base: 'M', letters: '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C'},
|
||||
{base: 'N', letters: '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4'},
|
||||
{base: 'NJ',letters: '\u01CA'},
|
||||
{base: 'Nj',letters: '\u01CB'},
|
||||
{base: 'O', letters: '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C'},
|
||||
{base: 'OI',letters: '\u01A2'},
|
||||
{base: 'OO',letters: '\uA74E'},
|
||||
{base: 'OU',letters: '\u0222'},
|
||||
{base: 'OE',letters: '\u008C\u0152'},
|
||||
{base: 'oe',letters: '\u009C\u0153'},
|
||||
{base: 'P', letters: '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754'},
|
||||
{base: 'Q', letters: '\u0051\u24C6\uFF31\uA756\uA758\u024A'},
|
||||
{base: 'R', letters: '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782'},
|
||||
{base: 'S', letters: '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784'},
|
||||
{base: 'T', letters: '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786'},
|
||||
{base: 'TZ',letters: '\uA728'},
|
||||
{base: 'U', letters: '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244'},
|
||||
{base: 'V', letters: '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245'},
|
||||
{base: 'VY',letters: '\uA760'},
|
||||
{base: 'W', letters: '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72'},
|
||||
{base: 'X', letters: '\u0058\u24CD\uFF38\u1E8A\u1E8C'},
|
||||
{base: 'Y', letters: '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE'},
|
||||
{base: 'Z', letters: '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762'},
|
||||
{base: 'a', letters: '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250'},
|
||||
{base: 'aa',letters: '\uA733'},
|
||||
{base: 'ae',letters: '\u00E6\u01FD\u01E3'},
|
||||
{base: 'ao',letters: '\uA735'},
|
||||
{base: 'au',letters: '\uA737'},
|
||||
{base: 'av',letters: '\uA739\uA73B'},
|
||||
{base: 'ay',letters: '\uA73D'},
|
||||
{base: 'b', letters: '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253'},
|
||||
{base: 'c', letters: '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184'},
|
||||
{base: 'd', letters: '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A'},
|
||||
{base: 'dz',letters: '\u01F3\u01C6'},
|
||||
{base: 'e', letters: '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD'},
|
||||
{base: 'f', letters: '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C'},
|
||||
{base: 'g', letters: '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F'},
|
||||
{base: 'h', letters: '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265'},
|
||||
{base: 'hv',letters: '\u0195'},
|
||||
{base: 'i', letters: '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131'},
|
||||
{base: 'j', letters: '\u006A\u24D9\uFF4A\u0135\u01F0\u0249'},
|
||||
{base: 'k', letters: '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3'},
|
||||
{base: 'l', letters: '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747'},
|
||||
{base: 'lj',letters: '\u01C9'},
|
||||
{base: 'm', letters: '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F'},
|
||||
{base: 'n', letters: '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5'},
|
||||
{base: 'nj',letters: '\u01CC'},
|
||||
{base: 'o', letters: '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275'},
|
||||
{base: 'oi',letters: '\u01A3'},
|
||||
{base: 'ou',letters: '\u0223'},
|
||||
{base: 'oo',letters: '\uA74F'},
|
||||
{base: 'p',letters: '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755'},
|
||||
{base: 'q',letters: '\u0071\u24E0\uFF51\u024B\uA757\uA759'},
|
||||
{base: 'r',letters: '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783'},
|
||||
{base: 's',letters: '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B'},
|
||||
{base: 't',letters: '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787'},
|
||||
{base: 'tz',letters: '\uA729'},
|
||||
{base: 'u',letters: '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289'},
|
||||
{base: 'v',letters: '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C'},
|
||||
{base: 'vy',letters: '\uA761'},
|
||||
{base: 'w',letters: '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73'},
|
||||
{base: 'x',letters: '\u0078\u24E7\uFF58\u1E8B\u1E8D'},
|
||||
{base: 'y',letters: '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF'},
|
||||
{base: 'z',letters: '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763'}
|
||||
]
|
||||
|
||||
const initNeutraliser = () => {
|
||||
for (const diacritic of defaultAccentsDiacritics) {
|
||||
const letters = diacritic.letters
|
||||
for (let i = 0; i < letters.length; i++) {
|
||||
diacriticsMap[letters[i]] = diacritic.base
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable no-control-regex */
|
||||
const removeDiacritics = str => str.replace(/[^\u0000-\u007E]/g, a => diacriticsMap[a] || a)
|
||||
|
||||
$.extend($.fn.bootstrapTable.defaults, {
|
||||
searchAccentNeutralise: false
|
||||
})
|
||||
|
||||
$.BootstrapTable = class extends $.BootstrapTable {
|
||||
init () {
|
||||
if (this.options.searchAccentNeutralise) {
|
||||
initNeutraliser()
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
initSearch () {
|
||||
if (this.options.sidePagination !== 'server') {
|
||||
let s = this.searchText && this.searchText.toLowerCase()
|
||||
const f = $.isEmptyObject(this.filterColumns) ? null : this.filterColumns
|
||||
|
||||
// Check filter
|
||||
this.data = f ? this.options.data.filter((item, i) => {
|
||||
for (const key in f) {
|
||||
if (item[key] !== f[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}) : this.options.data
|
||||
|
||||
this.data = s ? this.options.data.filter((item, i) => {
|
||||
for (let [key, value] of Object.entries(item)) {
|
||||
key = $.isNumeric(key) ? parseInt(key, 10) : key
|
||||
const column = this.columns[this.fieldsColumnsIndex[key]]
|
||||
const j = this.header.fields.indexOf(key)
|
||||
|
||||
if (column && column.searchFormatter) {
|
||||
value = $.fn.bootstrapTable.utils.calculateObjectValue(column,
|
||||
this.header.formatters[j], [value, item, i], value)
|
||||
}
|
||||
|
||||
const index = this.header.fields.indexOf(key)
|
||||
if (index !== -1 && this.header.searchables[index] && typeof value === 'string') {
|
||||
if (this.options.searchAccentNeutralise) {
|
||||
value = removeDiacritics(value)
|
||||
s = removeDiacritics(s)
|
||||
}
|
||||
if (this.options.strictSearch) {
|
||||
if ((`${value}`).toLowerCase() === s) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if ((`${value}`).toLowerCase().includes(s)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}) : this.data
|
||||
}
|
||||
}
|
||||
}
|
||||
})(jQuery)
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Accent Neutralise",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to neutralise the words.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/accent-neutralise",
|
||||
"example": "#",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-accent-neutralise",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/accent-neutralise"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Auto Refresh",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to automatically refresh the table on an interval.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/auto-refresh",
|
||||
"example": "#",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-auto-refresh",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/auto-refresh"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "fenichaler",
|
||||
"image": "https://avatars.githubusercontent.com/u/3437075"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Cookie",
|
||||
"version": "1.2.1",
|
||||
"description": "Plugin to use the cookie of the browser.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/cookie",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/cookie.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-cookie",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/cookie"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Copy Rows",
|
||||
"version": "1.0.0",
|
||||
"description": "Allows pushing of selected column data to the clipboard.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/copy-rows",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/copy-rows.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "copy-rows",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/copy-rows"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "Homer Glascock",
|
||||
"image": "https://avatars1.githubusercontent.com/u/5546710"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "DeferURL",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to defer server side processing.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/defer-url",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/defer-url.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-defer-url",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/defer-url"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "rubensa",
|
||||
"image": "https://avatars1.githubusercontent.com/u/1469340"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Table Editable",
|
||||
"version": "1.1.0",
|
||||
"description": "Use the x-editable to in-place editing your table.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/editable",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/editable.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "x-editable",
|
||||
"url": "https://github.com/vitalets/x-editable"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "wenzhixin",
|
||||
"image": "https://avatars1.githubusercontent.com/u/2117018"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Table Export",
|
||||
"version": "1.1.0",
|
||||
"description": "Export your table data to JSON, XML, CSV, TXT, SQL, Word, Excel, PNG, PDF.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/export",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/export.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "tableExport.jquery.plugin",
|
||||
"url": "https://github.com/hhurz/tableExport.jquery.plugin"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "wenzhixin",
|
||||
"image": "https://avatars1.githubusercontent.com/u/2117018"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Filter Control",
|
||||
"version": "2.1.0",
|
||||
"description": "Plugin to add input/select element on the top of the columns in order to filter the data.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/filter-control",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/filter-control.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-filter-control",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/filter-control"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Group By V2",
|
||||
"version": "1.0.0",
|
||||
"description": "Group the data by field",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by-v2",
|
||||
"example": "",
|
||||
"plugins": [],
|
||||
"author": {
|
||||
"name": "Knoxvillekm",
|
||||
"image": "https://avatars3.githubusercontent.com/u/11072464"
|
||||
}
|
||||
}
|
||||
53
InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css
vendored
Normal file
53
InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.css
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
table.treetable tbody tr td {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
table.treetable span {
|
||||
background-position: center left;
|
||||
background-repeat: no-repeat;
|
||||
padding: .2em 0 .2em 1.5em;
|
||||
}
|
||||
|
||||
table.treetable tr.collapsed span.indenter a {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHlJREFUeNrcU1sNgDAQ6wgmcAM2MICGGlg1gJnNzWQcvwQGy1j4oUl/7tH0mpwzM7SgQyO+EZAUWh2MkkzSWhJwuRAlHYsJwEwyvs1gABDuzqoJcTw5qxaIJN0bgQRgIjnlmn1heSO5PE6Y2YXe+5Cr5+h++gs12AcAS6FS+7YOsj4AAAAASUVORK5CYII=);
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
table.treetable tr.expanded span.indenter a {
|
||||
background-image: url(data:image/png;base64,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);
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
table.treetable tr.branch {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
table.treetable tr.selected {
|
||||
background-color: #3875d7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
table.treetable tr span.indenter a {
|
||||
outline: none; /* Expander shows outline after upgrading to 3.0 (#141) */
|
||||
}
|
||||
|
||||
table.treetable tr.collapsed.selected span.indenter a {
|
||||
background-image: url(data:image/png;base64,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);
|
||||
}
|
||||
|
||||
table.treetable tr.expanded.selected span.indenter a {
|
||||
background-image: url(data:image/png;base64,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);
|
||||
}
|
||||
|
||||
table.treetable tr.accept {
|
||||
background-color: #a3bce4;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
table.treetable tr.collapsed.accept td span.indenter a {
|
||||
background-image: url(data:image/x-png;base64,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);
|
||||
}
|
||||
|
||||
table.treetable tr.expanded.accept td span.indenter a {
|
||||
background-image: url(data:image/x-png;base64,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);
|
||||
}
|
||||
243
InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js
vendored
Normal file
243
InvenTree/InvenTree/static/bootstrap-table/extensions/group-by/bootstrap-table-group-by.js
vendored
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* @author: Dennis Hernández
|
||||
* @webSite: http://djhvscf.github.io/Blog
|
||||
* @version: v1.1.0
|
||||
*/
|
||||
|
||||
!function ($) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var originalRowAttr,
|
||||
dataTTId = 'data-tt-id',
|
||||
dataTTParentId = 'data-tt-parent-id',
|
||||
obj = {},
|
||||
parentId = undefined;
|
||||
|
||||
var getParentRowId = function (that, id) {
|
||||
var parentRows = that.$body.find('tr').not('[' + 'data-tt-parent-id]');
|
||||
|
||||
for (var i = 0; i < parentRows.length; i++) {
|
||||
if (i === id) {
|
||||
return $(parentRows[i]).attr('data-tt-id');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
var sumData = function (that, data) {
|
||||
var sumRow = {};
|
||||
$.each(data, function (i, row) {
|
||||
if (!row.IsParent) {
|
||||
for (var prop in row) {
|
||||
if (!isNaN(parseFloat(row[prop]))) {
|
||||
if (that.columns[that.fieldsColumnsIndex[prop]].groupBySumGroup) {
|
||||
if (sumRow[prop] === undefined) {
|
||||
sumRow[prop] = 0;
|
||||
}
|
||||
sumRow[prop] += +row[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return sumRow;
|
||||
};
|
||||
|
||||
var rowAttr = function (row, index) {
|
||||
//Call the User Defined Function
|
||||
originalRowAttr.apply([row, index]);
|
||||
|
||||
obj[dataTTId.toString()] = index;
|
||||
|
||||
if (!row.IsParent) {
|
||||
obj[dataTTParentId.toString()] = parentId === undefined ? index : parentId;
|
||||
} else {
|
||||
parentId = index;
|
||||
delete obj[dataTTParentId.toString()];
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
var setObjectKeys = function () {
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
|
||||
Object.keys = function (o) {
|
||||
if (o !== Object(o)) {
|
||||
throw new TypeError('Object.keys called on a non-object');
|
||||
}
|
||||
var k = [],
|
||||
p;
|
||||
for (p in o) {
|
||||
if (Object.prototype.hasOwnProperty.call(o, p)) {
|
||||
k.push(p);
|
||||
}
|
||||
}
|
||||
return k;
|
||||
}
|
||||
};
|
||||
|
||||
var getDataArrayFromItem = function (that, item) {
|
||||
var itemDataArray = [];
|
||||
for (var i = 0; i < that.options.groupByField.length; i++) {
|
||||
itemDataArray.push(item[that.options.groupByField[i]]);
|
||||
}
|
||||
|
||||
return itemDataArray;
|
||||
};
|
||||
|
||||
var getNewRow = function (that, result, index) {
|
||||
var newRow = {};
|
||||
for (var i = 0; i < that.options.groupByField.length; i++) {
|
||||
newRow[that.options.groupByField[i].toString()] = result[index][0][that.options.groupByField[i]];
|
||||
}
|
||||
|
||||
newRow.IsParent = true;
|
||||
|
||||
return newRow;
|
||||
};
|
||||
|
||||
var groupBy = function (array, f) {
|
||||
var groups = {};
|
||||
$.each(array, function (i, o) {
|
||||
var group = JSON.stringify(f(o));
|
||||
groups[group] = groups[group] || [];
|
||||
groups[group].push(o);
|
||||
});
|
||||
return Object.keys(groups).map(function (group) {
|
||||
return groups[group];
|
||||
});
|
||||
};
|
||||
|
||||
var makeGrouped = function (that, data) {
|
||||
var newData = [],
|
||||
sumRow = {};
|
||||
|
||||
var result = groupBy(data, function (item) {
|
||||
return getDataArrayFromItem(that, item);
|
||||
});
|
||||
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
result[i].unshift(getNewRow(that, result, i));
|
||||
if (that.options.groupBySumGroup) {
|
||||
sumRow = sumData(that, result[i]);
|
||||
if (!$.isEmptyObject(sumRow)) {
|
||||
result[i].push(sumRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newData = newData.concat.apply(newData, result);
|
||||
|
||||
if (!that.options.loaded && newData.length > 0) {
|
||||
that.options.loaded = true;
|
||||
that.options.originalData = that.options.data;
|
||||
that.options.data = newData;
|
||||
}
|
||||
|
||||
return newData;
|
||||
};
|
||||
|
||||
$.extend($.fn.bootstrapTable.defaults, {
|
||||
groupBy: false,
|
||||
groupByField: [],
|
||||
groupBySumGroup: false,
|
||||
groupByInitExpanded: undefined, //node, 'all'
|
||||
//internal variables
|
||||
loaded: false,
|
||||
originalData: undefined
|
||||
});
|
||||
|
||||
$.fn.bootstrapTable.methods.push('collapseAll', 'expandAll', 'refreshGroupByField');
|
||||
|
||||
$.extend($.fn.bootstrapTable.COLUMN_DEFAULTS, {
|
||||
groupBySumGroup: false
|
||||
});
|
||||
|
||||
var BootstrapTable = $.fn.bootstrapTable.Constructor,
|
||||
_init = BootstrapTable.prototype.init,
|
||||
_initData = BootstrapTable.prototype.initData;
|
||||
|
||||
BootstrapTable.prototype.init = function () {
|
||||
//Temporal validation
|
||||
if (!this.options.sortName) {
|
||||
if ((this.options.groupBy) && (this.options.groupByField.length > 0)) {
|
||||
var that = this;
|
||||
|
||||
// Compatibility: IE < 9 and old browsers
|
||||
if (!Object.keys) {
|
||||
$.fn.bootstrapTable.utils.objectKeys();
|
||||
}
|
||||
|
||||
//Make sure that the internal variables are set correctly
|
||||
this.options.loaded = false;
|
||||
this.options.originalData = undefined;
|
||||
|
||||
originalRowAttr = this.options.rowAttributes;
|
||||
this.options.rowAttributes = rowAttr;
|
||||
this.$el.off('post-body.bs.table').on('post-body.bs.table', function () {
|
||||
that.$el.treetable({
|
||||
expandable: true,
|
||||
onNodeExpand: function () {
|
||||
if (that.options.height) {
|
||||
that.resetHeader();
|
||||
}
|
||||
},
|
||||
onNodeCollapse: function () {
|
||||
if (that.options.height) {
|
||||
that.resetHeader();
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
if (that.options.groupByInitExpanded !== undefined) {
|
||||
if (typeof that.options.groupByInitExpanded === 'number') {
|
||||
that.expandNode(that.options.groupByInitExpanded);
|
||||
} else if (that.options.groupByInitExpanded.toLowerCase() === 'all') {
|
||||
that.expandAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
_init.apply(this, Array.prototype.slice.apply(arguments));
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.initData = function (data, type) {
|
||||
//Temporal validation
|
||||
if (!this.options.sortName) {
|
||||
if ((this.options.groupBy) && (this.options.groupByField.length > 0)) {
|
||||
|
||||
this.options.groupByField = typeof this.options.groupByField === 'string' ?
|
||||
this.options.groupByField.replace('[', '').replace(']', '')
|
||||
.replace(/ /g, '').toLowerCase().split(',') : this.options.groupByField;
|
||||
|
||||
data = makeGrouped(this, data ? data : this.options.data);
|
||||
}
|
||||
}
|
||||
_initData.apply(this, [data, type]);
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.expandAll = function () {
|
||||
this.$el.treetable('expandAll');
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.collapseAll = function () {
|
||||
this.$el.treetable('collapseAll');
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.expandNode = function (id) {
|
||||
id = getParentRowId(this, id);
|
||||
if (id !== undefined) {
|
||||
this.$el.treetable('expandNode', id);
|
||||
}
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.refreshGroupByField = function (groupByFields) {
|
||||
if (!$.fn.bootstrapTable.utils.compareObjects(this.options.groupByField, groupByFields)) {
|
||||
this.options.groupByField = groupByFields;
|
||||
this.load(this.options.originalData);
|
||||
}
|
||||
};
|
||||
}(jQuery);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Group By",
|
||||
"version": "1.1.0",
|
||||
"description": "Plugin to group the data by fields.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by",
|
||||
"example": "#",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-group-by",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/group-by"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "i18n Enhance",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to add i18n API in order to change column's title and table locale.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/i18n-enhance.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-i18n-enhance",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/i18n-enhance"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "Jewway",
|
||||
"image": "https://avatars0.githubusercontent.com/u/3501899"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Key Events",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to support the key events in the bootstrap table.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/key-events.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-key-events",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/key-events"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Mobile",
|
||||
"version": "1.1.0",
|
||||
"description": "Plugin to support the responsive feature.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/mobile.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-mobile",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/mobile"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @author Homer Glascock <HopGlascock@gmail.com>
|
||||
* @version: v1.0.0
|
||||
*/
|
||||
|
||||
!function ($) {
|
||||
"use strict";
|
||||
|
||||
var sprintf = $.fn.bootstrapTable.utils.sprintf;
|
||||
|
||||
var reInit = function (self) {
|
||||
self.initHeader();
|
||||
self.initSearch();
|
||||
self.initPagination();
|
||||
self.initBody();
|
||||
};
|
||||
|
||||
$.extend($.fn.bootstrapTable.defaults, {
|
||||
showToggleBtn: false,
|
||||
multiToggleDefaults: [], //column names go here
|
||||
});
|
||||
|
||||
$.fn.bootstrapTable.methods.push('hideAllColumns', 'showAllColumns');
|
||||
|
||||
var BootstrapTable = $.fn.bootstrapTable.Constructor,
|
||||
_initToolbar = BootstrapTable.prototype.initToolbar;
|
||||
|
||||
BootstrapTable.prototype.initToolbar = function () {
|
||||
|
||||
_initToolbar.apply(this, Array.prototype.slice.apply(arguments));
|
||||
|
||||
var that = this,
|
||||
$btnGroup = this.$toolbar.find('>.btn-group');
|
||||
|
||||
if (typeof this.options.multiToggleDefaults === 'string') {
|
||||
this.options.multiToggleDefaults = JSON.parse(this.options.multiToggleDefaults);
|
||||
}
|
||||
|
||||
if (this.options.showToggleBtn && this.options.showColumns) {
|
||||
var showbtn = "<button class='btn btn-default hidden' id='showAllBtn'><span class='glyphicon glyphicon-resize-full icon-zoom-in'></span></button>",
|
||||
hidebtn = "<button class='btn btn-default' id='hideAllBtn'><span class='glyphicon glyphicon-resize-small icon-zoom-out'></span></button>";
|
||||
|
||||
$btnGroup.append(showbtn + hidebtn);
|
||||
|
||||
$btnGroup.find('#showAllBtn').click(function () { that.showAllColumns();
|
||||
$btnGroup.find('#hideAllBtn').toggleClass('hidden');
|
||||
$btnGroup.find('#showAllBtn').toggleClass('hidden');
|
||||
});
|
||||
$btnGroup.find('#hideAllBtn').click(function () { that.hideAllColumns();
|
||||
$btnGroup.find('#hideAllBtn').toggleClass('hidden');
|
||||
$btnGroup.find('#showAllBtn').toggleClass('hidden');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.hideAllColumns = function () {
|
||||
var that = this,
|
||||
defaults = that.options.multiToggleDefaults;
|
||||
|
||||
$.each(this.columns, function (index, column) {
|
||||
//if its one of the defaults dont touch it
|
||||
if (defaults.indexOf(column.field) == -1 && column.switchable) {
|
||||
column.visible = false;
|
||||
var $items = that.$toolbar.find('.keep-open input').prop('disabled', false);
|
||||
$items.filter(sprintf('[value="%s"]', index)).prop('checked', false);
|
||||
}
|
||||
});
|
||||
|
||||
reInit(that);
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.showAllColumns = function () {
|
||||
var that = this;
|
||||
$.each(this.columns, function (index, column) {
|
||||
if (column.switchable) {
|
||||
column.visible = true;
|
||||
}
|
||||
|
||||
var $items = that.$toolbar.find('.keep-open input').prop('disabled', false);
|
||||
$items.filter(sprintf('[value="%s"]', index)).prop('checked', true);
|
||||
});
|
||||
|
||||
reInit(that);
|
||||
|
||||
that.toggleColumn(0, that.columns[0].visible, false);
|
||||
};
|
||||
|
||||
}(jQuery);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Multi Column Toggle",
|
||||
"version": "1.0.0",
|
||||
"description": "Allows hiding and showing of multiple columns at once.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multi-column-toggle",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/multi-column-toggle.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "multi-column-toggle",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multi-column-toggle"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "Homer Glascock",
|
||||
"image": "https://avatars1.githubusercontent.com/u/5546710"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @author: Dennis Hernández
|
||||
* @webSite: http://djhvscf.github.io/Blog
|
||||
* @version: v1.0.0
|
||||
*/
|
||||
|
||||
!function ($) {
|
||||
|
||||
'use strict';
|
||||
|
||||
$.extend($.fn.bootstrapTable.defaults, {
|
||||
multipleSearch: false,
|
||||
delimeter: " "
|
||||
});
|
||||
|
||||
var BootstrapTable = $.fn.bootstrapTable.Constructor,
|
||||
_initSearch = BootstrapTable.prototype.initSearch;
|
||||
|
||||
BootstrapTable.prototype.initSearch = function () {
|
||||
if (this.options.multipleSearch) {
|
||||
if (this.searchText === undefined) {
|
||||
return;
|
||||
}
|
||||
var strArray = this.searchText.split(this.options.delimeter),
|
||||
that = this,
|
||||
f = $.isEmptyObject(this.filterColumns) ? null : this.filterColumns,
|
||||
dataFiltered = [];
|
||||
|
||||
if (strArray.length === 1) {
|
||||
_initSearch.apply(this, Array.prototype.slice.apply(arguments));
|
||||
} else {
|
||||
for (var i = 0; i < strArray.length; i++) {
|
||||
var str = strArray[i].trim();
|
||||
dataFiltered = str ? $.grep(dataFiltered.length === 0 ? this.options.data : dataFiltered, function (item, i) {
|
||||
for (var key in item) {
|
||||
key = $.isNumeric(key) ? parseInt(key, 10) : key;
|
||||
var value = item[key],
|
||||
column = that.columns[that.fieldsColumnsIndex[key]],
|
||||
j = $.inArray(key, that.header.fields);
|
||||
|
||||
// Fix #142: search use formated data
|
||||
if (column && column.searchFormatter) {
|
||||
value = $.fn.bootstrapTable.utils.calculateObjectValue(column,
|
||||
that.header.formatters[j], [value, item, i], value);
|
||||
}
|
||||
|
||||
var index = $.inArray(key, that.header.fields);
|
||||
if (index !== -1 && that.header.searchables[index] && (typeof value === 'string' || typeof value === 'number')) {
|
||||
if (that.options.strictSearch) {
|
||||
if ((value + '').toLowerCase() === str) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if ((value + '').toLowerCase().indexOf(str) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) : this.data;
|
||||
}
|
||||
|
||||
this.data = dataFiltered;
|
||||
}
|
||||
} else {
|
||||
_initSearch.apply(this, Array.prototype.slice.apply(arguments));
|
||||
}
|
||||
};
|
||||
|
||||
}(jQuery);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Multiple Search",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to support the multiple search.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-search",
|
||||
"example": "#",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-multiple-search",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-search"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.multiple-select-row-selected {
|
||||
background: lightBlue
|
||||
}
|
||||
|
||||
.table tbody tr:hover td,
|
||||
.table tbody tr:hover th {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
.table-striped tbody tr:nth-child(odd):hover td {
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
.fixed-table-container tbody .selected td {
|
||||
background: lightBlue;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @author: Dennis Hernández
|
||||
* @webSite: http://djhvscf.github.io/Blog
|
||||
* @version: v1.0.0
|
||||
*/
|
||||
|
||||
!function ($) {
|
||||
|
||||
'use strict';
|
||||
|
||||
document.onselectstart = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
var getTableObjectFromCurrentTarget = function (currentTarget) {
|
||||
currentTarget = $(currentTarget);
|
||||
return currentTarget.is("table") ? currentTarget : currentTarget.parents().find(".table");
|
||||
};
|
||||
|
||||
var getRow = function (target) {
|
||||
target = $(target);
|
||||
return target.parent().parent();
|
||||
};
|
||||
|
||||
var onRowClick = function (e) {
|
||||
var that = getTableObjectFromCurrentTarget(e.currentTarget);
|
||||
|
||||
if (window.event.ctrlKey) {
|
||||
toggleRow(e.currentTarget, that, false, false);
|
||||
}
|
||||
|
||||
if (window.event.button === 0) {
|
||||
if (!window.event.ctrlKey && !window.event.shiftKey) {
|
||||
clearAll(that);
|
||||
toggleRow(e.currentTarget, that, false, false);
|
||||
}
|
||||
|
||||
if (window.event.shiftKey) {
|
||||
selectRowsBetweenIndexes([that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow.rowIndex, e.currentTarget.rowIndex], that)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var onCheckboxChange = function (e) {
|
||||
var that = getTableObjectFromCurrentTarget(e.currentTarget);
|
||||
clearAll(that);
|
||||
toggleRow(getRow(e.currentTarget), that, false, false);
|
||||
};
|
||||
|
||||
var toggleRow = function (row, that, clearAll, useShift) {
|
||||
if (clearAll) {
|
||||
row = $(row);
|
||||
that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow = undefined;
|
||||
row.removeClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass);
|
||||
that.bootstrapTable("uncheck", row.data("index"));
|
||||
} else {
|
||||
that.bootstrapTable("getOptions").multipleSelectRowLastSelectedRow = row;
|
||||
row = $(row);
|
||||
if (useShift) {
|
||||
row.addClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass);
|
||||
that.bootstrapTable("check", row.data("index"));
|
||||
} else {
|
||||
if(row.hasClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass)) {
|
||||
row.removeClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass)
|
||||
that.bootstrapTable("uncheck", row.data("index"));
|
||||
} else {
|
||||
row.addClass(that.bootstrapTable("getOptions").multipleSelectRowCssClass);
|
||||
that.bootstrapTable("check", row.data("index"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var selectRowsBetweenIndexes = function (indexes, that) {
|
||||
indexes.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
for (var i = indexes[0]; i <= indexes[1]; i++) {
|
||||
toggleRow(that.bootstrapTable("getOptions").multipleSelectRowRows[i-1], that, false, true);
|
||||
}
|
||||
};
|
||||
|
||||
var clearAll = function (that) {
|
||||
for (var i = 0; i < that.bootstrapTable("getOptions").multipleSelectRowRows.length; i++) {
|
||||
toggleRow(that.bootstrapTable("getOptions").multipleSelectRowRows[i], that, true, false);
|
||||
}
|
||||
};
|
||||
|
||||
$.extend($.fn.bootstrapTable.defaults, {
|
||||
multipleSelectRow: false,
|
||||
multipleSelectRowCssClass: 'multiple-select-row-selected',
|
||||
//internal variables used by the extension
|
||||
multipleSelectRowLastSelectedRow: undefined,
|
||||
multipleSelectRowRows: []
|
||||
});
|
||||
|
||||
var BootstrapTable = $.fn.bootstrapTable.Constructor,
|
||||
_init = BootstrapTable.prototype.init,
|
||||
_initBody = BootstrapTable.prototype.initBody;
|
||||
|
||||
BootstrapTable.prototype.init = function () {
|
||||
if (this.options.multipleSelectRow) {
|
||||
var that = this;
|
||||
|
||||
//Make sure that the internal variables have the correct value
|
||||
this.options.multipleSelectRowLastSelectedRow = undefined;
|
||||
this.options.multipleSelectRowRows = [];
|
||||
|
||||
this.$el.on("post-body.bs.table", function (e) {
|
||||
setTimeout(function () {
|
||||
that.options.multipleSelectRowRows = that.$body.children();
|
||||
that.options.multipleSelectRowRows.click(onRowClick);
|
||||
that.options.multipleSelectRowRows.find("input[type=checkbox]").change(onCheckboxChange);
|
||||
}, 1);
|
||||
});
|
||||
}
|
||||
|
||||
_init.apply(this, Array.prototype.slice.apply(arguments));
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.clearAllMultipleSelectionRow = function () {
|
||||
clearAll(this);
|
||||
};
|
||||
|
||||
$.fn.bootstrapTable.methods.push('clearAllMultipleSelectionRow');
|
||||
}(jQuery);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Multiple Selection Row",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to enable the multiple selection row. You can use the ctrl+click to select one row or use ctrl+shift+click to select a range of rows.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-selection-row",
|
||||
"example": "",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-multiple-selection-row",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-selection-row"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Multiple Sort",
|
||||
"version": "1.1.0",
|
||||
"description": "Plugin to support the multiple sort.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort",
|
||||
"example": "#",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-multiple-sort",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/multiple-sort"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "dimbslmh",
|
||||
"image": "https://avatars1.githubusercontent.com/u/745635"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @author: Brian Huisman
|
||||
* @webSite: http://www.greywyvern.com
|
||||
* @version: v1.0.0
|
||||
* JS functions to allow natural sorting on bootstrap-table columns
|
||||
* add data-sorter="alphanum" or data-sorter="numericOnly" to any th
|
||||
*
|
||||
* @update Dennis Hernández <http://djhvscf.github.io/Blog>
|
||||
* @update Duane May
|
||||
*/
|
||||
|
||||
function alphanum(a, b) {
|
||||
function chunkify(t) {
|
||||
var tz = [],
|
||||
x = 0,
|
||||
y = -1,
|
||||
n = 0,
|
||||
i,
|
||||
j;
|
||||
|
||||
while (i = (j = t.charAt(x++)).charCodeAt(0)) {
|
||||
var m = (i === 46 || (i >= 48 && i <= 57));
|
||||
if (m !== n) {
|
||||
tz[++y] = "";
|
||||
n = m;
|
||||
}
|
||||
tz[y] += j;
|
||||
}
|
||||
return tz;
|
||||
}
|
||||
|
||||
function stringfy(v) {
|
||||
if (typeof(v) === "number") {
|
||||
v = "" + v;
|
||||
}
|
||||
if (!v) {
|
||||
v = "";
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
var aa = chunkify(stringfy(a));
|
||||
var bb = chunkify(stringfy(b));
|
||||
|
||||
for (x = 0; aa[x] && bb[x]; x++) {
|
||||
if (aa[x] !== bb[x]) {
|
||||
var c = Number(aa[x]),
|
||||
d = Number(bb[x]);
|
||||
|
||||
if (c == aa[x] && d == bb[x]) {
|
||||
return c - d;
|
||||
} else {
|
||||
return (aa[x] > bb[x]) ? 1 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return aa.length - bb.length;
|
||||
}
|
||||
|
||||
function numericOnly(a, b) {
|
||||
function stripNonNumber(s) {
|
||||
s = s.replace(new RegExp(/[^0-9]/g), "");
|
||||
return parseInt(s, 10);
|
||||
}
|
||||
|
||||
return stripNonNumber(a) - stripNonNumber(b);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Natural Sorting",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to support the natural sorting.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/natural-sorting",
|
||||
"example": "#",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-natural-sorting",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/natural-sorting"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "GreyWyvern",
|
||||
"image": "https://avatars1.githubusercontent.com/u/137631"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2019 doug-the-guy <badlydrawnsun@yahoo.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Bootstrap Table Pipelining
|
||||
|
||||
Use Plugin: [bootstrap-table-pipeline]
|
||||
|
||||
This plugin enables client side data caching for server side requests which will
|
||||
eliminate the need to issue a new request every page change. This will allow
|
||||
for a performance balance for a large data set between returning all data at once
|
||||
(client side paging) and a new server side request (server side paging).
|
||||
|
||||
There are two new options:
|
||||
- usePipeline: enables this feature
|
||||
- pipelineSize: the size of each cache window
|
||||
|
||||
The size of the pipeline must be evenly divisible by the current page size. This is
|
||||
assured by rounding up to the nearest evenly divisible value. For example, if
|
||||
the pipeline size is 4990 and the current page size is 25, then pipeline size will
|
||||
be dynamically set to 5000.
|
||||
|
||||
The cache windows are computed based on the pipeline size and the total number of rows
|
||||
returned by the server side query. For example, with pipeline size 500 and total rows
|
||||
1300, the cache windows will be:
|
||||
|
||||
[{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}]
|
||||
|
||||
Using the limit (i.e. the pipelineSize) and offset parameters, the server side request
|
||||
**MUST** return only the data in the requested cache window **AND** the total number of rows.
|
||||
To wit, the server side code must use the offset and limit parameters to prepare the response
|
||||
data.
|
||||
|
||||
On a page change, the new offset is checked if it is within the current cache window. If so,
|
||||
the requested page data is returned from the cached data set. Otherwise, a new server side
|
||||
request will be issued for the new cache window.
|
||||
|
||||
The current cached data is only invalidated on these events:
|
||||
- sorting
|
||||
- searching
|
||||
- page size change
|
||||
- page change moves into a new cache window
|
||||
|
||||
There are two new events:
|
||||
- cached-data-hit.bs.table: issued when cached data is used on a page change
|
||||
- cached-data-reset.bs.table: issued when the cached data is invalidated and new server side request is issued
|
||||
|
||||
## Features
|
||||
|
||||
* Created with Bootstrap 4
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
# assumed import of bootstrap and bootstrap-table assets
|
||||
<script src="/path/to/bootstrap-table-pipeline.js"></script>
|
||||
...
|
||||
<table id="pipeline_table"
|
||||
class="table table-striped"
|
||||
data-method='post'
|
||||
data-use-pipeline="true"
|
||||
data-pipeline-size="5000"
|
||||
data-pagination="true"
|
||||
data-side-pagination="server"
|
||||
data-page-size="50">
|
||||
<thead><tr>
|
||||
<th data-field="type" data-sortable="true">Type</th>
|
||||
<th data-field="value" data-sortable="true">Value</th>
|
||||
<th data-field="date" data-sortable="true">Date</th>
|
||||
</tr></thead>
|
||||
</table>
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### usePipeline
|
||||
|
||||
* type: Boolean
|
||||
* description: Set true to enable pipelining
|
||||
* default: `false`
|
||||
|
||||
## pipelineSize
|
||||
|
||||
* type: Integer
|
||||
* description: Size of each cache window. Must be greater than 0
|
||||
* default: `1000`
|
||||
|
||||
## Events
|
||||
|
||||
### onCachedDataHit(cached-data-hit.bs.table)
|
||||
|
||||
* Fires when paging was able to use the locally cached data.
|
||||
|
||||
### onCachedDataReset(cached-data-reset.bs.table)
|
||||
|
||||
* Fires when the locally cached data needed to be reset (i.e. on sorting, searching, page size change or paged out of current cache window)
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Pipeline",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to support a hybrid approach to server/client side paging.",
|
||||
"url": "",
|
||||
"example": "#",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-pipeline",
|
||||
"url": ""
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "doug-the-guy",
|
||||
"image": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Reorder Columns",
|
||||
"version": "1.1.0",
|
||||
"description": "Plugin to support the reordering columns feature.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/reorder-columns.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-reorder-columns",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Reorder Rows",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to support the reordering rows feature.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-rows",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/reorder-rows.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-reorder-rows",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-rows"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Resizable",
|
||||
"version": "1.1.0",
|
||||
"description": "Plugin to support the resizable feature.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/resizable.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-resizable",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* @author: Jewway
|
||||
* @version: v1.1.1
|
||||
*/
|
||||
|
||||
! function ($) {
|
||||
'use strict';
|
||||
|
||||
function getCurrentHeader(that) {
|
||||
var header = that.$header;
|
||||
if (that.options.height) {
|
||||
header = that.$tableHeader;
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
function initFilterValues(that) {
|
||||
if (!$.isEmptyObject(that.filterColumnsPartial)) {
|
||||
var $header = getCurrentHeader(that);
|
||||
|
||||
$.each(that.columns, function (idx, column) {
|
||||
var value = that.filterColumnsPartial[column.field];
|
||||
|
||||
if (column.filter) {
|
||||
if (column.filter.setFilterValue) {
|
||||
var $filter = $header.find('[data-field=' + column.field + '] .filter');
|
||||
column.filter.setFilterValue($filter, column.field, value);
|
||||
} else {
|
||||
var $ele = $header.find('[data-filter-field=' + column.field + ']');
|
||||
switch (column.filter.type) {
|
||||
case 'input':
|
||||
$ele.val(value);
|
||||
case 'select':
|
||||
$ele.val(value).trigger('change');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createFilter(that, header) {
|
||||
var enableFilter = false,
|
||||
isVisible,
|
||||
html,
|
||||
timeoutId = 0;
|
||||
|
||||
$.each(that.columns, function (i, column) {
|
||||
isVisible = 'hidden';
|
||||
html = null;
|
||||
|
||||
if (!column.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!column.filter) {
|
||||
html = $('<div class="no-filter"></div>');
|
||||
} else {
|
||||
var filterClass = column.filter.class ? ' ' + column.filter.class : '';
|
||||
html = $('<div style="margin: 0px 2px 2px 2px;" class="filter' + filterClass + '">');
|
||||
|
||||
if (column.searchable) {
|
||||
enableFilter = true;
|
||||
isVisible = 'visible'
|
||||
}
|
||||
|
||||
if (column.filter.template) {
|
||||
html.append(column.filter.template(that, column, isVisible));
|
||||
} else {
|
||||
var $filter = $(that.options.filterTemplate[column.filter.type.toLowerCase()](that, column, isVisible));
|
||||
|
||||
switch (column.filter.type) {
|
||||
case 'input':
|
||||
var cpLock = true;
|
||||
$filter.off('compositionstart').on('compositionstart', function (event) {
|
||||
cpLock = false;
|
||||
});
|
||||
|
||||
$filter.off('compositionend').on('compositionend', function (event) {
|
||||
cpLock = true;
|
||||
var $input = $(this);
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(function () {
|
||||
that.onColumnSearch(event, column.field, $input.val());
|
||||
}, that.options.searchTimeOut);
|
||||
});
|
||||
|
||||
$filter.off('keyup').on('keyup', function (event) {
|
||||
if (cpLock) {
|
||||
var $input = $(this);
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(function () {
|
||||
that.onColumnSearch(event, column.field, $input.val());
|
||||
}, that.options.searchTimeOut);
|
||||
}
|
||||
});
|
||||
|
||||
$filter.off('mouseup').on('mouseup', function (event) {
|
||||
var $input = $(this),
|
||||
oldValue = $input.val();
|
||||
|
||||
if (oldValue === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
var newValue = $input.val();
|
||||
|
||||
if (newValue === "") {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(function () {
|
||||
that.onColumnSearch(event, column.field, newValue);
|
||||
}, that.options.searchTimeOut);
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
break;
|
||||
case 'select':
|
||||
$filter.on('select2:select', function (event) {
|
||||
that.onColumnSearch(event, column.field, $(this).val());
|
||||
});
|
||||
|
||||
$filter.on("select2:unselecting", function (event) {
|
||||
var $select2 = $(this);
|
||||
event.preventDefault();
|
||||
$select2.val(null).trigger('change');
|
||||
that.searchText = undefined;
|
||||
that.onColumnSearch(event, column.field, $select2.val());
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
html.append($filter);
|
||||
}
|
||||
}
|
||||
|
||||
$.each(header.children().children(), function (i, tr) {
|
||||
tr = $(tr);
|
||||
if (tr.data('field') === column.field) {
|
||||
tr.find('.fht-cell').append(html);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!enableFilter) {
|
||||
header.find('.filter').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function initSelect2(that) {
|
||||
var $header = getCurrentHeader(that);
|
||||
|
||||
$.each(that.columns, function (idx, column) {
|
||||
if (column.filter && column.filter.type === 'select') {
|
||||
var $selectEle = $header.find('select[data-filter-field="' + column.field + '"]');
|
||||
|
||||
if ($selectEle.length > 0 && !$selectEle.data().select2) {
|
||||
var select2Opts = {
|
||||
placeholder: "",
|
||||
allowClear: true,
|
||||
data: column.filter.data,
|
||||
dropdownParent: that.$el.closest(".bootstrap-table")
|
||||
};
|
||||
|
||||
$selectEle.select2(select2Opts);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.extend($.fn.bootstrapTable.defaults, {
|
||||
filter: false,
|
||||
filterValues: {},
|
||||
filterTemplate: {
|
||||
input: function (instance, column, isVisible) {
|
||||
return '<input type="text" class="form-control" data-filter-field="' + column.field + '" style="width: 100%; visibility:' + isVisible + '">';
|
||||
},
|
||||
select: function (instance, column, isVisible) {
|
||||
return '<select data-filter-field="' + column.field + '" style="width: 100%; visibility:' + isVisible + '"></select>';
|
||||
}
|
||||
},
|
||||
onColumnSearch: function (field, text) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$.extend($.fn.bootstrapTable.COLUMN_DEFAULTS, {
|
||||
filter: undefined
|
||||
});
|
||||
|
||||
$.extend($.fn.bootstrapTable.Constructor.EVENTS, {
|
||||
'column-search.bs.table': 'onColumnSearch'
|
||||
});
|
||||
|
||||
var BootstrapTable = $.fn.bootstrapTable.Constructor,
|
||||
_init = BootstrapTable.prototype.init,
|
||||
_initHeader = BootstrapTable.prototype.initHeader,
|
||||
_initSearch = BootstrapTable.prototype.initSearch;
|
||||
|
||||
BootstrapTable.prototype.init = function () {
|
||||
//Make sure that the filtercontrol option is set
|
||||
if (this.options.filter) {
|
||||
var that = this;
|
||||
|
||||
if (that.options.filterTemplate) {
|
||||
that.options.filterTemplate = $.extend({}, $.fn.bootstrapTable.defaults.filterTemplate, that.options.filterTemplate);
|
||||
}
|
||||
|
||||
if (!$.isEmptyObject(that.options.filterValues)) {
|
||||
that.filterColumnsPartial = that.options.filterValues;
|
||||
that.options.filterValues = {};
|
||||
}
|
||||
|
||||
this.$el.on('reset-view.bs.table', function () {
|
||||
//Create controls on $tableHeader if the height is set
|
||||
if (!that.options.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Avoid recreate the controls
|
||||
if (that.$tableHeader.find('select').length > 0 || that.$tableHeader.find('input').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
createFilter(that, that.$tableHeader);
|
||||
}).on('post-header.bs.table', function () {
|
||||
var timeoutId = 0;
|
||||
|
||||
initSelect2(that);
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(function () {
|
||||
initFilterValues(that);
|
||||
}, that.options.searchTimeOut - 1000);
|
||||
}).on('column-switch.bs.table', function (field, checked) {
|
||||
initFilterValues(that);
|
||||
});
|
||||
}
|
||||
|
||||
_init.apply(this, Array.prototype.slice.apply(arguments));
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.initHeader = function () {
|
||||
_initHeader.apply(this, Array.prototype.slice.apply(arguments));
|
||||
if (this.options.filter) {
|
||||
createFilter(this, this.$header);
|
||||
}
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.initSearch = function () {
|
||||
var that = this,
|
||||
filterValues = that.filterColumnsPartial;
|
||||
|
||||
// Filter for client
|
||||
if (that.options.sidePagination === 'client') {
|
||||
this.data = $.grep(this.data, function (row, idx) {
|
||||
for (var field in filterValues) {
|
||||
var column = that.columns[that.fieldsColumnsIndex[field]],
|
||||
filterValue = filterValues[field].toLowerCase(),
|
||||
rowValue = row[field];
|
||||
|
||||
rowValue = $.fn.bootstrapTable.utils.calculateObjectValue(
|
||||
that.header,
|
||||
that.header.formatters[$.inArray(field, that.header.fields)], [rowValue, row, idx], rowValue);
|
||||
|
||||
if (column.filterStrictSearch) {
|
||||
if (!($.inArray(field, that.header.fields) !== -1 &&
|
||||
(typeof rowValue === 'string' || typeof rowValue === 'number') &&
|
||||
rowValue.toString().toLowerCase() === filterValue.toString().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!($.inArray(field, that.header.fields) !== -1 &&
|
||||
(typeof rowValue === 'string' || typeof rowValue === 'number') &&
|
||||
(rowValue + '').toLowerCase().indexOf(filterValue) !== -1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
_initSearch.apply(this, Array.prototype.slice.apply(arguments));
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.onColumnSearch = function (event, field, value) {
|
||||
if ($.isEmptyObject(this.filterColumnsPartial)) {
|
||||
this.filterColumnsPartial = {};
|
||||
}
|
||||
|
||||
if (value) {
|
||||
this.filterColumnsPartial[field] = value;
|
||||
} else {
|
||||
delete this.filterColumnsPartial[field];
|
||||
}
|
||||
|
||||
this.options.pageNumber = 1;
|
||||
this.onSearch(event);
|
||||
this.trigger('column-search', field, value);
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.setSelect2Data = function (field, data) {
|
||||
var that = this,
|
||||
$header = getCurrentHeader(that),
|
||||
$selectEle = $header.find('select[data-filter-field=\"' + field + '\"]');
|
||||
$selectEle.empty();
|
||||
$selectEle.select2({
|
||||
data: data,
|
||||
placeholder: "",
|
||||
allowClear: true,
|
||||
dropdownParent: that.$el.closest(".bootstrap-table")
|
||||
});
|
||||
|
||||
$.each(this.columns, function (idx, column) {
|
||||
if (column.field === field) {
|
||||
column.filter.data = data;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
BootstrapTable.prototype.setFilterValues = function (values) {
|
||||
this.filterColumnsPartial = values;
|
||||
};
|
||||
|
||||
$.fn.bootstrapTable.methods.push('setSelect2Data');
|
||||
$.fn.bootstrapTable.methods.push('setFilterValues');
|
||||
|
||||
}(jQuery);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Select2 Filter",
|
||||
"version": "1.1.0",
|
||||
"description": "Plugin to add select2 filter on the top of the columns in order to filter the data.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/select2-filter",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/select2-filter.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-select2-filter",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/select2-filter"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "Jewway",
|
||||
"image": "https://avatars0.githubusercontent.com/u/3501899"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Sticky Header",
|
||||
"version": "1.0.0",
|
||||
"description": "An extension which provides a sticky header for table columns when scrolling on a long page and / or table. Works for tables with many columns and narrow width with horizontal scrollbars too.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/sticky-header.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-sticky-header",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "vinzloh",
|
||||
"image": "https://avatars0.githubusercontent.com/u/5501845"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Toolbar",
|
||||
"version": "2.0.0",
|
||||
"description": "Plugin to support the advanced search.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/toolbar.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-toolbar",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "djhvscf",
|
||||
"image": "https://avatars1.githubusercontent.com/u/4496763"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.table:not(.table-condensed)>tbody>tr>td.treenode{padding-top:0;padding-bottom:0;border-bottom:solid #fff 1px}.table:not(.table-condensed)>tbody>tr:last-child>td.treenode{border-bottom:none}.treenode .text{float:left;display:block;padding-top:6px;padding-bottom:6px}.treenode .vertical,.treenode .vertical.last{float:left;display:block;width:1px;border-left:dashed silver 1px;height:38px;margin-left:8px}.treenode .vertical.last{height:15px}.treenode .space,.treenode .node{float:left;display:block;width:15px;height:5px;margin-top:15px}.treenode .node{border-top:dashed silver 1px}
|
||||
130
InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js
vendored
Normal file
130
InvenTree/InvenTree/static/bootstrap-table/extensions/tree-column/bootstrap-table-tree-column.js
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @author: KingYang
|
||||
* @webSite: https://github.com/kingyang
|
||||
* @version: v1.0.0
|
||||
*/
|
||||
|
||||
! function ($) {
|
||||
|
||||
'use strict';
|
||||
|
||||
$.extend($.fn.bootstrapTable.defaults, {
|
||||
treeShowField: null,
|
||||
idField: 'id',
|
||||
parentIdField: 'pid',
|
||||
treeVerticalcls: 'vertical',
|
||||
treeVerticalLastcls: 'vertical last',
|
||||
treeSpacecls: 'space',
|
||||
treeNodecls: 'node',
|
||||
treeCellcls: 'treenode',
|
||||
treeTextcls: 'text',
|
||||
onTreeFormatter: function (row) {
|
||||
var that = this,
|
||||
options = that.options,
|
||||
level = row._level || 0,
|
||||
plevel = row._parent && row._parent._level || 0,
|
||||
paddings = [];
|
||||
for (var i = 0; i < plevel; i++) {
|
||||
paddings.push('<i class="' + options.treeVerticalcls + '"></i>');
|
||||
paddings.push('<i class="' + options.treeSpacecls + '"></i>');
|
||||
}
|
||||
|
||||
for (var i = plevel; i < level; i++) {
|
||||
if (row._last && i === (level - 1)) {
|
||||
paddings.push('<i class="' + options.treeVerticalLastcls + '"></i>');
|
||||
} else {
|
||||
paddings.push('<i class="' + options.treeVerticalcls + '"></i>');
|
||||
}
|
||||
paddings.push('<i class="' + options.treeNodecls + '"></i>');
|
||||
}
|
||||
return paddings.join('');
|
||||
}, onGetNodes: function (row, data) {
|
||||
var that = this;
|
||||
var nodes = [];
|
||||
$.each(data, function (i, item) {
|
||||
if (row[that.options.idField] === item[that.options.parentIdField]) {
|
||||
nodes.push(item);
|
||||
}
|
||||
});
|
||||
return nodes;
|
||||
},
|
||||
onCheckLeaf: function (row, data) {
|
||||
if (row.isLeaf !== undefined) {
|
||||
return row.isLeaf;
|
||||
}
|
||||
return !row._nodes || !row._nodes.length;
|
||||
}, onCheckRoot: function (row, data) {
|
||||
var that = this;
|
||||
return !row[that.options.parentIdField];
|
||||
}
|
||||
});
|
||||
|
||||
var BootstrapTable = $.fn.bootstrapTable.Constructor,
|
||||
_initRow = BootstrapTable.prototype.initRow,
|
||||
_initHeader = BootstrapTable.prototype.initHeader;
|
||||
|
||||
BootstrapTable.prototype.initHeader = function () {
|
||||
var that = this;
|
||||
_initHeader.apply(that, Array.prototype.slice.apply(arguments));
|
||||
var treeShowField = that.options.treeShowField;
|
||||
if (treeShowField) {
|
||||
$.each(this.header.fields, function (i, field) {
|
||||
if (treeShowField === field) {
|
||||
that.treeEnable = true;
|
||||
var _formatter = that.header.formatters[i];
|
||||
var _class = [that.options.treeCellcls];
|
||||
if (that.header.classes[i]) {
|
||||
_class.push(that.header.classes[i].split('"')[1] || '');
|
||||
}
|
||||
that.header.classes[i] = ' class="' + _class.join(' ') + '"';
|
||||
that.header.formatters[i] = function (value, row, index) {
|
||||
var colTree = [that.options.onTreeFormatter.apply(that, [row])];
|
||||
colTree.push('<span class="' + that.options.treeTextcls + '">');
|
||||
if (_formatter) {
|
||||
colTree.push(_formatter.apply(this, Array.prototype.slice.apply(arguments)));
|
||||
} else {
|
||||
colTree.push(value);
|
||||
}
|
||||
colTree.push('</span>');
|
||||
return colTree.join('');
|
||||
};
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var initNode = function (item, idx, data, parentDom) {
|
||||
var that = this;
|
||||
var nodes = that.options.onGetNodes.apply(that, [item, data]);
|
||||
item._nodes = nodes;
|
||||
parentDom.append(_initRow.apply(that, [item, idx, data, parentDom]));
|
||||
var len = nodes.length - 1;
|
||||
for (var i = 0; i <= len; i++) {
|
||||
var node = nodes[i];
|
||||
node._level = item._level + 1;
|
||||
node._parent = item;
|
||||
if (i === len)
|
||||
node._last = 1;
|
||||
initNode.apply(that, [node, $.inArray(node, data), data, parentDom]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
BootstrapTable.prototype.initRow = function (item, idx, data, parentDom) {
|
||||
var that = this;
|
||||
if (that.treeEnable) {
|
||||
if (that.options.onCheckRoot.apply(that, [item, data])) {
|
||||
if (item._level === undefined) {
|
||||
item._level = 0;
|
||||
}
|
||||
initNode.apply(that, [item, idx, data, parentDom]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
return _initRow.apply(that, Array.prototype.slice.apply(arguments));
|
||||
};
|
||||
|
||||
} (jQuery);
|
||||
@@ -0,0 +1,43 @@
|
||||
.table:not(.table-condensed) > tbody > tr > td.treenode {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: solid #fff 1px;
|
||||
}
|
||||
|
||||
.table:not(.table-condensed) > tbody > tr:last-child > td.treenode {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.treenode {
|
||||
.text {
|
||||
float: left;
|
||||
display: block;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.vertical,
|
||||
.vertical.last {
|
||||
float: left;
|
||||
display: block;
|
||||
width: 1px;
|
||||
border-left: dashed silver 1px;
|
||||
height: 38px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.vertical.last {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.space,
|
||||
.node {
|
||||
float: left;
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.node {
|
||||
border-top: dashed silver 1px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Tree column",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to support display tree data column.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/tree-column",
|
||||
"example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/tree-column.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-reorder-rows",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/tree-column"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "KingYang",
|
||||
"image": "https://avatars3.githubusercontent.com/u/1540211"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 346 B |
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "treegrid",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin to support the jquery treegrid.",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/treegrid",
|
||||
"example": "https://github.com/wenzhixin/bootstrap-table-examples/blob/master/extensions/treegrid.html",
|
||||
|
||||
"plugins": [{
|
||||
"name": "bootstrap-table-treegrid",
|
||||
"url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/treegrid"
|
||||
}],
|
||||
|
||||
"author": {
|
||||
"name": "foreveryang321",
|
||||
"image": "https://avatars0.githubusercontent.com/u/5868190"
|
||||
}
|
||||
}
|
||||
@@ -1,790 +0,0 @@
|
||||
/**
|
||||
* @author Dustin Utecht
|
||||
* https://github.com/wenzhixin/bootstrap-table/
|
||||
*/
|
||||
.bootstrap-table .fixed-table-toolbar::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .bs-bars,
|
||||
.bootstrap-table .fixed-table-toolbar .search,
|
||||
.bootstrap-table .fixed-table-toolbar .columns {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group {
|
||||
display: inline-block;
|
||||
margin-left: -1px !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group > .btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group:first-child > .btn {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group > .btn-group:last-child > .btn {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .dropdown-menu {
|
||||
text-align: left;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: scrollbar;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns label {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: normal;
|
||||
line-height: 1.428571429;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns-left {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns-right {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .pull-right .dropdown-menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container {
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table {
|
||||
width: 100%;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table th,
|
||||
.bootstrap-table .fixed-table-container .table td {
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th {
|
||||
vertical-align: bottom;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th:focus {
|
||||
outline: 0 solid transparent;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th.detail {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .th-inner {
|
||||
padding: 0.75rem;
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .sortable {
|
||||
cursor: pointer;
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 30px !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .both {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC");
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .asc {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table thead th .desc {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ");
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr.selected td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr.no-records-found td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr .card-view {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-title {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
min-width: 30%;
|
||||
width: auto !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table tbody tr .card-view .card-view-value {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type="radio"],
|
||||
.bootstrap-table .fixed-table-container .table .bs-checkbox label input[type="checkbox"] {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .table.table-sm .th-inner {
|
||||
padding: 0.3rem;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height:not(.has-footer) {
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height.has-card-view {
|
||||
border-top: 1px solid #dbdbdb;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height .fixed-table-border {
|
||||
border-left: 1px solid #dbdbdb;
|
||||
border-right: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height .table thead th {
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container.fixed-height .table-dark thead th {
|
||||
border-bottom: 1px solid #32383e;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-header {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
transition: visibility 0s, opacity 0.15s ease-in-out;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .loading-text {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::before {
|
||||
content: "";
|
||||
animation-duration: 1.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: LOADING;
|
||||
background: #363636;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: 5px;
|
||||
margin: 0 4px;
|
||||
opacity: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-dot {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading .loading-wrap .animation-wrap::after {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark {
|
||||
background: #363636;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-dot,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::after,
|
||||
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading.table-dark .animation-wrap::before {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-container .fixed-table-footer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail,
|
||||
.bootstrap-table .fixed-table-pagination > .pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .pagination-info {
|
||||
line-height: 34px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list .btn-group {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination-detail .page-list .btn-group .dropdown-menu {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a::before {
|
||||
content: '\2B05';
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.page-intermediate a::after {
|
||||
content: '\27A1';
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-pagination > .pagination ul.pagination li.disabled a {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bootstrap-table.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1050;
|
||||
width: 100% !important;
|
||||
background: #fff;
|
||||
height: calc(100vh);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.bootstrap-table.bootstrap4 .pagination-lg .page-link, .bootstrap-table.bootstrap5 .pagination-lg .page-link {
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
.bootstrap-table.bootstrap5 .float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.bootstrap-table.bootstrap5 .float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* calculate scrollbar width */
|
||||
div.fixed-table-scroll-inner {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
div.fixed-table-scroll-outer {
|
||||
top: 0;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes LOADING {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'bootstrap-table';
|
||||
src: url("fonts/bootstrap-table.eot?gmdfsp");
|
||||
src: url("fonts/bootstrap-table.eot") format("embedded-opentype"), url("fonts/bootstrap-table.ttf") format("truetype"), url("fonts/bootstrap-table.woff") format("woff"), url("fonts/bootstrap-table.svg") format("svg");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
[class^="icon-"],
|
||||
[class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'bootstrap-table', sans-serif !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-arrow-down-circle:before {
|
||||
content: "\e907";
|
||||
}
|
||||
|
||||
.icon-arrow-up-circle:before {
|
||||
content: "\e908";
|
||||
}
|
||||
|
||||
.icon-chevron-left:before {
|
||||
content: "\e900";
|
||||
}
|
||||
|
||||
.icon-chevron-right:before {
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
.icon-clock:before {
|
||||
content: "\e90c";
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: "\e909";
|
||||
}
|
||||
|
||||
.icon-download:before {
|
||||
content: "\e90d";
|
||||
}
|
||||
|
||||
.icon-list:before {
|
||||
content: "\e902";
|
||||
}
|
||||
|
||||
.icon-maximize:before {
|
||||
content: "\1f5ce";
|
||||
}
|
||||
|
||||
.icon-minus:before {
|
||||
content: "\e90f";
|
||||
}
|
||||
|
||||
.icon-move:before {
|
||||
content: "\e903";
|
||||
}
|
||||
|
||||
.icon-plus:before {
|
||||
content: "\e90e";
|
||||
}
|
||||
|
||||
.icon-printer:before {
|
||||
content: "\e90b";
|
||||
}
|
||||
|
||||
.icon-refresh-cw:before {
|
||||
content: "\e904";
|
||||
}
|
||||
|
||||
.icon-search:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
|
||||
.icon-toggle-right:before {
|
||||
content: "\e905";
|
||||
}
|
||||
|
||||
.icon-trash-2:before {
|
||||
content: "\e906";
|
||||
}
|
||||
|
||||
.icon-sort-amount-asc:before {
|
||||
content: "\ea4c";
|
||||
}
|
||||
|
||||
.bootstrap-table * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bootstrap-table input.form-control,
|
||||
.bootstrap-table select.form-control,
|
||||
.bootstrap-table .btn {
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.bootstrap-table select.form-control {
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.bootstrap-table .btn {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bootstrap-table .btn.active {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.bootstrap-table .btn:focus, .bootstrap-table .btn:hover {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
.bootstrap-table .caret {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
border-top: 4px dashed;
|
||||
border-top: 4px solid;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.bootstrap-table .detail-icon {
|
||||
text-decoration: none;
|
||||
color: #3679e4;
|
||||
}
|
||||
|
||||
.bootstrap-table .detail-icon:hover {
|
||||
color: #154a9f;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns,
|
||||
.bootstrap-table .fixed-table-toolbar .columns .btn-group {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child):not(:last-child),
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child):not(:last-child) > .btn,
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child):not(:last-child),
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child):not(:last-child) > .btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:last-child) > .btn,
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:last-child):not(.dropdown-toggle),
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:last-child) > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child):not(.dropdown-toggle),
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn:not(:first-child) > .btn,
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child):not(.dropdown-toggle),
|
||||
.bootstrap-table .fixed-table-toolbar .columns > .btn-group:not(:first-child) > .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns label {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .columns .dropdown-divider {
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .search .input-group .search-input {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .search .input-group button[name="search"],
|
||||
.bootstrap-table .fixed-table-toolbar .search .input-group button[name="clearSearch"] {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .fixed-table-toolbar .search .input-group button[name="search"]:not(:last-child),
|
||||
.bootstrap-table .fixed-table-toolbar .search .input-group button[name="clearSearch"]:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.bootstrap-table .open.dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bootstrap-table .dropdown-menu-up .dropdown-menu {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.bootstrap-table .dropdown-menu {
|
||||
display: none;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 120px;
|
||||
margin-top: 2px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 3px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.175);
|
||||
}
|
||||
|
||||
.bootstrap-table .dropdown-menu .dropdown-item {
|
||||
color: #363636;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 5px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bootstrap-table .dropdown-menu .dropdown-item:hover {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
.bootstrap-table .dropdown-menu .dropdown-item.active {
|
||||
background-color: #3679e4;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bootstrap-table .dropdown-menu .dropdown-item.active:hover {
|
||||
background-color: #1b5fcc;
|
||||
}
|
||||
|
||||
.bootstrap-table .columns-left .dropdown-menu {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination-detail {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination-detail .dropdown-item {
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bootstrap-table table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.bootstrap-table table th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.bootstrap-table table.table-bordered thead tr th,
|
||||
.bootstrap-table table.table-bordered tbody tr td {
|
||||
border: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
.bootstrap-table table.table-bordered tbody tr td {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.bootstrap-table table.table-hover tbody tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.bootstrap-table .float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.bootstrap-table .float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination {
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .page-item {
|
||||
border: 1px solid #dbdbdb;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
margin: 2px;
|
||||
padding: 5px 2px 5px 2px;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .page-item:hover {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .page-item .page-link {
|
||||
padding: 6px 12px;
|
||||
line-height: 1.428571429;
|
||||
color: #363636;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .page-item.active {
|
||||
background-color: #3679e4;
|
||||
border: 1px solid #206ae1;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .page-item.active .page-link {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .page-item.active:hover {
|
||||
background-color: #1b5fcc;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .btn-group {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .btn-group .btn:not(:first-child):not(:last-child),
|
||||
.bootstrap-table .pagination .btn-group input:not(:first-child):not(:last-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .btn-group .btn:first-child:not(:last-child):not(.dropdown-toggle),
|
||||
.bootstrap-table .pagination .btn-group input:first-child:not(:last-child):not(.dropdown-toggle) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .btn-group .btn:last-child:not(:first-child),
|
||||
.bootstrap-table .pagination .btn-group input:last-child:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .pagination .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.bootstrap-table .filter-control {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bootstrap-table .page-jump-to input,
|
||||
.bootstrap-table .page-jump-to .btn {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
display: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal .btn {
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
padding: 6px 12px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal .btn.active {
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
.modal .modal-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 998;
|
||||
background-color: rgba(10, 10, 10, 0.86);
|
||||
}
|
||||
|
||||
.modal .modal-content {
|
||||
position: relative;
|
||||
width: 600px;
|
||||
margin: 30px auto;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal .modal-content .box {
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by IcoMoon</metadata>
|
||||
<defs>
|
||||
<font id="bootstrap-table" horiz-adv-x="1024">
|
||||
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" glyph-name="chevron-left" d="M670.165 200.832l-225.835 225.835 225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-256-256c-16.683-16.683-16.683-43.691 0-60.331l256-256c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331z" />
|
||||
<glyph unicode="" glyph-name="chevron-right" d="M414.165 140.502l256 256c16.683 16.683 16.683 43.691 0 60.331l-256 256c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331l225.835-225.835-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0z" />
|
||||
<glyph unicode="" glyph-name="list" d="M341.333 640h554.667c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-554.667c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM341.333 384h554.667c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-554.667c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM341.333 128h554.667c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-554.667c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM170.667 682.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667 19.115-42.667 42.667-42.667 42.667 19.115 42.667 42.667zM170.667 426.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667 19.115-42.667 42.667-42.667 42.667 19.115 42.667 42.667zM170.667 170.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667 19.115-42.667 42.667-42.667 42.667 19.115 42.667 42.667z" />
|
||||
<glyph unicode="" glyph-name="move" d="M469.333 750.336v-281.003h-281.003l55.168 55.168c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-128-128c-0.043-0.043-0.085-0.128-0.171-0.171-4.011-4.053-7.040-8.704-9.088-13.653-4.309-10.453-4.309-22.229 0-32.683 2.048-4.949 5.077-9.643 9.088-13.653 0.043-0.043 0.085-0.128 0.171-0.171l128-128c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-55.168 55.168h281.003v-281.003l-55.168 55.168c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331l128-128c4.096-4.096 8.832-7.168 13.867-9.259 5.12-2.091 10.539-3.2 15.957-3.243 5.675-0.043 11.349 1.024 16.64 3.243 5.035 2.091 9.771 5.163 13.867 9.259l128 128c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-55.168-55.168v281.003h281.003l-55.168-55.168c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l128 128c4.096 4.096 7.168 8.832 9.259 13.867 2.091 5.12 3.2 10.539 3.243 15.957 0.043 5.675-1.024 11.349-3.243 16.64-2.091 5.035-5.163 9.771-9.259 13.867l-128 128c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331l55.168-55.168h-281.003v281.003l55.168-55.168c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-128 128c-0.043 0.043-0.128 0.085-0.171 0.171-4.053 4.011-8.704 7.040-13.653 9.088-10.453 4.309-22.229 4.309-32.683 0-4.949-2.048-9.643-5.077-13.653-9.088-0.043-0.043-0.128-0.085-0.171-0.171l-128-128c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0z" />
|
||||
<glyph unicode="" glyph-name="refresh-cw" d="M189.995 540.416c15.445 43.648 38.827 82.133 67.883 114.432 30.208 33.579 66.645 60.587 106.88 79.744s84.096 30.549 129.237 32.939c43.392 2.304 88.021-3.755 131.669-19.2 50.603-17.92 94.123-46.421 127.275-80.213l120.704-113.451h-148.309c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h256c0.171 0 0.299 0 0.469 0 5.845 0.043 11.435 1.323 16.469 3.499 5.205 2.261 9.856 5.504 13.739 9.515 0.555 0.597 1.152 1.195 1.664 1.835 3.072 3.541 5.547 7.637 7.296 12.032 1.749 4.352 2.773 9.045 2.944 13.952 0.085 0.64 0.085 1.237 0.085 1.835v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-157.397l-124.843 117.291c-42.325 43.093-96.896 78.635-159.701 100.864-54.4 19.243-110.208 26.837-164.608 23.979-56.491-2.987-111.317-17.195-161.493-41.131s-95.701-57.643-133.547-99.669c-36.437-40.491-65.664-88.619-84.907-143.061-7.851-22.229 3.755-46.592 25.984-54.443s46.592 3.755 54.443 25.984zM85.333 242.688l126.080-118.485c39.851-39.893 86.955-70.784 137.259-91.648 52.224-21.632 107.861-32.469 163.456-32.469s111.232 10.795 163.456 32.384c50.347 20.821 97.451 51.669 138.283 92.501 47.104 47.104 81.109 102.699 100.736 159.787 7.68 22.272-4.181 46.549-26.496 54.229s-46.549-4.181-54.229-26.496c-15.403-44.8-42.368-89.216-80.341-127.189-32.768-32.725-70.4-57.387-110.592-73.984-41.728-17.28-86.272-25.941-130.816-25.899s-89.045 8.704-130.773 25.984c-40.149 16.64-77.781 41.301-111.488 74.965l-119.467 112.299h148.267c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-256c-0.171 0-0.299 0-0.469 0-5.845-0.043-11.435-1.323-16.469-3.499-5.205-2.261-9.899-5.547-13.781-9.6-0.555-0.555-1.067-1.152-1.579-1.749-3.072-3.584-5.589-7.68-7.339-12.117-1.707-4.352-2.731-9.003-2.944-13.909-0.085-0.597-0.085-1.195-0.085-1.792v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667z" />
|
||||
<glyph unicode="" glyph-name="toggle-right" d="M341.333 768c-46.165 0-90.283-9.173-130.56-25.856-41.813-17.323-79.36-42.667-110.805-74.112s-56.789-69.035-74.112-110.805c-16.683-40.277-25.856-84.395-25.856-130.56s9.173-90.283 25.856-130.56c17.323-41.813 42.667-79.36 74.112-110.805s69.035-56.789 110.805-74.112c40.277-16.683 84.395-25.856 130.56-25.856h341.333c46.165 0 90.283 9.173 130.56 25.856 41.813 17.323 79.36 42.667 110.805 74.112s56.789 69.035 74.112 110.805c16.683 40.277 25.856 84.395 25.856 130.56s-9.173 90.283-25.856 130.56c-17.323 41.813-42.667 79.36-74.112 110.805s-69.035 56.789-110.805 74.112c-40.277 16.683-84.395 25.856-130.56 25.856zM341.333 682.667h341.333c34.773 0 67.797-6.912 97.877-19.371 31.275-12.971 59.477-31.957 83.115-55.595s42.667-51.84 55.595-83.115c12.501-30.123 19.413-63.147 19.413-97.92s-6.912-67.797-19.371-97.877c-12.971-31.275-31.957-59.477-55.595-83.115s-51.84-42.667-83.115-55.595c-30.123-12.501-63.147-19.413-97.92-19.413h-341.333c-34.773 0-67.797 6.912-97.877 19.371-31.275 12.971-59.477 31.957-83.115 55.595s-42.667 51.84-55.595 83.115c-12.501 30.123-19.413 63.147-19.413 97.92s6.912 67.797 19.371 97.877c12.971 31.275 31.957 59.477 55.595 83.115s51.84 42.667 83.115 55.595c30.123 12.501 63.147 19.413 97.92 19.413zM853.333 426.667c0 23.040-4.608 45.099-12.928 65.28-8.661 20.907-21.333 39.68-37.035 55.381s-34.475 28.373-55.381 37.035c-20.224 8.363-42.283 12.971-65.323 12.971s-45.099-4.608-65.28-12.928c-20.907-8.661-39.68-21.333-55.381-37.035s-28.373-34.475-37.035-55.381c-8.363-20.224-12.971-42.283-12.971-65.323s4.608-45.099 12.928-65.28c8.661-20.907 21.333-39.68 37.035-55.381s34.475-28.373 55.381-37.035c20.224-8.363 42.283-12.971 65.323-12.971s45.099 4.608 65.28 12.928c20.907 8.661 39.68 21.333 55.381 37.035s28.373 34.475 37.035 55.381c8.363 20.224 12.971 42.283 12.971 65.323zM768 426.667c0-11.648-2.304-22.613-6.443-32.64-4.309-10.411-10.667-19.797-18.56-27.733-7.893-7.893-17.323-14.251-27.733-18.56-9.984-4.096-20.949-6.4-32.597-6.4s-22.613 2.304-32.64 6.443c-10.411 4.309-19.797 10.667-27.733 18.56s-14.251 17.323-18.56 27.733c-4.096 9.984-6.4 20.949-6.4 32.597s2.304 22.613 6.443 32.64c4.309 10.411 10.667 19.797 18.56 27.733s17.323 14.251 27.733 18.56c9.984 4.096 20.949 6.4 32.597 6.4s22.613-2.304 32.64-6.443c10.411-4.309 19.797-10.667 27.733-18.56 7.893-7.893 14.251-17.323 18.56-27.733 4.096-9.984 6.4-20.949 6.4-32.597z" />
|
||||
<glyph unicode="" glyph-name="trash-2" d="M768 640v-554.667c0-5.845-1.152-11.349-3.2-16.299-2.133-5.205-5.333-9.899-9.301-13.867s-8.661-7.125-13.867-9.301c-4.949-2.048-10.453-3.2-16.299-3.2h-426.667c-5.845 0-11.349 1.152-16.299 3.2-5.205 2.133-9.899 5.333-13.867 9.301s-7.125 8.661-9.301 13.867c-2.048 4.949-3.2 10.453-3.2 16.299v554.667zM725.333 725.334v42.667c0 17.28-3.456 33.835-9.728 48.981-6.485 15.701-16 29.781-27.776 41.557s-25.856 21.291-41.557 27.776c-15.104 6.229-31.659 9.685-48.939 9.685h-170.667c-17.28 0-33.835-3.456-48.981-9.728-15.659-6.485-29.739-16-41.515-27.776s-21.291-25.856-27.776-41.515c-6.272-15.147-9.728-31.701-9.728-48.981v-42.667h-170.667c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h42.667v-554.667c0-17.28 3.456-33.835 9.728-48.981 6.485-15.701 16-29.781 27.776-41.557s25.856-21.291 41.557-27.776c15.104-6.229 31.659-9.685 48.939-9.685h426.667c17.28 0 33.835 3.456 48.981 9.728 15.701 6.485 29.781 16 41.557 27.776s21.291 25.856 27.776 41.557c6.229 15.104 9.685 31.659 9.685 48.939v554.667h42.667c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM384 725.334v42.667c0 5.845 1.152 11.349 3.2 16.299 2.133 5.205 5.333 9.899 9.301 13.867s8.661 7.125 13.867 9.301c4.949 2.048 10.453 3.2 16.299 3.2h170.667c5.845 0 11.349-1.152 16.299-3.2 5.205-2.133 9.899-5.333 13.867-9.301s7.125-8.661 9.301-13.867c2.048-4.949 3.2-10.453 3.2-16.299v-42.667zM384 469.334v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667zM554.667 469.334v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="arrow-down-circle" d="M981.333 426.667c0 63.488-12.629 124.16-35.541 179.499-23.808 57.472-58.667 109.141-101.888 152.363s-94.891 78.123-152.363 101.888c-55.381 22.955-116.053 35.584-179.541 35.584s-124.16-12.629-179.499-35.541c-57.472-23.808-109.141-58.667-152.363-101.931s-78.123-94.891-101.931-152.363c-22.912-55.339-35.541-116.011-35.541-179.499s12.629-124.16 35.541-179.499c23.808-57.472 58.667-109.141 101.888-152.363s94.891-78.123 152.363-101.888c55.381-22.955 116.053-35.584 179.541-35.584s124.16 12.629 179.499 35.541c57.472 23.808 109.141 58.667 152.363 101.888s78.123 94.891 101.888 152.363c22.955 55.381 35.584 116.053 35.584 179.541zM896 426.667c0-52.096-10.368-101.675-29.056-146.859-19.456-46.976-47.957-89.259-83.413-124.672s-77.739-63.957-124.672-83.413c-45.184-18.688-94.763-29.056-146.859-29.056s-101.675 10.368-146.859 29.056c-46.976 19.456-89.259 47.957-124.672 83.413-35.456 35.456-63.957 77.739-83.413 124.672-18.688 45.184-29.056 94.763-29.056 146.859s10.368 101.675 29.056 146.859c19.456 46.976 47.957 89.259 83.413 124.672s77.739 63.957 124.672 83.413c45.184 18.688 94.763 29.056 146.859 29.056s101.675-10.368 146.859-29.056c46.976-19.456 89.259-47.957 124.672-83.413 35.456-35.456 63.957-77.739 83.413-124.672 18.688-45.184 29.056-94.763 29.056-146.859zM469.333 597.334v-238.336l-97.835 97.835c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331l170.667-170.667c4.096-4.096 8.832-7.168 13.867-9.259 5.12-2.091 10.539-3.2 15.957-3.243 0.213 0 0.469 0 0.683 0 5.419 0.043 10.88 1.109 15.957 3.243 5.035 2.091 9.771 5.163 13.867 9.259l170.667 170.667c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-97.835-97.835v238.336c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="arrow-up-circle" d="M981.333 426.667c0 63.488-12.629 124.16-35.541 179.499-23.808 57.472-58.667 109.141-101.888 152.363s-94.891 78.123-152.363 101.888c-55.381 22.955-116.053 35.584-179.541 35.584s-124.16-12.629-179.499-35.541c-57.472-23.808-109.141-58.667-152.363-101.931s-78.123-94.891-101.931-152.363c-22.912-55.339-35.541-116.011-35.541-179.499s12.629-124.16 35.541-179.499c23.808-57.472 58.667-109.141 101.888-152.363s94.891-78.123 152.363-101.888c55.381-22.955 116.053-35.584 179.541-35.584s124.16 12.629 179.499 35.541c57.472 23.808 109.141 58.667 152.363 101.888s78.123 94.891 101.888 152.363c22.955 55.381 35.584 116.053 35.584 179.541zM896 426.667c0-52.096-10.368-101.675-29.056-146.859-19.456-46.976-47.957-89.259-83.413-124.672s-77.739-63.957-124.672-83.413c-45.184-18.688-94.763-29.056-146.859-29.056s-101.675 10.368-146.859 29.056c-46.976 19.456-89.259 47.957-124.672 83.413-35.456 35.456-63.957 77.739-83.413 124.672-18.688 45.184-29.056 94.763-29.056 146.859s10.368 101.675 29.056 146.859c19.456 46.976 47.957 89.259 83.413 124.672s77.739 63.957 124.672 83.413c45.184 18.688 94.763 29.056 146.859 29.056s101.675-10.368 146.859-29.056c46.976-19.456 89.259-47.957 124.672-83.413 35.456-35.456 63.957-77.739 83.413-124.672 18.688-45.184 29.056-94.763 29.056-146.859zM554.667 256v238.336l97.835-97.835c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-170.667 170.667c-0.085 0.085-0.128 0.128-0.213 0.213-4.053 3.968-8.661 6.997-13.611 9.045-10.453 4.309-22.229 4.309-32.683 0-4.949-2.048-9.557-5.077-13.611-9.045-0.085-0.085-0.128-0.128-0.213-0.213l-170.667-170.667c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l97.835 97.835v-238.336c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667z" />
|
||||
<glyph unicode="" glyph-name="copy" d="M469.333 597.334c-17.28 0-33.835-3.456-48.981-9.728-15.659-6.485-29.739-16-41.515-27.776s-21.291-25.856-27.776-41.515c-6.272-15.147-9.728-31.701-9.728-48.981v-384c0-17.28 3.456-33.835 9.728-48.981 6.485-15.701 16-29.781 27.776-41.557s25.856-21.291 41.557-27.776c15.104-6.229 31.659-9.685 48.939-9.685h384c17.28 0 33.835 3.456 48.981 9.728 15.701 6.485 29.781 16 41.557 27.776s21.291 25.856 27.776 41.557c6.229 15.104 9.685 31.659 9.685 48.939v384c0 17.28-3.456 33.835-9.728 48.981-6.485 15.701-16 29.781-27.776 41.557s-25.856 21.291-41.557 27.776c-15.104 6.229-31.659 9.685-48.939 9.685zM469.333 512h384c5.845 0 11.349-1.152 16.299-3.2 5.205-2.133 9.899-5.333 13.867-9.301s7.125-8.661 9.301-13.867c2.048-4.949 3.2-10.453 3.2-16.299v-384c0-5.845-1.152-11.349-3.2-16.299-2.133-5.205-5.333-9.899-9.301-13.867s-8.661-7.125-13.867-9.301c-4.949-2.048-10.453-3.2-16.299-3.2h-384c-5.845 0-11.349 1.152-16.299 3.2-5.205 2.133-9.899 5.333-13.867 9.301s-7.125 8.661-9.301 13.867c-2.048 4.949-3.2 10.453-3.2 16.299v384c0 5.845 1.152 11.349 3.2 16.299 2.133 5.205 5.333 9.899 9.301 13.867s8.661 7.125 13.867 9.301c4.949 2.048 10.453 3.2 16.299 3.2zM213.333 341.334h-42.667c-5.845 0-11.349 1.152-16.299 3.2-5.205 2.133-9.899 5.333-13.867 9.301s-7.125 8.661-9.301 13.867c-2.048 4.949-3.2 10.453-3.2 16.299v384c0 5.845 1.152 11.349 3.2 16.299 2.133 5.205 5.333 9.899 9.301 13.867s8.661 7.125 13.867 9.301c4.949 2.048 10.453 3.2 16.299 3.2h384c5.845 0 11.349-1.152 16.299-3.2 5.205-2.133 9.899-5.333 13.867-9.301s7.125-8.661 9.301-13.867c2.048-4.949 3.2-10.453 3.2-16.299v-42.667c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v42.667c0 17.28-3.456 33.835-9.728 48.981-6.485 15.701-16 29.781-27.776 41.557s-25.856 21.291-41.557 27.776c-15.104 6.229-31.659 9.685-48.939 9.685h-384c-17.28 0-33.835-3.456-48.981-9.728-15.659-6.485-29.739-16-41.515-27.776s-21.291-25.856-27.776-41.515c-6.272-15.147-9.728-31.701-9.728-48.981v-384c0-17.28 3.456-33.835 9.728-48.981 6.485-15.701 16-29.781 27.776-41.557s25.856-21.291 41.557-27.776c15.104-6.229 31.659-9.685 48.939-9.685h42.667c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667z" />
|
||||
<glyph unicode="" glyph-name="search" d="M684.416 262.144c-1.451-1.109-2.859-2.347-4.224-3.712s-2.56-2.731-3.712-4.224c-26.752-25.771-58.24-46.549-93.013-60.971-35.072-14.507-73.6-22.571-114.133-22.571s-79.061 8.064-114.219 22.613c-36.523 15.104-69.419 37.291-96.981 64.896s-49.749 60.459-64.896 96.981c-14.507 35.115-22.571 73.643-22.571 114.176s8.064 79.061 22.613 114.219c15.104 36.48 37.291 69.419 64.853 96.981s60.501 49.749 96.981 64.853c35.157 14.549 73.685 22.613 114.219 22.613s79.061-8.064 114.219-22.613c36.523-15.104 69.419-37.291 96.981-64.896s49.749-60.459 64.896-96.981c14.507-35.115 22.571-73.643 22.571-114.176s-8.064-79.061-22.613-114.219c-14.421-34.773-35.2-66.261-60.971-93.013zM926.165 72.832l-156.8 156.8c22.4 27.989 40.96 59.179 54.869 92.843 18.773 45.312 29.099 94.933 29.099 146.859s-10.325 101.547-29.099 146.859c-19.456 47.019-48 89.301-83.371 124.672s-77.653 63.915-124.672 83.371c-45.312 18.773-94.933 29.099-146.859 29.099s-101.547-10.325-146.859-29.099c-47.019-19.456-89.301-48-124.672-83.371s-63.915-77.653-83.371-124.672c-18.773-45.312-29.099-94.933-29.099-146.859s10.325-101.547 29.099-146.859c19.456-47.019 48-89.301 83.371-124.672s77.653-63.915 124.672-83.371c45.312-18.773 94.933-29.099 146.859-29.099s101.547 10.325 146.859 29.099c33.621 13.952 64.853 32.512 92.843 54.869l156.8-156.8c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331z" />
|
||||
<glyph unicode="" glyph-name="printer" d="M725.333 597.334h-426.667v213.333h426.667zM213.333 128v-128c0-23.552 19.115-42.667 42.667-42.667h512c23.552 0 42.667 19.115 42.667 42.667v128h42.667c17.28 0 33.835 3.456 48.981 9.728 15.701 6.485 29.781 16 41.557 27.776s21.291 25.856 27.776 41.557c6.229 15.104 9.685 31.659 9.685 48.939v213.333c0 17.28-3.456 33.835-9.728 48.981-6.485 15.701-16 29.781-27.776 41.557s-25.856 21.291-41.557 27.776c-15.104 6.229-31.659 9.685-48.939 9.685h-42.667v256c0 23.552-19.115 42.667-42.667 42.667h-512c-23.552 0-42.667-19.115-42.667-42.667v-256h-42.667c-17.28 0-33.835-3.456-48.981-9.728-15.659-6.485-29.739-16-41.515-27.776s-21.291-25.856-27.776-41.515c-6.272-15.147-9.728-31.701-9.728-48.981v-213.333c0-17.28 3.456-33.835 9.728-48.981 6.485-15.701 16-29.781 27.776-41.557s25.856-21.291 41.557-27.776c15.104-6.229 31.659-9.685 48.939-9.685zM256 384c-23.552 0-42.667-19.115-42.667-42.667v-128h-42.667c-5.845 0-11.349 1.152-16.299 3.2-5.205 2.133-9.899 5.333-13.867 9.301s-7.125 8.661-9.301 13.867c-2.048 4.949-3.2 10.453-3.2 16.299v213.333c0 5.845 1.152 11.349 3.2 16.299 2.133 5.205 5.333 9.899 9.301 13.867s8.661 7.125 13.867 9.301c4.949 2.048 10.453 3.2 16.299 3.2h682.667c5.845 0 11.349-1.152 16.299-3.2 5.205-2.133 9.899-5.333 13.867-9.301s7.125-8.661 9.301-13.867c2.048-4.949 3.2-10.453 3.2-16.299v-213.333c0-5.845-1.152-11.349-3.2-16.299-2.133-5.205-5.333-9.899-9.301-13.867s-8.661-7.125-13.867-9.301c-4.949-2.048-10.453-3.2-16.299-3.2h-42.667v128c0 23.552-19.115 42.667-42.667 42.667zM298.667 298.667h426.667v-256h-426.667z" />
|
||||
<glyph unicode="" glyph-name="clock" d="M981.333 426.667c0 63.488-12.629 124.16-35.541 179.499-23.808 57.472-58.667 109.141-101.888 152.363s-94.891 78.123-152.363 101.888c-55.381 22.955-116.053 35.584-179.541 35.584s-124.16-12.629-179.499-35.541c-57.472-23.808-109.141-58.667-152.363-101.931s-78.123-94.891-101.931-152.363c-22.912-55.339-35.541-116.011-35.541-179.499s12.629-124.16 35.541-179.499c23.808-57.472 58.667-109.141 101.888-152.363s94.891-78.123 152.363-101.888c55.381-22.955 116.053-35.584 179.541-35.584s124.16 12.629 179.499 35.541c57.472 23.808 109.141 58.667 152.363 101.888s78.123 94.891 101.888 152.363c22.955 55.381 35.584 116.053 35.584 179.541zM896 426.667c0-52.096-10.368-101.675-29.056-146.859-19.456-46.976-47.957-89.259-83.413-124.672s-77.739-63.957-124.672-83.413c-45.184-18.688-94.763-29.056-146.859-29.056s-101.675 10.368-146.859 29.056c-46.976 19.456-89.259 47.957-124.672 83.413-35.456 35.456-63.957 77.739-83.413 124.672-18.688 45.184-29.056 94.763-29.056 146.859s10.368 101.675 29.056 146.859c19.456 46.976 47.957 89.259 83.413 124.672s77.739 63.957 124.672 83.413c45.184 18.688 94.763 29.056 146.859 29.056s101.675-10.368 146.859-29.056c46.976-19.456 89.259-47.957 124.672-83.413 35.456-35.456 63.957-77.739 83.413-124.672 18.688-45.184 29.056-94.763 29.056-146.859zM469.333 682.667v-256c0-16.597 9.472-31.019 23.595-38.144l170.667-85.333c21.077-10.539 46.72-2.005 57.259 19.072s2.005 46.72-19.072 57.259l-147.115 73.515v229.632c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="download" d="M853.333 298.667v-170.667c0-5.845-1.152-11.349-3.2-16.299-2.133-5.205-5.333-9.899-9.301-13.867s-8.661-7.125-13.867-9.301c-4.949-2.048-10.453-3.2-16.299-3.2h-597.333c-5.845 0-11.349 1.152-16.299 3.2-5.205 2.133-9.899 5.333-13.867 9.301s-7.125 8.661-9.301 13.867c-2.048 4.949-3.2 10.453-3.2 16.299v170.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-170.667c0-17.28 3.456-33.835 9.728-48.981 6.485-15.701 16-29.781 27.776-41.557s25.856-21.291 41.557-27.776c15.104-6.229 31.659-9.685 48.939-9.685h597.333c17.28 0 33.835 3.456 48.981 9.728 15.701 6.485 29.781 16 41.557 27.776s21.291 25.856 27.776 41.557c6.229 15.104 9.685 31.659 9.685 48.939v170.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667zM554.667 401.664v409.003c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-409.003l-140.501 140.501c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331l213.333-213.333c0.085-0.085 0.171-0.171 0.256-0.256 4.053-3.968 8.661-6.955 13.568-9.003 5.12-2.133 10.624-3.2 16.085-3.243 0.171 0 0.341 0 0.469 0 5.461 0.043 10.965 1.109 16.085 3.243 5.035 2.091 9.728 5.163 13.824 9.259l213.333 213.333c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0z" />
|
||||
<glyph unicode="" glyph-name="plus" d="M213.333 384h256v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256h256c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-256v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-256h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="minus" d="M213.333 384h597.333c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-597.333c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="sort-amount-asc" d="M320 192v768h-128v-768h-160l224-224 224 224h-160zM448 384h576v-128h-576v128zM448 576h448v-128h-448v128zM448 768h320v-128h-320v128zM448 960h192v-128h-192v128z" />
|
||||
<glyph unicode="🗎" glyph-name="maximize" d="M341.333 853.334h-128c-17.28 0-33.835-3.456-48.981-9.728-15.659-6.485-29.739-16-41.515-27.776s-21.291-25.856-27.776-41.515c-6.272-15.147-9.728-31.701-9.728-48.981v-128c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v128c0 5.845 1.152 11.349 3.2 16.299 2.133 5.205 5.333 9.899 9.301 13.867s8.661 7.125 13.867 9.301c4.949 2.048 10.453 3.2 16.299 3.2h128c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM938.667 597.334v128c0 17.28-3.456 33.835-9.728 48.981-6.485 15.701-16 29.781-27.776 41.557s-25.856 21.291-41.557 27.776c-15.104 6.229-31.659 9.685-48.939 9.685h-128c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h128c5.845 0 11.349-1.152 16.299-3.2 5.205-2.133 9.899-5.333 13.867-9.301s7.125-8.661 9.301-13.867c2.048-4.949 3.2-10.453 3.2-16.299v-128c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667zM682.667-0h128c17.28 0 33.835 3.456 48.981 9.728 15.701 6.485 29.781 16 41.557 27.776s21.291 25.856 27.776 41.557c6.229 15.104 9.685 31.659 9.685 48.939v128c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-128c0-5.845-1.152-11.349-3.2-16.299-2.133-5.205-5.333-9.899-9.301-13.867s-8.661-7.125-13.867-9.301c-4.949-2.048-10.453-3.2-16.299-3.2h-128c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM85.333 256v-128c0-17.28 3.456-33.835 9.728-48.981 6.485-15.701 16-29.781 27.776-41.557s25.856-21.291 41.557-27.776c15.104-6.229 31.659-9.685 48.939-9.685h128c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-128c-5.845 0-11.349 1.152-16.299 3.2-5.205 2.133-9.899 5.333-13.867 9.301s-7.125 8.661-9.301 13.867c-2.048 4.949-3.2 10.453-3.2 16.299v128c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 23 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user