Merge branch 'master' into notify-context
This commit is contained in:
commit
0468d8b3cd
|
|
@ -13,5 +13,5 @@ runs:
|
|||
invoke export-records -f data.json
|
||||
python3 ./src/backend/InvenTree/manage.py flush --noinput
|
||||
invoke migrate
|
||||
invoke import-records -c -f data.json
|
||||
invoke import-records -c -f data.json
|
||||
invoke import-records -c -f data.json --strict
|
||||
invoke import-records -c -f data.json --strict
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ inputs:
|
|||
required: false
|
||||
description: 'Install the InvenTree requirements?'
|
||||
default: 'false'
|
||||
static:
|
||||
required: false
|
||||
description: 'Should the static files be built?'
|
||||
default: 'false'
|
||||
dev-install:
|
||||
required: false
|
||||
description: 'Install the InvenTree development requirements?'
|
||||
|
|
@ -103,3 +107,7 @@ runs:
|
|||
if: ${{ inputs.update == 'true' }}
|
||||
shell: bash
|
||||
run: invoke update --skip-backup --skip-static
|
||||
- name: Collect static files
|
||||
if: ${{ inputs.static == 'true' }}
|
||||
shell: bash
|
||||
run: invoke static --skip-plugins
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
"""Script to check a data file exported using the 'export-records' command.
|
||||
|
||||
This script is intended to be used as part of the CI workflow,
|
||||
in conjunction with the "workflows/import_export.yaml" workflow.
|
||||
|
||||
In reads the exported data file, to ensure that:
|
||||
|
||||
- The file can be read and parsed as JSON
|
||||
- The file contains the expected metadata
|
||||
- The file contains the expected plugin configuration
|
||||
- The file contains the expected plugin database records
|
||||
|
||||
"""
|
||||
|
||||
PLUGIN_KEY = 'dummy_app_plugin'
|
||||
PLUGIN_SLUG = 'dummy-app-plugin'
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Check exported data file')
|
||||
parser.add_argument('datafile', help='Path to the exported data file (JSON)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isfile(args.datafile):
|
||||
print(f'Error: File not found: {args.datafile}')
|
||||
exit(1)
|
||||
|
||||
with open(args.datafile, encoding='utf-8') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
print(f'Successfully loaded data from {args.datafile}')
|
||||
print(f'Number of records: {len(data)}')
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'Error: Failed to parse JSON file: {e}')
|
||||
exit(1)
|
||||
|
||||
found_metadata = False
|
||||
found_installed_apps = False
|
||||
found_plugin_config = False
|
||||
plugin_data_records = {}
|
||||
|
||||
# Inspect the data and check that it has the expected structure and content.
|
||||
for entry in data:
|
||||
# Check metadata entry for expected values
|
||||
if entry.get('metadata', False):
|
||||
print('Found metadata entry')
|
||||
found_metadata = True
|
||||
|
||||
expected_apps = ['InvenTree', 'allauth', 'dbbackup', PLUGIN_KEY]
|
||||
|
||||
apps = entry.get('installed_apps', [])
|
||||
|
||||
for app in expected_apps:
|
||||
if app not in apps:
|
||||
print(f'- Expected app "{app}" not found in installed apps list')
|
||||
exit(1)
|
||||
|
||||
found_installed_apps = True
|
||||
|
||||
elif entry.get('model', None) == 'plugin.pluginconfig':
|
||||
key = entry['fields']['key']
|
||||
|
||||
if key == PLUGIN_SLUG:
|
||||
print(f'Found plugin configuration for plugin "{PLUGIN_KEY}"')
|
||||
found_plugin_config = True
|
||||
|
||||
elif entry.get('model', None) == f'{PLUGIN_KEY}.examplemodel':
|
||||
key = entry['fields']['key']
|
||||
value = entry['fields']['value']
|
||||
|
||||
plugin_data_records[key] = value
|
||||
|
||||
if not found_metadata:
|
||||
print('Error: No metadata entry found in exported data')
|
||||
exit(1)
|
||||
|
||||
if not found_installed_apps:
|
||||
print(
|
||||
f'Error: Plugin "{PLUGIN_KEY}" not found in installed apps list in metadata'
|
||||
)
|
||||
exit(1)
|
||||
|
||||
if not found_plugin_config:
|
||||
print(f'Error: No plugin configuration found for plugin "{PLUGIN_KEY}"')
|
||||
exit(1)
|
||||
|
||||
# Check the extracted plugin records
|
||||
expected_keys = ['alpha', 'beta', 'gamma', 'delta']
|
||||
|
||||
for key in expected_keys:
|
||||
if key not in plugin_data_records:
|
||||
print(
|
||||
f'Error: Expected plugin record with key "{key}" not found in exported data'
|
||||
)
|
||||
exit(1)
|
||||
|
||||
print('All checks passed successfully!')
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -168,13 +168,13 @@ jobs:
|
|||
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
|
||||
- name: Set up QEMU
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # pin@v3.7.0
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # pin@v4.0.0
|
||||
- name: Set up Docker Buildx
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # pin@v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # pin@v4.0.0
|
||||
- name: Set up cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # pin@v4.0.0
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # pin@v4.1.1
|
||||
- name: Check if Dockerhub login is required
|
||||
id: docker_login
|
||||
run: |
|
||||
|
|
@ -185,14 +185,14 @@ jobs:
|
|||
fi
|
||||
- name: Login to Dockerhub
|
||||
if: github.event_name != 'pull_request' && steps.docker_login.outputs.skip_dockerhub_login != 'true'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # pin@v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # pin@v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log into registry ghcr.io
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # pin@v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # pin@v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
|
@ -201,7 +201,7 @@ jobs:
|
|||
- name: Extract Docker metadata
|
||||
if: github.event_name != 'pull_request'
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # pin@v5.10.0
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # pin@v6.0.0
|
||||
with:
|
||||
images: |
|
||||
inventree/inventree
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
# Ensure that data import / export functionality works as expected.
|
||||
# - Create a dataset in a Postgres database (including plugin data)
|
||||
# - Export the dataset to an agnostic format (JSON)
|
||||
# - Import the dataset into a Sqlite database
|
||||
|
||||
name: Import / Export
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: ["l10*"]
|
||||
pull_request:
|
||||
branches-ignore: ["l10*"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
python_version: 3.11
|
||||
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
INVENTREE_DEBUG: false
|
||||
INVENTREE_LOG_LEVEL: WARNING
|
||||
INVENTREE_MEDIA_ROOT: /home/runner/work/InvenTree/test_inventree_media
|
||||
INVENTREE_STATIC_ROOT: /home/runner/work/InvenTree/test_inventree_static
|
||||
INVENTREE_BACKUP_DIR: /home/runner/work/InvenTree/test_inventree_backup
|
||||
INVENTREE_SITE_URL: http://localhost:8000
|
||||
|
||||
INVENTREE_PLUGINS_ENABLED: true
|
||||
INVENTREE_AUTO_UPDATE: true
|
||||
INVENTREE_PLUGINS_MANDATORY: "dummy-app-plugin"
|
||||
INVENTREE_GLOBAL_SETTINGS: '{"ENABLE_PLUGINS_APP": true}'
|
||||
|
||||
DATA_FILE: /home/runner/work/InvenTree/test_inventree_data.json
|
||||
|
||||
INVENTREE_DB_ENGINE: postgresql
|
||||
INVENTREE_DB_NAME: inventree
|
||||
INVENTREE_DB_USER: inventree
|
||||
INVENTREE_DB_PASSWORD: password
|
||||
INVENTREE_DB_HOST: "127.0.0.1"
|
||||
INVENTREE_DB_PORT: 5432
|
||||
|
||||
jobs:
|
||||
|
||||
paths-filter:
|
||||
name: filter
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
server: ${{ steps.filter.outputs.server }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
server:
|
||||
- .github/workflows/import_export.yaml
|
||||
- .github/scripts/check_exported_data.py
|
||||
- 'src/backend/**'
|
||||
- 'tasks.py'
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: paths-filter
|
||||
if: needs.paths-filter.outputs.server == 'true' || contains(github.event.pull_request.labels.*.name, 'full-run')
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
env:
|
||||
POSTGRES_USER: inventree
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Environment Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
apt-dependency: gettext poppler-utils libpq-dev
|
||||
pip-dependency: psycopg
|
||||
update: true
|
||||
static: false
|
||||
- name: Setup Postgres Database
|
||||
run: |
|
||||
invoke migrate
|
||||
invoke dev.setup-test -i
|
||||
- name: Create Plugin Data
|
||||
run: |
|
||||
pip install -U inventree-dummy-app-plugin
|
||||
invoke migrate
|
||||
cd src/backend/InvenTree && python manage.py create_dummy_data
|
||||
- name: Export Postgres Dataset
|
||||
run: |
|
||||
invoke export-records -o -f ${{ env.DATA_FILE }}
|
||||
python .github/scripts/check_exported_data.py ${{ env.DATA_FILE }}
|
||||
invoke dev.delete-data --force
|
||||
- name: Update Environment Variables for Sqlite
|
||||
run: |
|
||||
echo "Updating environment variables for Sqlite"
|
||||
echo "INVENTREE_DB_ENGINE=sqlite" >> $GITHUB_ENV
|
||||
echo "INVENTREE_DB_NAME=/home/runner/work/InvenTree/test_inventree_db.sqlite3" >> $GITHUB_ENV
|
||||
- name: Setup Sqlite Database
|
||||
run: |
|
||||
invoke migrate
|
||||
test -f /home/runner/work/InvenTree/test_inventree_db.sqlite3 || (echo "Sqlite database not created" && exit 1)
|
||||
- name: Import Sqlite Dataset
|
||||
run: |
|
||||
invoke import-records -c -f ${{ env.DATA_FILE }} --strict
|
||||
cd src/backend/InvenTree && python manage.py check_dummy_data
|
||||
- name: Export Sqlite Dataset
|
||||
run: |
|
||||
invoke export-records -o -f ${{ env.DATA_FILE }}
|
||||
python .github/scripts/check_exported_data.py ${{ env.DATA_FILE }}
|
||||
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -222,7 +222,7 @@ jobs:
|
|||
echo "Downloaded api.yaml"
|
||||
- name: Running OpenAPI Spec diff action
|
||||
id: breaking_changes
|
||||
uses: oasdiff/oasdiff-action/diff@1c611ffb1253a72924624aa4fb662e302b3565d3 # pin@main
|
||||
uses: oasdiff/oasdiff-action/diff@1f38ea5ea0b4a2e4e49901c3bcdf4386a05e9ea1 # pin@main
|
||||
with:
|
||||
base: "api.yaml"
|
||||
revision: "src/backend/InvenTree/schema.yml"
|
||||
|
|
@ -284,7 +284,7 @@ jobs:
|
|||
- name: Create artifact directory
|
||||
run: mkdir -p artifact
|
||||
- name: Download schema artifact
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # pin@v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
|
||||
with:
|
||||
path: artifact
|
||||
merge-multiple: true
|
||||
|
|
@ -341,6 +341,7 @@ jobs:
|
|||
apt-dependency: gettext poppler-utils
|
||||
dev-install: true
|
||||
update: true
|
||||
static: true
|
||||
npm: true
|
||||
- name: Download Python Code For `${WRAPPER_NAME}`
|
||||
run: git clone --depth 1 https://github.com/inventree/${WRAPPER_NAME} ./${WRAPPER_NAME}
|
||||
|
|
@ -362,7 +363,7 @@ jobs:
|
|||
pip install .
|
||||
if: needs.paths-filter.outputs.submit-performance == 'true'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # pin@v4
|
||||
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # pin@v4
|
||||
# check if we are in inventree/inventree - reporting only works in that OIDC context
|
||||
if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true'
|
||||
with:
|
||||
|
|
@ -398,6 +399,7 @@ jobs:
|
|||
apt-dependency: gettext poppler-utils
|
||||
dev-install: true
|
||||
update: true
|
||||
static: true
|
||||
- name: Data Export Test
|
||||
uses: ./.github/actions/migration
|
||||
- name: Test Translations
|
||||
|
|
@ -413,7 +415,7 @@ jobs:
|
|||
path: .coverage
|
||||
retention-days: 14
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # pin@v5.5.2
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # pin@v6.0.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -452,7 +454,7 @@ jobs:
|
|||
env:
|
||||
node_version: '>=20.19.0'
|
||||
- name: Performance Reporting
|
||||
uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # pin@v4
|
||||
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # pin@v4
|
||||
with:
|
||||
mode: walltime
|
||||
run: inv dev.test --pytest
|
||||
|
|
@ -500,6 +502,7 @@ jobs:
|
|||
pip-dependency: psycopg django-redis>=5.0.0
|
||||
dev-install: true
|
||||
update: true
|
||||
static: true
|
||||
- name: Run Tests
|
||||
run: invoke dev.test --check --translations
|
||||
- name: Data Export Test
|
||||
|
|
@ -548,6 +551,7 @@ jobs:
|
|||
pip-dependency: mysqlclient
|
||||
dev-install: true
|
||||
update: true
|
||||
static: true
|
||||
- name: Run Tests
|
||||
run: invoke dev.test --check --translations
|
||||
- name: Data Export Test
|
||||
|
|
@ -593,7 +597,7 @@ jobs:
|
|||
- name: Run Tests
|
||||
run: invoke dev.test --check --migrations --report --coverage --translations
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # pin@v5.5.2
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # pin@v6.0.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -702,7 +706,7 @@ jobs:
|
|||
npm: true
|
||||
install: true
|
||||
update: true
|
||||
apt-dependency: postgresql-client libpq-dev
|
||||
apt-dependency: gettext postgresql-client libpq-dev
|
||||
pip-dependency: psycopg2
|
||||
- name: Set up test data
|
||||
run: |
|
||||
|
|
@ -730,7 +734,7 @@ jobs:
|
|||
- name: Report coverage
|
||||
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # pin@v5.5.2
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # pin@v6.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: inventree/InvenTree
|
||||
|
|
@ -759,7 +763,7 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: cd src/frontend && yarn install
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && yarn run compile && yarn run build
|
||||
run: cd src/frontend && yarn run compile && yarn run lib && yarn run build
|
||||
- name: Write version file - SHA
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
|
||||
- name: Zip frontend
|
||||
|
|
@ -785,13 +789,13 @@ jobs:
|
|||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin@v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: hynek/setup-cached-uv@757bedc3f972eb7227a1aa657651f15a8527c817 # pin@v2
|
||||
- uses: hynek/setup-cached-uv@4300ec2180bc77d705e626a34e381b81a4772c51 # pin@v2
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # pin@v3
|
||||
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # pin@v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
- name: Build frontend
|
||||
run: cd src/frontend && npm run compile && npm run build
|
||||
- name: Create SBOM for frontend
|
||||
uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # pin@v0
|
||||
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # pin@v0
|
||||
with:
|
||||
artifact-name: frontend-build.spdx
|
||||
path: src/frontend
|
||||
|
|
@ -78,7 +78,7 @@ jobs:
|
|||
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"
|
||||
|
||||
- name: Upload frontend
|
||||
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
|
||||
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # pin@2.11.5
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: src/backend/InvenTree/web/static/frontend-build.zip
|
||||
|
|
@ -91,7 +91,7 @@ jobs:
|
|||
name: frontend-build
|
||||
path: src/backend/InvenTree/web/static/frontend-build.zip
|
||||
- name: Upload Attestation
|
||||
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
|
||||
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # pin@2.11.5
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
asset_name: frontend-build.intoto.jsonl
|
||||
|
|
@ -134,7 +134,7 @@ jobs:
|
|||
cd docs/site
|
||||
zip -r docs-html.zip *
|
||||
- name: Publish documentation
|
||||
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
|
||||
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # pin@2.11.5
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: docs/site/docs-html.zip
|
||||
|
|
@ -163,7 +163,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Get frontend artifact
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # pin@v8.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin@v8.0.1
|
||||
with:
|
||||
name: frontend-build
|
||||
- name: Setup
|
||||
|
|
@ -244,7 +244,7 @@ jobs:
|
|||
channel: ${{ env.pkg_channel }}
|
||||
file: ${{ steps.package.outputs.package_path }}
|
||||
- name: Publish to artifact
|
||||
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # pin@2.11.4
|
||||
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # pin@2.11.5
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.package.outputs.package_path }}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
echo "Resetting to HEAD~"
|
||||
git reset HEAD~ || true
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@8818ff65bfc4322384f983ea37e3926948c11745 # pin@v2
|
||||
uses: crowdin/github-action@7ca9c452bfe9197d3bb7fa83a4d7e2b0c9ae835d # pin@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ before:
|
|||
- contrib/packager.io/before.sh
|
||||
dependencies:
|
||||
- curl
|
||||
- poppler-utils
|
||||
- "python3.11 | python3.12 | python3.13 | python3.14"
|
||||
- "python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
|
||||
- "python3.11-dev | python3.12-dev | python3.13-dev | python3.14-dev"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@
|
|||
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
|
||||
"args": [
|
||||
"runserver",
|
||||
"0.0.0.0:8000"
|
||||
"0.0.0.0:8000",
|
||||
// "--sync",// Synchronize worker tasks to foreground thread
|
||||
// "--noreload", // disable auto-reload
|
||||
],
|
||||
"django": true,
|
||||
"justMyCode": false
|
||||
|
|
|
|||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -10,20 +10,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Breaking Changes
|
||||
|
||||
- [#11303](https://github.com/inventree/InvenTree/pull/11303) removes the `default_supplier` field from the `Part` model. Instead, the `SupplierPart` model now has a `primary` field which is used to indicate which supplier is the default for a given part. Any external client applications which made use of the old `default_supplier` field will need to be updated.
|
||||
- [#11500](https://github.com/inventree/InvenTree/pull/11500) fixes a spelling mistake in the database configuration values, which may affect some users running the PostgreSQL database backend. The `tcp_keepalives_internal` option has been renamed to `tcp_keepalives_interval` to reflect the correct PostgreSQL configuration option name. If you are using PostgreSQL, and have set a custom value for the `tcp_keepalives_internal` option, you will need to update this to `tcp_keepalives_interval` in your configuration (either via environment variable or config file).
|
||||
|
||||
### Added
|
||||
|
||||
- [#11702](https://github.com/inventree/InvenTree/pull/11702) adds "last updated" and "updated by" fields for label and report templates, allowing users to track when a template was last modified and by whom.
|
||||
- [#11685](https://github.com/inventree/InvenTree/pull/11685) exposes the data importer wizard to the plugin interface, allowing plugins to trigger the data importer wizard and perform custom data imports from the UI.
|
||||
- [#11692](https://github.com/inventree/InvenTree/pull/11692) adds line item numbering for external orders (purchase, sales and return orders). This allows users to specify a line number for each line item on the order, which can be used for reference purposes. The line number is optional, and can be left blank if not required. The line number is stored as a string, to allow for more flexible formatting (e.g. "1", "1.1", "A", etc).
|
||||
- [#11641](https://github.com/inventree/InvenTree/pull/11641) adds support for custom parameters against the SalesOrderShipment model.
|
||||
- [#11527](https://github.com/inventree/InvenTree/pull/11527) adds a new API endpoint for monitoring the status of a particular background task. This endpoint allows clients to check the status of a background task and receive updates when the task is complete. This is useful for long-running tasks that may take some time to complete, allowing clients to provide feedback to users about the progress of the task.
|
||||
- [#11405](https://github.com/inventree/InvenTree/pull/11405) adds default table filters, which hide inactive items by default. The default table filters are overridden by user filter selection, and only apply to the table view initially presented to the user. This means that users can still view inactive items if they choose to, but they will not be shown by default.
|
||||
- [#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs.
|
||||
- [#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API.
|
||||
- [#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules.
|
||||
- [#11372](https://github.com/inventree/InvenTree/pull/11372) adds backup metadata setter and restore metadata validator functions to ensure common footguns are harder to trigger when using the backup and restore functionality.
|
||||
- [#11374](https://github.com/inventree/InvenTree/pull/11374) adds `updated_at` field on purchase, sales and return orders.
|
||||
- [#11074](https://github.com/inventree/InvenTree/pull/11074) adds "Keep form open" option on create form which leaves dialog with form opened after form submitting.
|
||||
|
||||
### Changed
|
||||
|
||||
- [#11648](https://github.com/inventree/InvenTree/pull/11648) improves the import/export process, allowing data records defined by plugins to be loaded when importing a database from a file.
|
||||
- [#11630](https://github.com/inventree/InvenTree/pull/11630) enhances the `import_records` and `export_records` system commands, by adding a metadata entry to the exported data file to allow for compatibility checks during data import.
|
||||
|
||||
### Removed
|
||||
|
||||
- [#11581](https://github.com/inventree/InvenTree/pull/11581) removes the ability to specify arbitrary filters when performing bulk operations via the API. This functionality represented a significant security risk, and was not required for any existing use cases. Bulk operations now only work with a provided list of primary keys.
|
||||
|
||||
## 1.2.0 - 2026-02-12
|
||||
|
||||
### Breaking Changes
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
# - Runs InvenTree web server under django development server
|
||||
# - Monitors source files for any changes, and live-reloads server
|
||||
|
||||
# Base image last bumped 2026-02-23
|
||||
FROM python:3.14-slim-trixie@sha256:486b8092bfb12997e10d4920897213a06563449c951c5506c2a2cfaf591c599f AS inventree_base
|
||||
# Base image last bumped 2026-03-20
|
||||
FROM python:3.14-slim-trixie@sha256:fb83750094b46fd6b8adaa80f66e2302ecbe45d513f6cece637a841e1025b4ca AS inventree_base
|
||||
|
||||
# Build arguments for this image
|
||||
ARG commit_tag=""
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ asgiref==3.11.1 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# django
|
||||
django==5.2.12 \
|
||||
--hash=sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7 \
|
||||
--hash=sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb
|
||||
django==5.2.13 \
|
||||
--hash=sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a \
|
||||
--hash=sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# -r contrib/container/requirements.in
|
||||
|
|
@ -17,9 +17,9 @@ django-auth-ldap==5.3.0 \
|
|||
--hash=sha256:743d8107b146240b46f7e97207dc06cb11facc0cd70dce490b7ca09dd5643d19 \
|
||||
--hash=sha256:aa880415983149b072f876d976ef8ec755a438090e176817998263a6ed9e1038
|
||||
# via -r contrib/container/requirements.in
|
||||
gunicorn==25.1.0 \
|
||||
--hash=sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616 \
|
||||
--hash=sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b
|
||||
gunicorn==25.3.0 \
|
||||
--hash=sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660 \
|
||||
--hash=sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# -r contrib/container/requirements.in
|
||||
|
|
@ -44,15 +44,14 @@ mariadb==1.1.14 \
|
|||
--hash=sha256:98d552a8bb599eceaa88f65002ad00bd88aeed160592c273a7e5c1d79ab733dd \
|
||||
--hash=sha256:e6d702a53eccf20922e47f2f45cfb5c7a0c2c6c0a46e4ee2d8a80d0ff4a52f34
|
||||
# via -r contrib/container/requirements.in
|
||||
mysqlclient==2.2.7 \
|
||||
--hash=sha256:199dab53a224357dd0cb4d78ca0e54018f9cee9bf9ec68d72db50e0a23569076 \
|
||||
--hash=sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5 \
|
||||
--hash=sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845 \
|
||||
--hash=sha256:2e3c11f7625029d7276ca506f8960a7fd3c5a0a0122c9e7404e6a8fe961b3d22 \
|
||||
--hash=sha256:4b4c0200890837fc64014cc938ef2273252ab544c1b12a6c1d674c23943f3f2e \
|
||||
--hash=sha256:92af368ed9c9144737af569c86d3b6c74a012a6f6b792eb868384787b52bb585 \
|
||||
--hash=sha256:977e35244fe6ef44124e9a1c2d1554728a7b76695598e4b92b37dc2130503069 \
|
||||
--hash=sha256:a22d99d26baf4af68ebef430e3131bb5a9b722b79a9fcfac6d9bbf8a88800687
|
||||
mysqlclient==2.2.8 \
|
||||
--hash=sha256:000c7ec3d11e7c411db832e4cfcd7f05db47464326381f5d5ae991b4bb572f93 \
|
||||
--hash=sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99 \
|
||||
--hash=sha256:60c9ed339dc09e3d5380cc2a9f42e86754fee25a661ced77a02df77990664c92 \
|
||||
--hash=sha256:86db31bba7b3480fec2751350e9790e24f016f89af33a87bab7e79f7196474e8 \
|
||||
--hash=sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199 \
|
||||
--hash=sha256:9bed7c8d3b629bdc09e17fb628d5b3b0a5fd1f12b09432b464b9126c727bedc0 \
|
||||
--hash=sha256:a81f5e12f8d05439709cb02fba97f9f76d1a6c528164f2260d8798fec969e300
|
||||
# via -r contrib/container/requirements.in
|
||||
packaging==26.0 \
|
||||
--hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \
|
||||
|
|
@ -62,74 +61,74 @@ packaging==26.0 \
|
|||
# gunicorn
|
||||
# mariadb
|
||||
# wheel
|
||||
psycopg[binary, pool]==3.3.2 \
|
||||
--hash=sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b \
|
||||
--hash=sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7
|
||||
psycopg[binary, pool]==3.3.3 \
|
||||
--hash=sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9 \
|
||||
--hash=sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698
|
||||
# via -r contrib/container/requirements.in
|
||||
psycopg-binary==3.3.2 \
|
||||
--hash=sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9 \
|
||||
--hash=sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1 \
|
||||
--hash=sha256:0611f4822674f3269e507a307236efb62ae5a828fcfc923ac85fe22ca19fd7c8 \
|
||||
--hash=sha256:0768c5f32934bb52a5df098317eca9bdcf411de627c5dca2ee57662b64b54b41 \
|
||||
--hash=sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b \
|
||||
--hash=sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2 \
|
||||
--hash=sha256:09b3014013f05cd89828640d3a1db5f829cc24ad8fa81b6e42b2c04685a0c9d4 \
|
||||
--hash=sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7 \
|
||||
--hash=sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9 \
|
||||
--hash=sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678 \
|
||||
--hash=sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0 \
|
||||
--hash=sha256:1ea41c0229f3f5a3844ad0857a83a9f869aa7b840448fa0c200e6bcf85d33d19 \
|
||||
--hash=sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed \
|
||||
--hash=sha256:3789d452a9d17a841c7f4f97bbcba51a21f957ea35641a4c98507520e6b6a068 \
|
||||
--hash=sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688 \
|
||||
--hash=sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c \
|
||||
--hash=sha256:44e89938d36acc4495735af70a886d206a5bfdc80258f95b69b52f68b2968d9e \
|
||||
--hash=sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e \
|
||||
--hash=sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e \
|
||||
--hash=sha256:522b79c7db547767ca923e441c19b97a2157f2f494272a119c854bba4804e186 \
|
||||
--hash=sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860 \
|
||||
--hash=sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4 \
|
||||
--hash=sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924 \
|
||||
--hash=sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf \
|
||||
--hash=sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d \
|
||||
--hash=sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0 \
|
||||
--hash=sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a \
|
||||
--hash=sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7 \
|
||||
--hash=sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca \
|
||||
--hash=sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121 \
|
||||
--hash=sha256:8ea05b499278790a8fa0ff9854ab0de2542aca02d661ddff94e830df971ff640 \
|
||||
--hash=sha256:90ed9da805e52985b0202aed4f352842c907c6b4fc6c7c109c6e646c32e2f43b \
|
||||
--hash=sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa \
|
||||
--hash=sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4 \
|
||||
--hash=sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390 \
|
||||
--hash=sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634 \
|
||||
--hash=sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f \
|
||||
--hash=sha256:c3a9ccdfee4ae59cf9bf1822777e763bc097ed208f4901e21537fca1070e1391 \
|
||||
--hash=sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c \
|
||||
--hash=sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12 \
|
||||
--hash=sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443 \
|
||||
--hash=sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d \
|
||||
--hash=sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d \
|
||||
--hash=sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578 \
|
||||
--hash=sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14 \
|
||||
--hash=sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed \
|
||||
--hash=sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d \
|
||||
--hash=sha256:de9173f8cc0efd88ac2a89b3b6c287a9a0011cdc2f53b2a12c28d6fd55f9f81c \
|
||||
--hash=sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc \
|
||||
--hash=sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da \
|
||||
--hash=sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b \
|
||||
--hash=sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30 \
|
||||
--hash=sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3 \
|
||||
--hash=sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7 \
|
||||
--hash=sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b
|
||||
psycopg-binary==3.3.3 \
|
||||
--hash=sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d \
|
||||
--hash=sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12 \
|
||||
--hash=sha256:0dde92cfde09293fb63b3f547919ba7d73bd2654573c03502b3263dd0218e44e \
|
||||
--hash=sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430 \
|
||||
--hash=sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc \
|
||||
--hash=sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383 \
|
||||
--hash=sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b \
|
||||
--hash=sha256:19f93235ece6dbfc4036b5e4f6d8b13f0b8f2b3eeb8b0bd2936d406991bcdd40 \
|
||||
--hash=sha256:1bef235a50a80f6aba05147002bc354559657cb6386dbd04d8e1c97d1d7cbe84 \
|
||||
--hash=sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd \
|
||||
--hash=sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351 \
|
||||
--hash=sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8 \
|
||||
--hash=sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d \
|
||||
--hash=sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d \
|
||||
--hash=sha256:48e500cf1c0984dacf1f28ea482c3cdbb4c2288d51c336c04bc64198ab21fc51 \
|
||||
--hash=sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2 \
|
||||
--hash=sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1 \
|
||||
--hash=sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d \
|
||||
--hash=sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6 \
|
||||
--hash=sha256:56c767007ca959ca32f796b42379fc7e1ae2ed085d29f20b05b3fc394f3715cc \
|
||||
--hash=sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e \
|
||||
--hash=sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14 \
|
||||
--hash=sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead \
|
||||
--hash=sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d \
|
||||
--hash=sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e \
|
||||
--hash=sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b \
|
||||
--hash=sha256:78c9ce98caaf82ac8484d269791c1b403d7598633e0e4e2fa1097baae244e2f1 \
|
||||
--hash=sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1 \
|
||||
--hash=sha256:883d68d48ca9ff3cb3d10c5fdebea02c79b48eecacdddbf7cce6e7cdbdc216b8 \
|
||||
--hash=sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83 \
|
||||
--hash=sha256:90eecd93073922f085967f3ed3a98ba8c325cbbc8c1a204e300282abd2369e13 \
|
||||
--hash=sha256:97c839717bf8c8df3f6d983a20949c4fb22e2a34ee172e3e427ede363feda27b \
|
||||
--hash=sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2 \
|
||||
--hash=sha256:9f7d0cf072c6fbac3795b08c98ef9ea013f11db609659dcfc6b1f6cc31f9e181 \
|
||||
--hash=sha256:a39f34c9b18e8f6794cca17bfbcd64572ca2482318db644268049f8c738f35a6 \
|
||||
--hash=sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830 \
|
||||
--hash=sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023 \
|
||||
--hash=sha256:a89bb9ee11177b2995d87186b1d9fa892d8ea725e85eab28c6525e4cc14ee048 \
|
||||
--hash=sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1 \
|
||||
--hash=sha256:b27d3a23c79fa59557d2cc63a7e8bb4c7e022c018558eda36f9d7c4e6b99a6e0 \
|
||||
--hash=sha256:b3385b58b2fe408a13d084c14b8dcf468cd36cbbe774408250facc128f9fa75c \
|
||||
--hash=sha256:b62cf8784eb6d35beaee1056d54caf94ec6ecf2b7552395e305518ab61eb8fd2 \
|
||||
--hash=sha256:cab7bc3d288d37a80aa8c0820033250c95e40b1c2b5c57cf59827b19c2a8b69d \
|
||||
--hash=sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508 \
|
||||
--hash=sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482 \
|
||||
--hash=sha256:d593612758d0041cb13cb0003f7f8d3fabb7ad9319e651e78afae49b1cf5860e \
|
||||
--hash=sha256:da2f331a01af232259a21573a01338530c6016dcfad74626c01330535bcd8628 \
|
||||
--hash=sha256:dac7ee2f88b4d7bb12837989ca354c38d400eeb21bce3b73dac02622f0a3c8d6 \
|
||||
--hash=sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925 \
|
||||
--hash=sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856 \
|
||||
--hash=sha256:e7b607f0e14f2a4cf7e78a05ebd13df6144acfba87cb90842e70d3f125d9f53f \
|
||||
--hash=sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df \
|
||||
--hash=sha256:eb36a08859b9432d94ea6b26ec41a2f98f83f14868c91321d0c1e11f672eeae7 \
|
||||
--hash=sha256:f24e8e17035200a465c178e9ea945527ad0738118694184c450f1192a452ff25 \
|
||||
--hash=sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0
|
||||
# via psycopg
|
||||
psycopg-pool==3.3.0 \
|
||||
--hash=sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063 \
|
||||
--hash=sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5
|
||||
# via psycopg
|
||||
pyasn1==0.6.2 \
|
||||
--hash=sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf \
|
||||
--hash=sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b
|
||||
pyasn1==0.6.3 \
|
||||
--hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \
|
||||
--hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# python-ldap
|
||||
|
|
@ -219,9 +218,9 @@ pyyaml==6.0.3 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# -r contrib/container/requirements.in
|
||||
setuptools==82.0.0 \
|
||||
--hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \
|
||||
--hash=sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0
|
||||
setuptools==82.0.1 \
|
||||
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
|
||||
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# -r contrib/container/requirements.in
|
||||
|
|
@ -237,26 +236,26 @@ typing-extensions==4.15.0 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# psycopg-pool
|
||||
uv==0.9.22 \
|
||||
--hash=sha256:012bdc5285a9cdb091ac514b7eb8a707e3b649af5355fe4afb4920bfe1958c00 \
|
||||
--hash=sha256:0cdc653fb601aa7f273242823fa93024f5fd319c66cdf22f36d784858493564c \
|
||||
--hash=sha256:0d8f007616cac5962620252b56a1d8224e9b2de566e78558efe04cc18526d507 \
|
||||
--hash=sha256:1f45e1e0f26dd47fa01eb421c54cfd39de10fd52ac0a9d7ae45b92fce7d92b0b \
|
||||
--hash=sha256:1f979c9d313b4616d9865859ef520bea5df0d4f15c57214589f5676fafa440c1 \
|
||||
--hash=sha256:2a4155cf7d0231d0adae94257ee10d70c57c2f592207536ddd55d924590a8c15 \
|
||||
--hash=sha256:3422b093b8e6e8de31261133b420c34dbef81f3fd1d82f787ac771b00b54adf8 \
|
||||
--hash=sha256:369b55341c6236f42d8fc335876308e5c57c921850975b3019cc9f7ebbe31567 \
|
||||
--hash=sha256:3b2bcce464186f8fafa3bf2aa5d82db4e3229366345399cc3f5bcafd616b8fe0 \
|
||||
--hash=sha256:41c73a4938818ede30e601cd0be87953e5c6a83dc4762e04e626f2eb9b240ebe \
|
||||
--hash=sha256:59c4f6b3659a68c26c50865432a7134386f607432160aad51e2247f862902697 \
|
||||
--hash=sha256:77ec4c101d41d7738226466191a7d62f9fa4de06ea580e0801da2f5cd5fa08aa \
|
||||
--hash=sha256:8f73043ade8ff6335e19fe1f4e7425d5e28aec9cafd72d13d5b40bb1cbb85690 \
|
||||
--hash=sha256:9c238525272506845fe07c0b9088c5e33fcd738e1f49ef49dc3c8112096d2e3a \
|
||||
--hash=sha256:b1985559b38663642658069e8d09fa6c30ed1c67654b7e5765240d9e4e9cdd57 \
|
||||
--hash=sha256:b78f2605d65c4925631d891dec99b677b05f50c774dedc6ef8968039a5bcfdb0 \
|
||||
--hash=sha256:b807bafe6b65fc1fe9c65ffd0d4228db894872de96e7200c44943f24beb68931 \
|
||||
--hash=sha256:d9d4be990bb92a68781f7c98d2321b528667b61d565c02ba978488c0210aa768 \
|
||||
--hash=sha256:e4b61a9c8b8dcbf64e642d2052342d36a46886b8bc3ccc407282962b970101af
|
||||
uv==0.10.12 \
|
||||
--hash=sha256:006812a086fce03d230fc987299f7295c7a73d17a1f1c17de1d1f327826f8481 \
|
||||
--hash=sha256:101481a1f48db6becf219914a591a588c0b3bfd05bef90768a5d04972bd6455e \
|
||||
--hash=sha256:2ace05115bd9ee1b30d341728257fe051817c4c0a652c085c90d4bd4fb0bc8f2 \
|
||||
--hash=sha256:2bb5893d79179727253e4a283871a693d7773c662a534fb897aa65496aa35765 \
|
||||
--hash=sha256:2c21e1b36c384f75dd3fd4a818b04871158ce115efff0bb4fdcd18ba2df7bd48 \
|
||||
--hash=sha256:2c5dfc7560453186e911c8c2e4ce95cd1c91e1c5926c3b34c5a825a307217be9 \
|
||||
--hash=sha256:384b7f36a1ae50efe5f50fe299f276a83bf7acc8b7147517f34e27103270f016 \
|
||||
--hash=sha256:551f799d53e397843b6cde7e3c61de716fb487da512a21a954b7d0cbc06967e0 \
|
||||
--hash=sha256:6727e3a0208059cd4d621684e580d5e254322dacbd806e0d218360abd0d48a68 \
|
||||
--hash=sha256:7099bdefffbe2df81accad52579657b8f9f870170caa779049c9fd82d645c9b3 \
|
||||
--hash=sha256:76ebe11572409dfbe20ec25a823f9bc8781400ece5356aa33ec44903af7ec316 \
|
||||
--hash=sha256:8dc352c93a47a4760cf824c31c55ce26511af780481e8f67c796d2779acaa928 \
|
||||
--hash=sha256:a5afe619e8a861fe4d49df8e10d2c6963de0dac6b79350c4832bf3366c8496cf \
|
||||
--hash=sha256:b9ca1d264059cb016c853ebbc4f21c72d983e0f347c927ca29e283aec2f596cf \
|
||||
--hash=sha256:bd84379292e3c1a1bf0a05847c7c72b66bb581dccf8da1ef94cc82bf517efa7c \
|
||||
--hash=sha256:be85acae8f31c68311505cd96202bad43165cbd7be110c59222f918677e93248 \
|
||||
--hash=sha256:cca36540d637c80d11d8a44a998a068355f0c78b75ec6b0f152ecbf89dfdd67b \
|
||||
--hash=sha256:e0f0ef58f0ba6fbfaf5f91b67aad6852252c49b8f78015a2a5800cf74c7538d5 \
|
||||
--hash=sha256:fa722691c7ae5c023778ad0b040ab8619367bcfe44fd0d9e05a58751af86cdf8
|
||||
# via -r contrib/container/requirements.in
|
||||
wheel==0.46.3 \
|
||||
--hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d \
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Packages needed for CI/packages
|
||||
requests==2.32.5
|
||||
requests==2.33.0
|
||||
pyyaml==6.0.3
|
||||
jc==1.25.6
|
||||
|
|
|
|||
|
|
@ -6,120 +6,136 @@ certifi==2026.2.25 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
charset-normalizer==3.4.4 \
|
||||
--hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \
|
||||
--hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \
|
||||
--hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \
|
||||
--hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \
|
||||
--hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \
|
||||
--hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \
|
||||
--hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \
|
||||
--hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \
|
||||
--hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \
|
||||
--hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \
|
||||
--hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \
|
||||
--hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \
|
||||
--hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \
|
||||
--hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \
|
||||
--hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \
|
||||
--hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \
|
||||
--hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \
|
||||
--hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \
|
||||
--hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \
|
||||
--hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \
|
||||
--hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \
|
||||
--hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \
|
||||
--hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \
|
||||
--hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \
|
||||
--hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \
|
||||
--hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \
|
||||
--hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \
|
||||
--hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \
|
||||
--hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \
|
||||
--hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \
|
||||
--hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \
|
||||
--hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \
|
||||
--hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \
|
||||
--hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \
|
||||
--hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \
|
||||
--hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \
|
||||
--hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \
|
||||
--hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \
|
||||
--hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \
|
||||
--hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \
|
||||
--hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \
|
||||
--hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \
|
||||
--hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \
|
||||
--hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \
|
||||
--hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \
|
||||
--hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \
|
||||
--hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \
|
||||
--hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \
|
||||
--hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \
|
||||
--hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \
|
||||
--hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \
|
||||
--hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \
|
||||
--hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \
|
||||
--hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \
|
||||
--hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \
|
||||
--hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \
|
||||
--hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \
|
||||
--hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \
|
||||
--hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \
|
||||
--hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \
|
||||
--hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \
|
||||
--hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \
|
||||
--hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \
|
||||
--hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \
|
||||
--hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \
|
||||
--hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \
|
||||
--hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \
|
||||
--hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \
|
||||
--hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \
|
||||
--hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \
|
||||
--hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \
|
||||
--hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \
|
||||
--hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \
|
||||
--hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \
|
||||
--hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \
|
||||
--hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \
|
||||
--hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \
|
||||
--hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \
|
||||
--hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \
|
||||
--hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \
|
||||
--hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \
|
||||
--hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \
|
||||
--hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \
|
||||
--hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \
|
||||
--hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \
|
||||
--hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \
|
||||
--hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \
|
||||
--hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \
|
||||
--hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \
|
||||
--hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \
|
||||
--hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \
|
||||
--hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \
|
||||
--hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \
|
||||
--hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \
|
||||
--hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \
|
||||
--hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \
|
||||
--hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \
|
||||
--hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \
|
||||
--hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \
|
||||
--hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \
|
||||
--hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \
|
||||
--hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \
|
||||
--hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \
|
||||
--hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \
|
||||
--hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \
|
||||
--hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \
|
||||
--hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \
|
||||
--hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \
|
||||
--hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \
|
||||
--hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \
|
||||
--hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \
|
||||
--hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \
|
||||
--hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608
|
||||
charset-normalizer==3.4.6 \
|
||||
--hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \
|
||||
--hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \
|
||||
--hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \
|
||||
--hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \
|
||||
--hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \
|
||||
--hash=sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0 \
|
||||
--hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \
|
||||
--hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \
|
||||
--hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \
|
||||
--hash=sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8 \
|
||||
--hash=sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264 \
|
||||
--hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \
|
||||
--hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \
|
||||
--hash=sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4 \
|
||||
--hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \
|
||||
--hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \
|
||||
--hash=sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa \
|
||||
--hash=sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95 \
|
||||
--hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \
|
||||
--hash=sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297 \
|
||||
--hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \
|
||||
--hash=sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e \
|
||||
--hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \
|
||||
--hash=sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8 \
|
||||
--hash=sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0 \
|
||||
--hash=sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9 \
|
||||
--hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \
|
||||
--hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \
|
||||
--hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \
|
||||
--hash=sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565 \
|
||||
--hash=sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7 \
|
||||
--hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \
|
||||
--hash=sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b \
|
||||
--hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \
|
||||
--hash=sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687 \
|
||||
--hash=sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9 \
|
||||
--hash=sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14 \
|
||||
--hash=sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89 \
|
||||
--hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \
|
||||
--hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \
|
||||
--hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \
|
||||
--hash=sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a \
|
||||
--hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \
|
||||
--hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \
|
||||
--hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \
|
||||
--hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \
|
||||
--hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \
|
||||
--hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \
|
||||
--hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \
|
||||
--hash=sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532 \
|
||||
--hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \
|
||||
--hash=sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae \
|
||||
--hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \
|
||||
--hash=sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64 \
|
||||
--hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \
|
||||
--hash=sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557 \
|
||||
--hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \
|
||||
--hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \
|
||||
--hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \
|
||||
--hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \
|
||||
--hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \
|
||||
--hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \
|
||||
--hash=sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597 \
|
||||
--hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \
|
||||
--hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \
|
||||
--hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \
|
||||
--hash=sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54 \
|
||||
--hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \
|
||||
--hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \
|
||||
--hash=sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4 \
|
||||
--hash=sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7 \
|
||||
--hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \
|
||||
--hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \
|
||||
--hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \
|
||||
--hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \
|
||||
--hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \
|
||||
--hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \
|
||||
--hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \
|
||||
--hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \
|
||||
--hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \
|
||||
--hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \
|
||||
--hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \
|
||||
--hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \
|
||||
--hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \
|
||||
--hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \
|
||||
--hash=sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6 \
|
||||
--hash=sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc \
|
||||
--hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \
|
||||
--hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \
|
||||
--hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \
|
||||
--hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \
|
||||
--hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \
|
||||
--hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \
|
||||
--hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \
|
||||
--hash=sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237 \
|
||||
--hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \
|
||||
--hash=sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778 \
|
||||
--hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \
|
||||
--hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \
|
||||
--hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \
|
||||
--hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \
|
||||
--hash=sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f \
|
||||
--hash=sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5 \
|
||||
--hash=sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611 \
|
||||
--hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \
|
||||
--hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \
|
||||
--hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \
|
||||
--hash=sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b \
|
||||
--hash=sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db \
|
||||
--hash=sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e \
|
||||
--hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \
|
||||
--hash=sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd \
|
||||
--hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \
|
||||
--hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \
|
||||
--hash=sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8 \
|
||||
--hash=sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe \
|
||||
--hash=sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058 \
|
||||
--hash=sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17 \
|
||||
--hash=sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833 \
|
||||
--hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \
|
||||
--hash=sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550 \
|
||||
--hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \
|
||||
--hash=sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2 \
|
||||
--hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \
|
||||
--hash=sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982 \
|
||||
--hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \
|
||||
--hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \
|
||||
--hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \
|
||||
--hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
|
|
@ -133,9 +149,9 @@ jc==1.25.6 \
|
|||
--hash=sha256:27f58befc7ae0a4c63322926c5f1ec892e3eac4a065eff3b07cfe420a6924a07 \
|
||||
--hash=sha256:7367b59e6e0da8babeede1e5b0da083f3c5aa6b6e585b4aed28dd7c4b2d76162
|
||||
# via -r contrib/dev_reqs/requirements.in
|
||||
pygments==2.19.2 \
|
||||
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
|
||||
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via jc
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
|
|
@ -214,9 +230,9 @@ pyyaml==6.0.3 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# -r contrib/dev_reqs/requirements.in
|
||||
requests==2.32.5 \
|
||||
--hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \
|
||||
--hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf
|
||||
requests==2.33.0 \
|
||||
--hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \
|
||||
--hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# -r contrib/dev_reqs/requirements.in
|
||||
|
|
@ -230,7 +246,7 @@ urllib3==2.6.3 \
|
|||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
xmltodict==1.0.2 \
|
||||
--hash=sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649 \
|
||||
--hash=sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d
|
||||
xmltodict==1.0.4 \
|
||||
--hash=sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61 \
|
||||
--hash=sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a
|
||||
# via jc
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ The *Part Settings* view allows you to configure various options governing what
|
|||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| Parameters | Enable display of part parameters in the part detail view |
|
||||
| Parameters | Enable display of parameters in the part detail view |
|
||||
| BOM | Enable bill of materials display in the part detail view |
|
||||
| Stock History | Enable display of stock history in the stock detail view |
|
||||
| Test Results | Enable display of test results in the stock detail view |
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 50 KiB |
|
|
@ -15,7 +15,7 @@ Parameters can be associated with various InvenTree models.
|
|||
|
||||
Any model which supports parameters will have a "Parameters" tab on its detail page. This tab displays all parameters associated with that object:
|
||||
|
||||
{{ image("concepts/parameter-tab.png", "Part Parameters Example") }}
|
||||
{{ image("concepts/parameter-tab.png", "Parameters Example") }}
|
||||
|
||||
## Parameter Templates
|
||||
|
||||
|
|
@ -40,9 +40,9 @@ Parameter templates are created and edited via the [admin interface](../settings
|
|||
To create a template:
|
||||
|
||||
- Navigate to the "Settings" page
|
||||
- Click on the "Part Parameters" tab
|
||||
- Click on the "Parameters" tab
|
||||
- Click on the "New Parameter" button
|
||||
- Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields
|
||||
- Fill out the `Create Parameter Template` form: `Name` (required) and `Units` (optional) fields
|
||||
- Click on the "Submit" button.
|
||||
|
||||
An existing template can be edited by clicking on the "Edit" button associated with that template:
|
||||
|
|
@ -53,9 +53,9 @@ An existing template can be edited by clicking on the "Edit" button associated w
|
|||
|
||||
After [creating a template](#create-template) or using the existing templates, you can add parameters to any part.
|
||||
|
||||
To add a parameter, navigate to a specific part detail page, click on the "Parameters" tab then click on the "New Parameters" button, the `Create Part Parameter` form will be displayed:
|
||||
To add a parameter, navigate to a specific part detail page, click on the "Parameters" tab then click on the "New Parameters" button, the `Create Parameter` form will be displayed:
|
||||
|
||||
{{ image("part/create_part_parameter.png", "Create Part Parameter Form") }}
|
||||
{{ image("part/create_part_parameter.png", "Create Parameter Form") }}
|
||||
|
||||
Select the parameter `Template` you would like to use for this parameter, fill-out the `Data` field (value of this specific parameter) and click the "Submit" button.
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ The in-built conversion functionality means that parameter values can be input i
|
|||
|
||||
### Incompatible Units
|
||||
|
||||
If a part parameter is created with a value which is incompatible with the units specified for the template, it will be rejected:
|
||||
If a parameter is created with a value which is incompatible with the units specified for the template, it will be rejected:
|
||||
|
||||
{{ image("part/part_invalid_units.png", "Invalid Parameter Units") }}
|
||||
|
||||
|
|
@ -151,4 +151,4 @@ Selection Lists can be used to add a large number of predefined values to a para
|
|||
It is possible that plugins lock selection lists to ensure a known state.
|
||||
|
||||
|
||||
Administration of lists can be done through the Part Parameter section in the [Admin Center](../settings/admin.md#admin-center) or via the API.
|
||||
Administration of lists can be done through the `Parameter` section in the [Admin Center](../settings/admin.md#admin-center) or via the API.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Deploying InvenTree to production requires to knowledge of the security assumpti
|
|||
|
||||
2. All users are trusted - therefore user uploaded files can be assumed to be safe. There are basic checks in place to ensure that the files are not using common attack vectors but those are not exhaustive.
|
||||
|
||||
3. Superuser permissions are only given to trusted users and not used for daily operations. A superuser account can manipulate or extract all files on the server that the InvenTree server process have access to.
|
||||
3. Superuser or staff permissions are only given to trusted users and not used for daily operations. A superuser account can manipulate or extract all files on the server that the InvenTree server process have access to. See [dangerous user flags](../settings/permissions.md#dangerous-user-flags) for more details on user permissions and flags.
|
||||
|
||||
4. All templates and plugins are trusted.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Support for real-world "physical" units of measure is implemented using the [pin
|
|||
|
||||
- Ensures consistent use of real units for your inventory management
|
||||
- Convert between compatible units of measure from suppliers
|
||||
- Enforce use of compatible units when creating part parameters
|
||||
- Enforce use of compatible units when creating parameters
|
||||
- Enable custom units as required
|
||||
|
||||
### Unit Conversion
|
||||
|
|
@ -61,7 +61,7 @@ The [supplier part](../part/index.md/#supplier-parts) model uses real-world unit
|
|||
|
||||
### Parameter
|
||||
|
||||
The [parameter template](../concepts/parameters.md#parameter-templates) model can specify units of measure, and part parameters can be specified against these templates with compatible units
|
||||
The [parameter template](../concepts/parameters.md#parameter-templates) model can specify units of measure, and parameters can be specified against these templates with compatible units
|
||||
|
||||
## Custom Units
|
||||
|
||||
|
|
|
|||
|
|
@ -224,6 +224,8 @@ Example: Creating a new part via the "Add Part" form:
|
|||
|
||||
{{ image("concepts/ui_form_add_part.png", "Add Part Button") }}
|
||||
|
||||
On several forms is displayed option "Keep form open" in bottom part of the form on left side of Submit button (option is visible on the screenshot above). When this switch is turned on, form window is not closed after submit and filled form data is not reset. This is useful for creating more entries at one time with similar properties (e.g. only different number in name).
|
||||
|
||||
### Data Editing
|
||||
|
||||
Example: Editing an existing purchase order via the "Edit Purchase Order" form:
|
||||
|
|
@ -280,11 +282,12 @@ Alternatively, the spotlight search can be opened using the keyboard shortcut `C
|
|||
|
||||
Users may opt to disable the spotlight search functionality if they do not find it useful or prefer not to use it. To disable the spotlight search, navigate to your [user settings](../settings/user.md) and locate the option to disable the spotlight feature. Once disabled, the spotlight search will no longer be accessible from the main menu or via keyboard shortcuts.
|
||||
|
||||
## Barcode Scanning
|
||||
## Copy Button
|
||||
|
||||
## Notifications
|
||||
Many fields within the InvenTree user interface include a "copy" button, which allows users to quickly copy the value of that field to their clipboard. This is particularly useful for fields that contain important identifiers, such as part numbers, stock item codes, or other relevant data that may need to be easily copied and pasted elsewhere.
|
||||
|
||||
## Customization
|
||||
!!! important "Secure Context"
|
||||
The "copy" button functionality relies on the browser's clipboard API, which may not be available in all contexts (e.g. if the user is accessing the InvenTree interface via a non-https connection, or through an embedded iframe or a non-standard browser). In such cases, the "copy" button may not function as intended.
|
||||
|
||||
## User Permissions
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,12 @@ If you only need a superuser, run the `superuser` task. It should prompt you for
|
|||
|
||||
#### Run background workers
|
||||
|
||||
If you need to process your queue with background workers, run the `worker` task. This is a foreground task which will execute in the terminal.
|
||||
If you need to process your queue with background workers, open a new terminal and run the `worker` task with `invoke worker`. This is a foreground task which will execute in the terminal.
|
||||
|
||||
If you are developing functions that will be executed by background workers there are a two debugging options.
|
||||
|
||||
- If the workers are started with the `worker` task you can add `print` or `logger` statements to the code and monitor the output in the terminal.
|
||||
- All tasks can be forced to run in the foreground worker by uncommenting the `--sync` and `--noreload` arguments under the `InvenTree Server - 3rd party` entry in `.vscode/launch.json`. With this setting you should not start a separate background worker, instead you start the `InvenTree Server - 3rd party` from the `Run and Debug` side panel. All task will now run in one single process and you can set breakpoints, inspect variables and single step also tasks that normally are offloaded to background workers. It should be noted that with this setting the GUI will be unresponsive while tasks are executed.
|
||||
|
||||
### Running InvenTree
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ In the example below, see that the *Wood Screw* line item is marked as consumabl
|
|||
|
||||
Further, in the [Build Order](./build.md) stock allocation table, we see that this line item cannot be allocated, as it is *consumable*.
|
||||
|
||||
### Optional BOM Line Items
|
||||
|
||||
If a BOM line item is marked as *optional*, this means that the part and quantity information is tracked in the BOM, but this line item is not required to be allocated to a [Build Order](./build.md). This may be useful for certain items which are not strictly required for the build process to be completed.
|
||||
|
||||
When completing a Build Order, the user can choose whether to include optional items in the build process or not. If optional items are included, they will be allocated to the Build Order as normal. If optional items are excluded, they will not be allocated to the Build Order, and the build process can be completed without them.
|
||||
|
||||
### Substitute BOM Line Items
|
||||
|
||||
Where alternative parts can be used when building an assembly, these parts are assigned as *Substitute* parts in the Bill of Materials. A particular line item may have multiple substitute parts assigned to it. When allocating stock to a [Build Order](./build.md), stock items associated with any of the substitute parts may be allocated against the particular line item.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ New parts can be created manually by selecting the *Create Part* option from the
|
|||
{{ image("part/part_create_form.png", "New part form") }}
|
||||
|
||||
|
||||
Fill out the required part parameters and then press *Submit* to create the new part. If there are any form errors, you must fix these before the form can be successfully submitted.
|
||||
Fill out the required attributes and then press *Submit* to create the new part. If there are any form errors, you must fix these before the form can be successfully submitted.
|
||||
|
||||
Once the form is completed, the browser window is redirected to the new part detail page.
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Parts can be locked to prevent them from being modified. This is useful for part
|
|||
|
||||
- Locked parts cannot be deleted
|
||||
- BOM items cannot be created, edited, or deleted when they are part of a locked assembly
|
||||
- Part parameters linked to a locked part cannot be created, edited or deleted
|
||||
- Parameters linked to a locked part cannot be created, edited or deleted
|
||||
|
||||
## Active Parts
|
||||
|
||||
|
|
|
|||
|
|
@ -27,24 +27,22 @@ When creating a new revision of a part, there are some restrictions which must b
|
|||
|
||||
* **Circular References**: A part cannot be a revision of itself. This would create a circular reference which is not allowed.
|
||||
* **Unique Revisions**: A part cannot have two revisions with the same revision number. Each revision (of a given part) must have a unique revision code.
|
||||
* **Revisions of Revisions**: A single part can have multiple revisions, but a revision cannot have its own revision. This restriction is in place to prevent overly complex part relationships.
|
||||
* **Template Revisions**: A part which is a [template part](./template.md) cannot have revisions. This is because the template part is used to create variants, and allowing revisions of templates would create disallowed relationship states in the database. However, variant parts are allowed to have revisions.
|
||||
* **Template References**: A part which is a revision of a variant part must point to the same template as the original part. This is to ensure that the revision is correctly linked to the original part.
|
||||
|
||||
## Revision Settings
|
||||
|
||||
The following options are available to control the behavior of part revisions.
|
||||
The following [global settings](../settings/global.md) are available to control the behavior of part revisions:
|
||||
|
||||
Note that these options can be changed in the InvenTree settings:
|
||||
| Name | Description | Default | Units |
|
||||
| ---- | ----------- | ------- | ----- |
|
||||
{{ globalsetting("PART_ENABLE_REVISION") }}
|
||||
{{ globalsetting("PART_REVISION_ASSEMBLY_ONLY") }}
|
||||
|
||||
{{ image("part/part_revision_settings.png", "Part revision settings") }}
|
||||
|
||||
* **Enable Revisions**: If this setting is enabled, parts can have revisions. If this setting is disabled, parts cannot have revisions.
|
||||
* **Assembly Revisions Only**: If this setting is enabled, only assembly parts can have revisions. This is useful if you only want to track revisions of assemblies, and not individual parts.
|
||||
|
||||
## Create a Revision
|
||||
|
||||
To create a new revision for a given part, navigate to the part detail page, and click on the "Revisions" tab.
|
||||
To create a new revision for a given part, navigate to the part detail page, and click on the part actions menu (three vertical dots on the top right of the page).
|
||||
|
||||
Select the "Duplicate Part" action, to create a new copy of the selected part. This will open the "Duplicate Part" form:
|
||||
|
||||
|
|
@ -67,4 +65,5 @@ When multiple revisions exist for a particular part, you can navigate between re
|
|||
|
||||
{{ image("part/part_revision_select.png", "Select part revision") }}
|
||||
|
||||
Note that this revision selector is only visible when multiple revisions exist for the part.
|
||||
!!! info "Revision Selector Visibility"
|
||||
Note that this revision selector is only visible when multiple revisions exist for the part.
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ Enter the package name into the form as shown below. You can add a path and a ve
|
|||
|
||||
{{ image("plugin/plugin_install_txt.png", "Plugin.txt file") }}
|
||||
|
||||
!!! info "Superuser Required"
|
||||
Only users with superuser privileges can manage plugins via the web interface.
|
||||
|
||||
#### Local Directory
|
||||
|
||||
Custom plugins can be placed in the `data/plugins/` directory, where they will be automatically discovered. This can be useful for developing and testing plugins, but can prove more difficult in production (e.g. when using Docker).
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ When a certain (server-side) event occurs, the background worker passes the even
|
|||
|
||||
{{ image("plugin/enable_events.png", "Enable event integration") }}
|
||||
|
||||
!!! info "Worker debugging"
|
||||
As the events are offloaded to a background worker debugging the `process_event()` function need some extra consideration. Please see the [Run background workers](../../develop/devcontainer.md#run-background-workers) section for further information.
|
||||
|
||||
## Events
|
||||
|
||||
Events are passed through using a string identifier, e.g. `build.completed`
|
||||
|
|
|
|||
|
|
@ -547,14 +547,18 @@ You can add asset images to the reports and labels by using the `{% raw %}{% ass
|
|||
|
||||
## Parameters
|
||||
|
||||
If you need to load a parameter value for a particular model instance, within the context of your template, you can use the `parameter` template tag:
|
||||
If you need to reference a parameter for a particular model instance, within the context of your template, you can use the `parameter` template tag:
|
||||
|
||||
### parameter
|
||||
|
||||
This returns a [Parameter](../concepts/parameters.md) object which contains the value of the parameter, as well as any associated metadata (e.g. units, description, etc).
|
||||
|
||||
::: report.templatetags.report.parameter
|
||||
options:
|
||||
show_docstring_description: false
|
||||
show_source: False
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
The following example assumes that you have a report or label which contains a valid [Part](../part/index.md) instance:
|
||||
|
||||
|
|
@ -580,6 +584,27 @@ A [Parameter](../concepts/parameters.md) has the following available attributes:
|
|||
| Units | The *units* of the parameter (e.g. "km") |
|
||||
| Template | A reference to a [ParameterTemplate](../concepts/parameters.md#parameter-templates) |
|
||||
|
||||
### parameter_value
|
||||
|
||||
To access just the value of a parameter, use the `parameter_value` template tag:
|
||||
|
||||
::: report.templatetags.report.parameter_value
|
||||
options:
|
||||
show_docstring_description: false
|
||||
show_source: False
|
||||
|
||||
#### Example
|
||||
|
||||
```
|
||||
{% raw %}
|
||||
{% load report %}
|
||||
|
||||
{% parameter_value part "length" backup_value="3"as length_value %}
|
||||
Part: {{ part.name }}<br>
|
||||
Length: {{ length_value }}
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
## Rendering Markdown
|
||||
|
||||
Some data fields (such as the *Notes* field available on many internal database models) support [markdown formatting](https://en.wikipedia.org/wiki/Markdown). To render markdown content in a custom report, there are template filters made available through the [django-markdownify](https://github.com/erwinmatijsen/django-markdownify) library. This library provides functionality for converting markdown content to HTML representation, allowing it to be then rendered to PDF by the InvenTree report generation pipeline.
|
||||
|
|
|
|||
|
|
@ -55,19 +55,12 @@ The admin interface allows *staff* users the ability to directly view / add / ed
|
|||
|
||||
#### Access Backend Admin Interface
|
||||
|
||||
To access the admin interface, select the "Admin" option from the drop-down user menu in the top-right corner of the screen.
|
||||
|
||||
|
||||
!!! info "Staff Only"
|
||||
Only users with staff access will be able to see the "Admin" option
|
||||
To directly access the admin interface, append /admin/ to the InvenTree site URL - e.g. http://localhost:8000/admin/.
|
||||
|
||||
An administration panel will be presented as shown below:
|
||||
|
||||
{{ image("admin/admin.png", "Admin panel") }}
|
||||
|
||||
!!! info "Admin URL"
|
||||
To directly access the admin interface, append /admin/ to the InvenTree site URL - e.g. http://localhost:8000/admin/
|
||||
|
||||
#### View Database Objects
|
||||
|
||||
Database objects can be listed and filtered directly. The image below shows an example of displaying existing part categories.
|
||||
|
|
|
|||
|
|
@ -210,6 +210,12 @@ The environment in which the backup was taken does not match the current environ
|
|||
|
||||
This warning will not prevent you from restoring the backup but it is recommended to ensure the mentioned issues are resolved before restoring the backup to prevent issues with the restored instance.
|
||||
|
||||
#### INVE-W14
|
||||
**Elevated privileges - Backend**
|
||||
|
||||
A user is logged in with elevated privileges. This might be a superuser or a administrator user. These types of users have elevated permissions and should not be used for regular usage.
|
||||
Use separate accounts for administrative tasks and regular usage to reduce risk. Make sure to review the [permission documentation](../settings/permissions.md#dangerous-user-flags).
|
||||
|
||||
|
||||
### INVE-I (InvenTree Information)
|
||||
Information — These are not errors but information messages. They might point out potential issues or just provide information.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ Configuration of basic server settings:
|
|||
{{ globalsetting("INVENTREE_INSTANCE_TITLE") }}
|
||||
{{ globalsetting("INVENTREE_INSTANCE_ID", default="Randomly generated value") }}
|
||||
{{ globalsetting("INVENTREE_ANNOUNCE_ID") }}
|
||||
{{ globalsetting("INVENTREE_SHOW_SUPERUSER_BANNER") }}
|
||||
{{ globalsetting("INVENTREE_SHOW_ADMIN_BANNER") }}
|
||||
{{ globalsetting("INVENTREE_RESTRICT_ABOUT") }}
|
||||
{{ globalsetting("DISPLAY_FULL_NAMES") }}
|
||||
{{ globalsetting("DISPLAY_PROFILE_INFO") }}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,17 @@ Within each role, there are four levels of available permissions:
|
|||
| **Add** | The *add* permission allows the user to add / create database records associated with the particular role |
|
||||
| **Delete** | The *delete* permission allows the user to delete / remove database records associated with the particular role |
|
||||
|
||||
## Dangerous User Flags
|
||||
|
||||
In addition to the above permissions, there are two special flags that can be assigned to a user:
|
||||
- **Staff** - A user with the *staff* flag is able to access the admin interface, and can trigger dangerous actions that might have a security impact such as changing parsable files on the server (templates / reports / plugins). Some of these actions require the *admin* role to be assigned as well.
|
||||
- **Superuser** - A user with the *superuser* flag is able to access and change all data and functions of InvenTree. A superuser can modify and access all data that the InvenTree installation / server has access to - including shell access on the server OS itself. This is a very powerful flag, and should be used with caution.
|
||||
|
||||
It is strongly recommended to register any users with staff / superuser flags with strong MFA methods to reduce the risk of unauthorized access. These accounts should be used with caution, and should not be used for day-to-day operations.
|
||||
|
||||
Practicing account tiering is strongly recommended.
|
||||
|
||||
|
||||
## Admin Interface Permissions
|
||||
|
||||
If a user does not have the required permissions to perform a certain action in the admin interface, those options not be displayed.
|
||||
|
|
|
|||
|
|
@ -185,7 +185,6 @@ Proxy configuration can be complex, and any configuration beyond the basic setup
|
|||
|
||||
Refer to the [proxy server documentation](./processes.md#proxy-server) for more information.
|
||||
|
||||
|
||||
## Admin Site
|
||||
|
||||
Django provides a powerful [administrator interface]({% include "django.html" %}/ref/contrib/admin/) which can be used to manage the InvenTree database. This interface is enabled by default, and available at the `/admin/` URL.
|
||||
|
|
@ -275,7 +274,7 @@ If running with a PostgreSQL database backend, the following additional options
|
|||
| INVENTREE_DB_TIMEOUT | database.timeout | Database connection timeout (s) | 2 |
|
||||
| INVENTREE_DB_TCP_KEEPALIVES | database.tcp_keepalives | TCP keepalive | 1 |
|
||||
| INVENTREE_DB_TCP_KEEPALIVES_IDLE | database.tcp_keepalives_idle | Idle TCP keepalive | 1 |
|
||||
| INVENTREE_DB_TCP_KEEPALIVES_INTERNAL | database.tcp_keepalives_internal | Internal TCP keepalive | 1|
|
||||
| INVENTREE_DB_TCP_KEEPALIVES_INTERVAL | database.tcp_keepalives_interval | TCP keepalive interval | 1|
|
||||
| INVENTREE_DB_TCP_KEEPALIVES_COUNT | database.tcp_keepalives_count | TCP keepalive count | 5 |
|
||||
| INVENTREE_DB_ISOLATION_SERIALIZABLE | database.serializable | Database isolation level configured to "serializable" | False |
|
||||
|
||||
|
|
@ -287,6 +286,18 @@ If running with a MySQL database backend, the following additional options are a
|
|||
| --- | --- | --- | --- |
|
||||
| INVENTREE_DB_ISOLATION_SERIALIZABLE | database.serializable | Database isolation level configured to "serializable" | False |
|
||||
|
||||
### SQLite Settings
|
||||
|
||||
!!! warning "SQLite Performance"
|
||||
SQLite is not recommended for production use, and should only be used for testing or development purposes. If you are using SQLite in production, you may want to adjust the following settings to improve performance.
|
||||
|
||||
If running with a SQLite database backend, the following additional options are available:
|
||||
|
||||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_DB_TIMEOUT | database.timeout | Database connection timeout (s) | 10 |
|
||||
| INVENTREE_DB_WAL_MODE | database.wal_mode | Enable Write-Ahead Logging (WAL) mode for SQLite databases | True |
|
||||
|
||||
## Caching
|
||||
|
||||
InvenTree can be configured to use [redis](https://redis.io) as a global cache backend.
|
||||
|
|
@ -401,14 +412,10 @@ It is also possible to use alternative storage backends for static and media fil
|
|||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_S3_ACCESS_KEY | storage.s3.access_key | Access key | *Not specified* |
|
||||
| INVENTREE_S3_SECRET_KEY | storage.s3.secret_key | Secret key |
|
||||
| *Not specified* |
|
||||
| INVENTREE_S3_BUCKET_NAME | storage.s3.bucket_name | Bucket name, required by most providers |
|
||||
| *Not specified* |
|
||||
| INVENTREE_S3_REGION_NAME | storage.s3.region_name | S3 region name |
|
||||
| *Not specified* |
|
||||
| INVENTREE_S3_ENDPOINT_URL | storage.s3.endpoint_url | Custom S3 endpoint URL, defaults to AWS endpoints if not set |
|
||||
| *Not specified* |
|
||||
| INVENTREE_S3_SECRET_KEY | storage.s3.secret_key | Secret key | *Not specified* |
|
||||
| INVENTREE_S3_BUCKET_NAME | storage.s3.bucket_name | Bucket name, required by most providers | *Not specified* |
|
||||
| INVENTREE_S3_REGION_NAME | storage.s3.region_name | S3 region name | *Not specified* |
|
||||
| INVENTREE_S3_ENDPOINT_URL | storage.s3.endpoint_url | Custom S3 endpoint URL, defaults to AWS endpoints if not set | *Not specified* |
|
||||
| INVENTREE_S3_LOCATION | storage.s3.location | Sub-Location that should be used | inventree-server |
|
||||
| INVENTREE_S3_DEFAULT_ACL | storage.s3.default_acl | Default ACL for uploaded files, defaults to provider default if not set | *Not specified* |
|
||||
| INVENTREE_S3_VERIFY_SSL | storage.s3.verify_ssl | Verify SSL certificate for S3 endpoint | True |
|
||||
|
|
@ -460,6 +467,17 @@ The login-experience can be altered with the following settings:
|
|||
|
||||
Custom authentication backends can be used by specifying them here. These can for example be used to add [LDAP / AD login](https://django-auth-ldap.readthedocs.io/en/latest/) to InvenTree
|
||||
|
||||
## Background Worker Options
|
||||
|
||||
The following options are available for configuring the InvenTree [background worker process](./processes.md#background-worker):
|
||||
|
||||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_BACKGROUND_WORKERS | background.workers | Number of background worker processes | 1 |
|
||||
| INVENTREE_BACKGROUND_TIMEOUT | background.timeout | Timeout for background worker tasks (seconds) | 90 |
|
||||
| INVENTREE_BACKGROUND_RETRY | background.retry | Time to wait before retrying a background task (seconds) | 300 |
|
||||
| INVENTREE_BACKGROUND_MAX_ATTEMPTS | background.max_attempts | Maximum number of attempts for a background task | 5 |
|
||||
|
||||
## Sentry Integration
|
||||
|
||||
The InvenTree server can be integrated with the [sentry.io](https://sentry.io) monitoring service, for error logging and performance tracking.
|
||||
|
|
@ -546,4 +564,4 @@ To override global settings, provide a "dictionary" of settings overrides in the
|
|||
|
||||
| Environment Variable | Configuration File | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| GLOBAL_SETTINGS_OVERRIDES | global_settings_overrides | JSON object containing global settings overrides | *Not specified* |
|
||||
| INVENTREE_GLOBAL_SETTINGS | global_settings | JSON object containing global settings overrides | *Not specified* |
|
||||
|
|
|
|||
|
|
@ -118,6 +118,9 @@ Extra python packages can be installed by setting the environment variable `SETU
|
|||
|
||||
The used database backend can be configured with environment variables (before the first setup) or in the config file after the installation. Check the [configuration section](./config.md#database-options) for more information.
|
||||
|
||||
!!! warning "SQLite Performance"
|
||||
SQLite is not recommended for production use, as it is not designed for high concurrency.
|
||||
|
||||
## Moving Data
|
||||
|
||||
To change the data storage location, link the new location to `/opt/inventree/data`. A rough outline of steps to achieve this could be:
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ invoke import-records -c -f data.json
|
|||
!!! warning "Character Encoding"
|
||||
If the character encoding of the data file does not exactly match the target database, the import operation may not succeed. In this case, some manual editing of the database JSON file may be required.
|
||||
|
||||
```
|
||||
{{ invoke_commands('import-records --help') }}
|
||||
```
|
||||
|
||||
### Copy Media Files
|
||||
|
||||
Any media files (images, documents, etc) that were stored in the original database must be copied to the new database. In a typical InvenTree installation, these files are stored in the `media` subdirectory of the InvenTree data location.
|
||||
|
|
@ -197,3 +201,32 @@ This will load the database records from the backup file into the new database.
|
|||
### Caveats
|
||||
|
||||
The process described here is a *suggested* procedure for migrating between incompatible database versions. However, due to the complexity of database software, there may be unforeseen complications that arise during the process.
|
||||
|
||||
## Migrating Plugin Data
|
||||
|
||||
Custom plugins may define their own database models, and thus have their own data records stored in the database. If a plugin is being migrated from one InvenTree installation to another, then the plugin data must also be migrated.
|
||||
|
||||
To account for this, the `export-records` and `import-records` commands have been designed to also export and import plugin data, in addition to the core InvenTree data.
|
||||
|
||||
### Exporting Plugin Data
|
||||
|
||||
When running the `export-records` command, any data records associated with plugins will also be exported, and included in the output JSON file.
|
||||
|
||||
### Importing Plugin Data
|
||||
|
||||
When running the `import-records` command, the import process will also attempt to import any plugin data records contained in the input JSON file. However, for the plugin data to be imported correctly, the following conditions must be met:
|
||||
|
||||
1. The plugin *code* must be present in the new InvenTree installation. Any plugins *not* installed will not have their tables created, and thus the import process will fail for those records.
|
||||
2. The plugin *version* must be the same in both installations. If the plugin version is different, then the database schema may be different, and thus the import process may fail.
|
||||
3. The InvenTree software version must be the same in both installations. If the InvenTree version is different, then the database schema may be different, and thus the import process may fail.
|
||||
|
||||
If all of the above conditions are met, then the plugin data *should* be imported correctly into the new database. To achieve this reliably, the following process steps are implemented in the `import-records` command:
|
||||
|
||||
1. The database is cleaned of all existing records (if the `-c` option is used).
|
||||
2. The core InvenTree database migrations are run to ensure that the core database schema is correct.
|
||||
3. User auth records are imported into the database
|
||||
4. Common configuration records (such as global settings) are imported into the database
|
||||
5. Plugin configuration records (defining which plugins are active) are imported into the database
|
||||
6. Database migrations are run once more, to ensure that any plugin database schema are correctly initialized
|
||||
7. The database is checked to ensure that all required apps are present (i.e. all plugins are installed and correctly activated)
|
||||
8. All remaining records (including plugin data) are imported into the database
|
||||
|
|
|
|||
|
|
@ -16,7 +16,22 @@ InvenTree supports a [number of database backends]({% include "django.html" %}/r
|
|||
|
||||
Refer to the [database configuration guide](./config.md#database-options) for more information on selecting and configuring the database backend.
|
||||
|
||||
In running InvenTree via [docker compose](./docker_install.md), the database process is managed by the `inventree-db` service which provides a [Postgres docker container](https://hub.docker.com/_/postgres).
|
||||
If running InvenTree via [docker compose](./docker_install.md), the database process is managed by the `inventree-db` service which provides a [Postgres docker container](https://hub.docker.com/_/postgres).
|
||||
|
||||
!!! tip "Postgres Recommended"
|
||||
We recommend using Postgres as the database backend for InvenTree, as it is a robust and scalable database which is well-suited to production use.
|
||||
|
||||
#### SQLite Limitations
|
||||
|
||||
!!! warning "SQLite Performance"
|
||||
SQLite is not recommended for production use, as it is not designed for high concurrency.
|
||||
|
||||
While SQLite is supported, it is strongly *not* recommended for a production installation, especially where there may be multiple users accessing the system concurrently. SQLite is designed for low-concurrency applications, and can experience performance issues when multiple users are accessing the database at the same time.
|
||||
|
||||
In addition to concurrency issues, there are other structural limitations which exist in SQLite that can prevent operations on large querysets.
|
||||
|
||||
If you are using SQLite, you should be aware of these limitations. It is important to ensure that the database file is stored on a fast storage medium (such as an SSD), and that the database options are configured correctly to minimize locking issues. Refer to the [database configuration guide](./config.md#database-options) for more information on configuring SQLite options.
|
||||
|
||||
|
||||
### Web Server
|
||||
|
||||
|
|
@ -112,6 +127,8 @@ If the background worker process is not running, InvenTree will not be able to p
|
|||
|
||||
If the [cache server](#cache-server) is not running, the background worker will be limited to running a single threaded worker. This is because the background worker uses the cache server to manage task locking, and without a global cache server to communicate between processes, concurrency issues can occur.
|
||||
|
||||
Additionally, if you are running SQLite as the database backend, the background worker will be limited to a single thread, due to database locking issues which can occur with SQLite when multiple threads are accessing the database concurrently.
|
||||
|
||||
### Cache Server
|
||||
|
||||
The InvenTree cache server is used to store temporary data which is shared between the InvenTree web server and the background worker processes. The cache server is also used to store task information, and to manage task locking between the background worker processes.
|
||||
|
|
@ -122,7 +139,11 @@ InvenTree uses the [Redis](https://redis.io/) cache server to manage cache data.
|
|||
|
||||
!!! info "Redis on Docker"
|
||||
Docker adds an additional network layer - that might lead to lower performance than bare metal.
|
||||
To optimize and configure your redis deployment follow the [official docker guide](https://redis.io/docs/getting-started/install-stack/docker/#configuration).
|
||||
To optimize and configure your redis deployment follow the [official docker guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/docker/).
|
||||
|
||||
!!! tip "Enable Cache"
|
||||
While a redis container is provided in the default configuration, by default it is not enabled in the InvenTree server. You can enable redis cache support by following the [caching configuration guide](./config.md#caching)
|
||||
|
||||
### Configuration
|
||||
|
||||
Refer to the [background worker configuration options](./config.md#background-worker-options) for more information on configuring the background worker process.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Each *Stock Item* is linked to the following information:
|
|||
|
||||
**Last Updated** - Date that the stock quantity was last updated
|
||||
|
||||
**Last Stocktake** - Date of most recent stocktake (count) of this item
|
||||
**Last Stocktake** - Date that this stock item was last counted
|
||||
|
||||
**Status** - Status of this stock item
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ babel==2.18.0 \
|
|||
# -c src/backend/requirements.txt
|
||||
# mkdocs-git-revision-date-localized-plugin
|
||||
# mkdocs-material
|
||||
backrefs==6.1 \
|
||||
--hash=sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853 \
|
||||
--hash=sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1 \
|
||||
--hash=sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231 \
|
||||
--hash=sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05 \
|
||||
--hash=sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0 \
|
||||
--hash=sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a \
|
||||
--hash=sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7
|
||||
backrefs==6.2 \
|
||||
--hash=sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be \
|
||||
--hash=sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8 \
|
||||
--hash=sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b \
|
||||
--hash=sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7 \
|
||||
--hash=sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90 \
|
||||
--hash=sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7 \
|
||||
--hash=sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49
|
||||
# via mkdocs-material
|
||||
beautifulsoup4==4.14.3 \
|
||||
--hash=sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb \
|
||||
|
|
@ -36,120 +36,136 @@ certifi==2026.2.25 \
|
|||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
charset-normalizer==3.4.4 \
|
||||
--hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \
|
||||
--hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \
|
||||
--hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \
|
||||
--hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \
|
||||
--hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \
|
||||
--hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \
|
||||
--hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \
|
||||
--hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \
|
||||
--hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \
|
||||
--hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \
|
||||
--hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \
|
||||
--hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \
|
||||
--hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \
|
||||
--hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \
|
||||
--hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \
|
||||
--hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \
|
||||
--hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \
|
||||
--hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \
|
||||
--hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \
|
||||
--hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \
|
||||
--hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \
|
||||
--hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \
|
||||
--hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \
|
||||
--hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \
|
||||
--hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \
|
||||
--hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \
|
||||
--hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \
|
||||
--hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \
|
||||
--hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \
|
||||
--hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \
|
||||
--hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \
|
||||
--hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \
|
||||
--hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \
|
||||
--hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \
|
||||
--hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \
|
||||
--hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \
|
||||
--hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \
|
||||
--hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \
|
||||
--hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \
|
||||
--hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \
|
||||
--hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \
|
||||
--hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \
|
||||
--hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \
|
||||
--hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \
|
||||
--hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \
|
||||
--hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \
|
||||
--hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \
|
||||
--hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \
|
||||
--hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \
|
||||
--hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \
|
||||
--hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \
|
||||
--hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \
|
||||
--hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \
|
||||
--hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \
|
||||
--hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \
|
||||
--hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \
|
||||
--hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \
|
||||
--hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \
|
||||
--hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \
|
||||
--hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \
|
||||
--hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \
|
||||
--hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \
|
||||
--hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \
|
||||
--hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \
|
||||
--hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \
|
||||
--hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \
|
||||
--hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \
|
||||
--hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \
|
||||
--hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \
|
||||
--hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \
|
||||
--hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \
|
||||
--hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \
|
||||
--hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \
|
||||
--hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \
|
||||
--hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \
|
||||
--hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \
|
||||
--hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \
|
||||
--hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \
|
||||
--hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \
|
||||
--hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \
|
||||
--hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \
|
||||
--hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \
|
||||
--hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \
|
||||
--hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \
|
||||
--hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \
|
||||
--hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \
|
||||
--hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \
|
||||
--hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \
|
||||
--hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \
|
||||
--hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \
|
||||
--hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \
|
||||
--hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \
|
||||
--hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \
|
||||
--hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \
|
||||
--hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \
|
||||
--hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \
|
||||
--hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \
|
||||
--hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \
|
||||
--hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \
|
||||
--hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \
|
||||
--hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \
|
||||
--hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \
|
||||
--hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \
|
||||
--hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \
|
||||
--hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \
|
||||
--hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \
|
||||
--hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \
|
||||
--hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \
|
||||
--hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \
|
||||
--hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \
|
||||
--hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \
|
||||
--hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \
|
||||
--hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608
|
||||
charset-normalizer==3.4.6 \
|
||||
--hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \
|
||||
--hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \
|
||||
--hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \
|
||||
--hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \
|
||||
--hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \
|
||||
--hash=sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0 \
|
||||
--hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \
|
||||
--hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \
|
||||
--hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \
|
||||
--hash=sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8 \
|
||||
--hash=sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264 \
|
||||
--hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \
|
||||
--hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \
|
||||
--hash=sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4 \
|
||||
--hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \
|
||||
--hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \
|
||||
--hash=sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa \
|
||||
--hash=sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95 \
|
||||
--hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \
|
||||
--hash=sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297 \
|
||||
--hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \
|
||||
--hash=sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e \
|
||||
--hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \
|
||||
--hash=sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8 \
|
||||
--hash=sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0 \
|
||||
--hash=sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9 \
|
||||
--hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \
|
||||
--hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \
|
||||
--hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \
|
||||
--hash=sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565 \
|
||||
--hash=sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7 \
|
||||
--hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \
|
||||
--hash=sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b \
|
||||
--hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \
|
||||
--hash=sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687 \
|
||||
--hash=sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9 \
|
||||
--hash=sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14 \
|
||||
--hash=sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89 \
|
||||
--hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \
|
||||
--hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \
|
||||
--hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \
|
||||
--hash=sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a \
|
||||
--hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \
|
||||
--hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \
|
||||
--hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \
|
||||
--hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \
|
||||
--hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \
|
||||
--hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \
|
||||
--hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \
|
||||
--hash=sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532 \
|
||||
--hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \
|
||||
--hash=sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae \
|
||||
--hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \
|
||||
--hash=sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64 \
|
||||
--hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \
|
||||
--hash=sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557 \
|
||||
--hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \
|
||||
--hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \
|
||||
--hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \
|
||||
--hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \
|
||||
--hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \
|
||||
--hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \
|
||||
--hash=sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597 \
|
||||
--hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \
|
||||
--hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \
|
||||
--hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \
|
||||
--hash=sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54 \
|
||||
--hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \
|
||||
--hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \
|
||||
--hash=sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4 \
|
||||
--hash=sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7 \
|
||||
--hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \
|
||||
--hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \
|
||||
--hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \
|
||||
--hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \
|
||||
--hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \
|
||||
--hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \
|
||||
--hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \
|
||||
--hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \
|
||||
--hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \
|
||||
--hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \
|
||||
--hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \
|
||||
--hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \
|
||||
--hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \
|
||||
--hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \
|
||||
--hash=sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6 \
|
||||
--hash=sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc \
|
||||
--hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \
|
||||
--hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \
|
||||
--hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \
|
||||
--hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \
|
||||
--hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \
|
||||
--hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \
|
||||
--hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \
|
||||
--hash=sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237 \
|
||||
--hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \
|
||||
--hash=sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778 \
|
||||
--hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \
|
||||
--hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \
|
||||
--hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \
|
||||
--hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \
|
||||
--hash=sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f \
|
||||
--hash=sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5 \
|
||||
--hash=sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611 \
|
||||
--hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \
|
||||
--hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \
|
||||
--hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \
|
||||
--hash=sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b \
|
||||
--hash=sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db \
|
||||
--hash=sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e \
|
||||
--hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \
|
||||
--hash=sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd \
|
||||
--hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \
|
||||
--hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \
|
||||
--hash=sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8 \
|
||||
--hash=sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe \
|
||||
--hash=sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058 \
|
||||
--hash=sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17 \
|
||||
--hash=sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833 \
|
||||
--hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \
|
||||
--hash=sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550 \
|
||||
--hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \
|
||||
--hash=sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2 \
|
||||
--hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \
|
||||
--hash=sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982 \
|
||||
--hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \
|
||||
--hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \
|
||||
--hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \
|
||||
--hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# requests
|
||||
|
|
@ -162,9 +178,7 @@ click==8.3.1 \
|
|||
colorama==0.4.6 \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via
|
||||
# griffe
|
||||
# mkdocs-material
|
||||
# via mkdocs-material
|
||||
editorconfig==0.17.1 \
|
||||
--hash=sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82 \
|
||||
--hash=sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745
|
||||
|
|
@ -173,9 +187,9 @@ essentials==1.1.9 \
|
|||
--hash=sha256:71ef161e0e27ef77cd6f5fc05e0b8688a575fcab870c01c95940f832e321dfbb \
|
||||
--hash=sha256:7fbea3a518cbeafe5374fb7e2ea2c15a109e8a7fd1eaab62ae87cbd1b3b1e8d0
|
||||
# via essentials-openapi
|
||||
essentials-openapi==1.3.0 \
|
||||
--hash=sha256:453327a0a847a431133f4472ced7e4a9180bf667437049b57381ddf88079e886 \
|
||||
--hash=sha256:9c2a88531e2c70c565d5b526d74043941e46f60c114f7a0e3ae91e9e6bef4dae
|
||||
essentials-openapi==1.4.0 \
|
||||
--hash=sha256:578c81501ccf6d18c0839d60636214fbd051f0ef37f1d207d4e3c92de2aac008 \
|
||||
--hash=sha256:86d879c32734248ad52482a90ee89a32883bce348b4edd323c01556b505b45b9
|
||||
# via neoteroi-mkdocs
|
||||
ghp-import==2.1.0 \
|
||||
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
|
||||
|
|
@ -189,9 +203,8 @@ gitpython==3.1.46 \
|
|||
--hash=sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f \
|
||||
--hash=sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058
|
||||
# via mkdocs-git-revision-date-localized-plugin
|
||||
griffe==1.15.0 \
|
||||
--hash=sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3 \
|
||||
--hash=sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea
|
||||
griffelib==2.0.0 \
|
||||
--hash=sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f
|
||||
# via mkdocstrings-python
|
||||
h11==0.16.0 \
|
||||
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
||||
|
|
@ -369,15 +382,15 @@ mkdocs==1.6.1 \
|
|||
# mkdocs-simple-hooks
|
||||
# mkdocstrings
|
||||
# neoteroi-mkdocs
|
||||
mkdocs-autorefs==1.4.3 \
|
||||
--hash=sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9 \
|
||||
--hash=sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75
|
||||
mkdocs-autorefs==1.4.4 \
|
||||
--hash=sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089 \
|
||||
--hash=sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197
|
||||
# via
|
||||
# mkdocstrings
|
||||
# mkdocstrings-python
|
||||
mkdocs-get-deps==0.2.0 \
|
||||
--hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \
|
||||
--hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134
|
||||
mkdocs-get-deps==0.2.2 \
|
||||
--hash=sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1 \
|
||||
--hash=sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650
|
||||
# via mkdocs
|
||||
mkdocs-git-revision-date-localized-plugin==1.5.1 \
|
||||
--hash=sha256:2b0239455cd84784dd87ac8dfc9253fe4b2dd35e102696f21b5d34e2175981c6 \
|
||||
|
|
@ -391,9 +404,9 @@ mkdocs-macros-plugin==1.5.0 \
|
|||
--hash=sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f \
|
||||
--hash=sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material==9.7.3 \
|
||||
--hash=sha256:37ebf7b4788c992203faf2e71900be3c197c70a4be9b0d72aed537b08a91dd9d \
|
||||
--hash=sha256:e5f0a18319699da7e78c35e4a8df7e93537a888660f61a86bd773a7134798f22
|
||||
mkdocs-material==9.7.6 \
|
||||
--hash=sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69 \
|
||||
--hash=sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material-extensions==1.3.1 \
|
||||
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
|
||||
|
|
@ -417,9 +430,9 @@ mkdocstrings[python]==1.0.3 \
|
|||
# via
|
||||
# -r docs/requirements.in
|
||||
# mkdocstrings-python
|
||||
mkdocstrings-python==2.0.1 \
|
||||
--hash=sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90 \
|
||||
--hash=sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732
|
||||
mkdocstrings-python==2.0.3 \
|
||||
--hash=sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12 \
|
||||
--hash=sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8
|
||||
# via mkdocstrings
|
||||
neoteroi-mkdocs==1.2.0 \
|
||||
--hash=sha256:58e25cb1b9db093ffa8d12bdb33264bf567cac30fb964b56e0a493efa749ad6e \
|
||||
|
|
@ -436,27 +449,27 @@ paginate==0.5.7 \
|
|||
--hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \
|
||||
--hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591
|
||||
# via mkdocs-material
|
||||
pathspec==1.0.1 \
|
||||
--hash=sha256:8870061f22c58e6d83463cfce9a7dd6eca0512c772c1001fb09ac64091816721 \
|
||||
--hash=sha256:e2769b508d0dd47b09af6ee2c75b2744a2cb1f474ae4b1494fd6a1b7a841613c
|
||||
pathspec==1.0.4 \
|
||||
--hash=sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645 \
|
||||
--hash=sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723
|
||||
# via
|
||||
# mkdocs
|
||||
# mkdocs-macros-plugin
|
||||
platformdirs==4.9.2 \
|
||||
--hash=sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd \
|
||||
--hash=sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291
|
||||
platformdirs==4.9.4 \
|
||||
--hash=sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934 \
|
||||
--hash=sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# mkdocs-get-deps
|
||||
pygments==2.19.2 \
|
||||
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
|
||||
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via
|
||||
# mkdocs-material
|
||||
# rich
|
||||
pymdown-extensions==10.20 \
|
||||
--hash=sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52 \
|
||||
--hash=sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f
|
||||
pymdown-extensions==10.21.2 \
|
||||
--hash=sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638 \
|
||||
--hash=sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc
|
||||
# via
|
||||
# mkdocs-material
|
||||
# mkdocs-mermaid2-plugin
|
||||
|
|
@ -554,9 +567,9 @@ pyyaml-env-tag==1.1 \
|
|||
--hash=sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 \
|
||||
--hash=sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff
|
||||
# via mkdocs
|
||||
requests==2.32.5 \
|
||||
--hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \
|
||||
--hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf
|
||||
requests==2.33.0 \
|
||||
--hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \
|
||||
--hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# mkdocs-macros-plugin
|
||||
|
|
@ -566,9 +579,9 @@ rich==14.3.3 \
|
|||
--hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \
|
||||
--hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b
|
||||
# via neoteroi-mkdocs
|
||||
setuptools==82.0.0 \
|
||||
--hash=sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb \
|
||||
--hash=sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0
|
||||
setuptools==82.0.1 \
|
||||
--hash=sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9 \
|
||||
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# mkdocs-mermaid2-plugin
|
||||
|
|
@ -579,13 +592,13 @@ six==1.17.0 \
|
|||
# -c src/backend/requirements.txt
|
||||
# jsbeautifier
|
||||
# python-dateutil
|
||||
smmap==5.0.2 \
|
||||
--hash=sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5 \
|
||||
--hash=sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e
|
||||
smmap==5.0.3 \
|
||||
--hash=sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c \
|
||||
--hash=sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f
|
||||
# via gitdb
|
||||
soupsieve==2.8.1 \
|
||||
--hash=sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350 \
|
||||
--hash=sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434
|
||||
soupsieve==2.8.3 \
|
||||
--hash=sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349 \
|
||||
--hash=sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95
|
||||
# via beautifulsoup4
|
||||
super-collections==0.6.2 \
|
||||
--hash=sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a \
|
||||
|
|
|
|||
|
|
@ -108,11 +108,12 @@ root = ["src/backend/InvenTree"]
|
|||
unresolved-reference="ignore" # 21 # see https://github.com/astral-sh/ty/issues/220
|
||||
unresolved-attribute="ignore" # 505 # need Plugin Mixin typing
|
||||
call-non-callable="ignore" # 8 ##
|
||||
invalid-assignment="ignore" # 17 # need to wait for better django field stubs
|
||||
invalid-method-override="ignore" # 99
|
||||
invalid-return-type="ignore" # 22 ##
|
||||
possibly-missing-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164
|
||||
unknown-argument="ignore" # 3 # need to wait for better django field stubs
|
||||
invalid-argument-type="ignore" # 49
|
||||
possibly-unbound-attribute="ignore" # 25 # https://github.com/astral-sh/ty/issues/164
|
||||
unknown-argument="ignore" # 3 # need to wait for betterdjango field stubs
|
||||
invalid-assignment="ignore" # 17 # need to wait for betterdjango field stubs
|
||||
no-matching-overload="ignore" # 3 # need to wait for betterdjango field stubs
|
||||
|
||||
[tool.coverage.run]
|
||||
|
|
@ -139,3 +140,4 @@ django_find_project = false
|
|||
pythonpath = ["src/backend/InvenTree"]
|
||||
DJANGO_SETTINGS_MODULE = "InvenTree.settings"
|
||||
python_files = ["test*.py",]
|
||||
timeout = "120"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Files generated during unit testing
|
||||
_testfolder/
|
||||
_tests_report*.txt
|
||||
|
||||
# Playwright files for CI
|
||||
InvenTree/static/img/playwright*.png
|
||||
|
|
|
|||
|
|
@ -423,23 +423,19 @@ class BulkOperationMixin:
|
|||
def get_bulk_queryset(self, request):
|
||||
"""Return a queryset based on the selection made in the request.
|
||||
|
||||
Selection can be made by providing either:
|
||||
|
||||
- items: A list of primary key values
|
||||
- filters: A dictionary of filter values
|
||||
Selection can be made by providing a list of primary key values,
|
||||
which will be used to filter the queryset.
|
||||
"""
|
||||
model = self.serializer_class.Meta.model
|
||||
|
||||
items = request.data.pop('items', None)
|
||||
filters = request.data.pop('filters', None)
|
||||
all_filter = request.GET.get('all', None)
|
||||
|
||||
queryset = model.objects.all()
|
||||
# Return the base queryset for this model
|
||||
queryset = self.get_queryset()
|
||||
|
||||
if not items and not filters and all_filter is None:
|
||||
if not items and all_filter is None:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _(
|
||||
'List of items or filters must be provided for bulk operation'
|
||||
'List of items must be provided for bulk operation'
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -457,19 +453,6 @@ class BulkOperationMixin:
|
|||
'non_field_errors': _('Invalid items list provided')
|
||||
})
|
||||
|
||||
if filters:
|
||||
if type(filters) is not dict:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('Filters must be provided as a dict')
|
||||
})
|
||||
|
||||
try:
|
||||
queryset = queryset.filter(**filters)
|
||||
except Exception:
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('Invalid filters provided')
|
||||
})
|
||||
|
||||
if all_filter and not helpers.str2bool(all_filter):
|
||||
raise ValidationError({
|
||||
'non_field_errors': _('All filter must only be used with true')
|
||||
|
|
@ -507,7 +490,7 @@ class BulkCreateMixin:
|
|||
if unique_create_fields := getattr(self, 'unique_create_fields', None):
|
||||
existing = collections.defaultdict(list)
|
||||
for idx, item in enumerate(data):
|
||||
key = tuple(item[v] for v in unique_create_fields)
|
||||
key = tuple(item[v] for v in list(unique_create_fields)) # type: ignore[not-subscriptable]
|
||||
existing[key].append(idx)
|
||||
|
||||
unique_errors = [[] for _ in range(len(data))]
|
||||
|
|
@ -594,17 +577,24 @@ class BulkUpdateMixin(BulkOperationMixin):
|
|||
|
||||
n = queryset.count()
|
||||
|
||||
instance_data = []
|
||||
|
||||
with transaction.atomic():
|
||||
# Perform object update
|
||||
# Note that we do not perform a bulk-update operation here,
|
||||
# as we want to trigger any custom post_save methods on the model
|
||||
|
||||
# Run validation first
|
||||
for instance in queryset:
|
||||
serializer = self.get_serializer(instance, data=data, partial=True)
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response({'success': f'Updated {n} items'}, status=200)
|
||||
instance_data.append(serializer.data)
|
||||
|
||||
return Response(
|
||||
{'success': f'Updated {n} items', 'items': instance_data}, status=200
|
||||
)
|
||||
|
||||
|
||||
class ParameterListMixin:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,59 @@
|
|||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 461
|
||||
INVENTREE_API_VERSION = 476
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v476 -> 2026-04-09 : https://github.com/inventree/InvenTree/pull/11705
|
||||
- Adds sorting / filtering / searching functionality to the SelectionListEntry API endpoint
|
||||
|
||||
v475 -> 2026-04-09 : https://github.com/inventree/InvenTree/pull/11702
|
||||
- Adds "updated" and "updated_by" fields to the LabelTemplate and ReportTemplate API endpoints
|
||||
|
||||
v474 -> 2026-04-08 : https://github.com/inventree/InvenTree/pull/11693
|
||||
- Adds DataImportMixin to the ManufacturerPartList API endpoint
|
||||
|
||||
v473 -> 2026-04-08 : https://github.com/inventree/InvenTree/pull/11692
|
||||
- Adds "line" field to PurchaseOrderLineItem and PurchaseOrderExtraLineItem API endpoints
|
||||
- Adds "line" field to SalesOrderLineItem and SalesOrderExtraLineItem API endpoints
|
||||
- Adds "line" field to ReturnOrderLineItem and ReturnOrderExtraLineItem API endpoints
|
||||
|
||||
v472 -> 2026-04-01 : https://github.com/inventree/InvenTree/pull/xxxx
|
||||
- Fixes writable fields on the user detail endpoint
|
||||
|
||||
v471 -> 2026-04-07 : https://github.com/inventree/InvenTree/pull/11685
|
||||
- Adds data importer support for the "SalesOrderShipment" model
|
||||
|
||||
v470 -> 2026-04-01 : https://github.com/inventree/InvenTree/pull/11659
|
||||
- Renames "is_staff" field to "is_admin" and updates help texts accordingly to highlight current security boundaries
|
||||
|
||||
v469 -> 2026-03-31 : https://github.com/inventree/InvenTree/pull/11641
|
||||
- Adds parameter support to the SalesOrderShipment model and API endpoints
|
||||
|
||||
v468 -> 2026-03-31 : https://github.com/inventree/InvenTree/pull/11649
|
||||
- Add ordering to contentype related fields - no functional changes
|
||||
|
||||
v467 -> 2026-03-20 : https://github.com/inventree/InvenTree/pull/11573
|
||||
- Fix definition for the "parent" field on the StockItemSerializer
|
||||
|
||||
v466 -> 2026-03-17 : https://github.com/inventree/InvenTree/pull/11525
|
||||
- SalesOrderShipmentComplete endpoint now returns a task ID which can be used to track the progress of the shipment completion process
|
||||
|
||||
v465 -> 2026-03-16 : https://github.com/inventree/InvenTree/pull/11529/
|
||||
- BuildOrderAutoAllocate endpoint now returns a task ID which can be used to track the progress of the auto-allocation process
|
||||
- BuildOrderConsume endpoint now returns a task ID which can be used to track the progress of the stock consumption process
|
||||
|
||||
v464 -> 2026-03-15 : https://github.com/inventree/InvenTree/pull/11527
|
||||
- Add API endpoint for monitoring the progress of a particular background task
|
||||
|
||||
v463 -> 2026-03-12 : https://github.com/inventree/InvenTree/pull/11499
|
||||
- Allow "bulk update" actions against StockItem endpoint
|
||||
|
||||
v462 -> 2026-03-12 : https://github.com/inventree/InvenTree/pull/11497
|
||||
- Allow "ScheduledTask" API endpoint to be filtered by "name" field
|
||||
|
||||
v461 -> 2026-03-10 : https://github.com/inventree/InvenTree/pull/11479
|
||||
- Adds option to copy parameters when duplicating an order via the API
|
||||
|
||||
|
|
@ -304,12 +352,6 @@ v381 -> 2025-08-06 : https://github.com/inventree/InvenTree/pull/10132
|
|||
v380 -> 2025-08-06 : https://github.com/inventree/InvenTree/pull/10135
|
||||
- Fixes "issued_by" filter for the BuildOrder list API endpoint
|
||||
|
||||
v380 -> 2025-08-06 : https://github.com/inventree/InvenTree/pull/10132
|
||||
- Refactor the "return stock item" API endpoint to align with other stock adjustment actions
|
||||
|
||||
v380 -> 2025-08-06 : https://github.com/inventree/InvenTree/pull/10135
|
||||
- Fixes "issued_by" filter for the BuildOrder list API endpoint
|
||||
|
||||
v379 -> 2025-08-04 : https://github.com/inventree/InvenTree/pull/10124
|
||||
- Removes "PartStocktakeReport" model and associated API endpoints
|
||||
- Remove "last_stocktake" field from the Part model
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ def load_config_data(set_cache: bool = False) -> map | None:
|
|||
if CONFIG_DATA is not None and not set_cache:
|
||||
return CONFIG_DATA
|
||||
|
||||
import yaml
|
||||
import yaml.parser
|
||||
|
||||
cfg_file = get_config_file()
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ def log_error(
|
|||
data = error_data
|
||||
else:
|
||||
try:
|
||||
formatted_exception = traceback.format_exception(kind, info, data) # type: ignore[no-matching-overload]
|
||||
formatted_exception = traceback.format_exception(kind, info, data)
|
||||
data = '\n'.join(formatted_exception)
|
||||
except AttributeError:
|
||||
data = 'No traceback information available'
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import json
|
|||
import os.path
|
||||
import re
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
from typing import Optional, TypeVar
|
||||
from wsgiref.util import FileWrapper
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
|
@ -21,19 +22,19 @@ from django.http import StreamingHttpResponse
|
|||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import bleach
|
||||
import bleach.css_sanitizer
|
||||
import bleach.sanitizer
|
||||
import nh3
|
||||
import structlog
|
||||
from bleach import clean
|
||||
from djmoney.money import Money
|
||||
from PIL import Image
|
||||
from stdimage.models import StdImageField, StdImageFieldFile
|
||||
|
||||
from common.currency import currency_code_default
|
||||
|
||||
from .setting.storages import StorageBackends
|
||||
from .settings import MEDIA_URL, STATIC_URL
|
||||
from InvenTree.sanitizer import (
|
||||
DEAFAULT_ATTRS,
|
||||
DEFAULT_CSS,
|
||||
DEFAULT_PROTOCOLS,
|
||||
DEFAULT_TAGS,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
|
@ -186,9 +187,8 @@ def getMediaUrl(
|
|||
)
|
||||
if name is not None:
|
||||
file = regenerate_imagefile(file, name)
|
||||
if settings.STORAGE_TARGET == StorageBackends.S3:
|
||||
return str(file.url)
|
||||
return os.path.join(MEDIA_URL, str(file.url))
|
||||
|
||||
return default_storage.url(file.name)
|
||||
|
||||
|
||||
def regenerate_imagefile(_file, _name: str):
|
||||
|
|
@ -199,7 +199,7 @@ def regenerate_imagefile(_file, _name: str):
|
|||
_name: Name of the variation (e.g. 'thumbnail', 'preview')
|
||||
"""
|
||||
name = _file.field.attr_class.get_variation_name(_file.name, _name)
|
||||
return ImageFieldFile(_file.instance, _file, name) # type: ignore
|
||||
return ImageFieldFile(_file.instance, _file, name) # ty:ignore[too-many-positional-arguments]
|
||||
|
||||
|
||||
def image2name(img_obj: StdImageField, do_preview: bool, do_thumbnail: bool):
|
||||
|
|
@ -226,11 +226,18 @@ def image2name(img_obj: StdImageField, do_preview: bool, do_thumbnail: bool):
|
|||
|
||||
def getStaticUrl(filename):
|
||||
"""Return the qualified access path for the given file, under the static media directory."""
|
||||
return os.path.join(STATIC_URL, str(filename))
|
||||
return StaticFilesStorage().url(filename)
|
||||
|
||||
|
||||
def TestIfImage(img):
|
||||
"""Test if an image file is indeed an image."""
|
||||
def TestIfImage(img) -> bool:
|
||||
"""Test if an image file is indeed an image.
|
||||
|
||||
Arguments:
|
||||
img: A file-like object
|
||||
|
||||
Returns:
|
||||
True if the file is a valid image, False otherwise
|
||||
"""
|
||||
try:
|
||||
Image.open(img).verify()
|
||||
return True
|
||||
|
|
@ -248,6 +255,13 @@ def getBlankThumbnail():
|
|||
return getStaticUrl('img/blank_image.thumbnail.png')
|
||||
|
||||
|
||||
def checkStaticFile(*args) -> bool:
|
||||
"""Check if a file exists in the static storage."""
|
||||
static_storage = StaticFilesStorage()
|
||||
fn = Path(*args)
|
||||
return static_storage.exists(str(fn))
|
||||
|
||||
|
||||
def getLogoImage(as_file=False, custom=True):
|
||||
"""Return the InvenTree logo image, or a custom logo if available."""
|
||||
if custom and settings.CUSTOM_LOGO:
|
||||
|
|
@ -311,7 +325,7 @@ def TestIfImageURL(url):
|
|||
]
|
||||
|
||||
|
||||
def str2bool(text, test=True):
|
||||
def str2bool(text, test=True) -> bool:
|
||||
"""Test if a string 'looks' like a boolean value.
|
||||
|
||||
Args:
|
||||
|
|
@ -881,13 +895,13 @@ def clean_decimal(number):
|
|||
|
||||
|
||||
def strip_html_tags(value: str, raise_error=True, field_name=None):
|
||||
"""Strip HTML tags from an input string using the bleach library.
|
||||
"""Strip HTML tags from an input string using the nh3 library.
|
||||
|
||||
If raise_error is True, a ValidationError will be thrown if HTML tags are detected
|
||||
"""
|
||||
value = str(value).strip()
|
||||
|
||||
cleaned = clean(value, strip=True, tags=[], attributes=[])
|
||||
cleaned = nh3.clean(value, tags=frozenset())
|
||||
|
||||
# Add escaped characters back in
|
||||
replacements = {'>': '>', '<': '<', '&': '&'}
|
||||
|
|
@ -947,34 +961,32 @@ def clean_markdown(value: str) -> str:
|
|||
output_format='html',
|
||||
)
|
||||
|
||||
# Bleach settings
|
||||
whitelist_tags = markdownify_settings.get(
|
||||
'WHITELIST_TAGS', bleach.sanitizer.ALLOWED_TAGS
|
||||
)
|
||||
whitelist_attrs = markdownify_settings.get(
|
||||
'WHITELIST_ATTRS', bleach.sanitizer.ALLOWED_ATTRIBUTES
|
||||
)
|
||||
whitelist_styles = markdownify_settings.get(
|
||||
'WHITELIST_STYLES', bleach.css_sanitizer.ALLOWED_CSS_PROPERTIES
|
||||
)
|
||||
# nh3 sanitizer settings
|
||||
whitelist_tags = markdownify_settings.get('WHITELIST_TAGS', DEFAULT_TAGS)
|
||||
whitelist_attrs = markdownify_settings.get('WHITELIST_ATTRS', DEAFAULT_ATTRS)
|
||||
whitelist_styles = markdownify_settings.get('WHITELIST_STYLES', DEFAULT_CSS)
|
||||
whitelist_protocols = markdownify_settings.get(
|
||||
'WHITELIST_PROTOCOLS', bleach.sanitizer.ALLOWED_PROTOCOLS
|
||||
'WHITELIST_PROTOCOLS', DEFAULT_PROTOCOLS
|
||||
)
|
||||
strip = markdownify_settings.get('STRIP', True)
|
||||
|
||||
css_sanitizer = bleach.css_sanitizer.CSSSanitizer(
|
||||
allowed_css_properties=whitelist_styles
|
||||
)
|
||||
cleaner = bleach.Cleaner(
|
||||
tags=whitelist_tags,
|
||||
attributes=whitelist_attrs,
|
||||
css_sanitizer=css_sanitizer,
|
||||
protocols=whitelist_protocols,
|
||||
strip=strip,
|
||||
)
|
||||
# Convert bleach-style attributes (list or dict) to nh3-compatible dict format
|
||||
if isinstance(whitelist_attrs, (list, tuple, set, frozenset)):
|
||||
attrs_dict = {'*': set(whitelist_attrs)}
|
||||
elif isinstance(whitelist_attrs, dict):
|
||||
attrs_dict = {tag: set(allowed) for tag, allowed in whitelist_attrs.items()}
|
||||
else:
|
||||
attrs_dict = None
|
||||
|
||||
# Clean the HTML content (for comparison). This must be the same as the original content
|
||||
clean_html = cleaner.clean(html)
|
||||
clean_html = nh3.clean(
|
||||
html,
|
||||
tags=set(whitelist_tags),
|
||||
attributes=attrs_dict,
|
||||
url_schemes=set(whitelist_protocols),
|
||||
filter_style_properties=set(whitelist_styles),
|
||||
link_rel=None,
|
||||
strip_comments=True,
|
||||
)
|
||||
|
||||
if html != clean_html:
|
||||
raise ValidationError(_('Data contains prohibited markdown content'))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import structlog
|
|||
from allauth.account.models import EmailAddress
|
||||
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks as tasks
|
||||
from common.models import Priority, issue_mail
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
|
@ -98,7 +99,7 @@ def send_email(
|
|||
)
|
||||
return False, 'INVE-W7: no from_email or DEFAULT_FROM_EMAIL specified'
|
||||
|
||||
InvenTree.tasks.offload_task(
|
||||
tasks.offload_task(
|
||||
issue_mail,
|
||||
subject=subject,
|
||||
body=body,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
"""Provides helper functions used throughout the InvenTree project that access the database."""
|
||||
|
||||
import io
|
||||
import ipaddress
|
||||
import socket
|
||||
from decimal import Decimal
|
||||
from typing import Optional, cast
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
|
|
@ -88,6 +90,36 @@ def construct_absolute_url(*arg, base_url=None, request=None):
|
|||
return urljoin(base_url, relative_url)
|
||||
|
||||
|
||||
def validate_url_no_ssrf(url):
|
||||
"""Validate that a URL does not point to a private/internal network address.
|
||||
|
||||
Resolves the hostname to an IP address and checks it against private,
|
||||
loopback, link-local, and reserved IP ranges to prevent SSRF attacks.
|
||||
|
||||
Arguments:
|
||||
url: The URL to validate
|
||||
|
||||
Raises:
|
||||
ValueError: If the URL resolves to a private or reserved IP address
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
|
||||
if not hostname:
|
||||
raise ValueError(_('Invalid URL: no hostname'))
|
||||
|
||||
try:
|
||||
addrinfo = socket.getaddrinfo(hostname, None)
|
||||
except socket.gaierror:
|
||||
raise ValueError(_('Invalid URL: hostname could not be resolved'))
|
||||
|
||||
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
||||
ip = ipaddress.ip_address(sockaddr[0])
|
||||
|
||||
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
|
||||
raise ValueError(_('URL points to a private or reserved IP address'))
|
||||
|
||||
|
||||
def download_image_from_url(remote_url, timeout=2.5):
|
||||
"""Download an image file from a remote URL.
|
||||
|
||||
|
|
@ -115,6 +147,9 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||
validator = URLValidator()
|
||||
validator(remote_url)
|
||||
|
||||
# SSRF protection: validate the resolved IP is not private/internal
|
||||
validate_url_no_ssrf(remote_url)
|
||||
|
||||
# Calculate maximum allowable image size (in bytes)
|
||||
max_size = (
|
||||
int(get_global_setting('INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE')) * 1024 * 1024
|
||||
|
|
@ -129,10 +164,36 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||
response = requests.get(
|
||||
remote_url,
|
||||
timeout=timeout,
|
||||
allow_redirects=True,
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# Handle redirects manually to validate each destination
|
||||
max_redirects = 5
|
||||
redirect_count = 0
|
||||
|
||||
while response.is_redirect and redirect_count < max_redirects:
|
||||
redirect_url = response.headers.get('Location')
|
||||
if not redirect_url:
|
||||
break
|
||||
|
||||
# Validate the redirect destination against SSRF
|
||||
validator(redirect_url)
|
||||
validate_url_no_ssrf(redirect_url)
|
||||
|
||||
redirect_count += 1
|
||||
response = requests.get(
|
||||
redirect_url,
|
||||
timeout=timeout,
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if redirect_count >= max_redirects:
|
||||
raise ValueError(_('Too many redirects'))
|
||||
|
||||
# Throw an error if anything goes wrong
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
|
|
@ -143,6 +204,8 @@ def download_image_from_url(remote_url, timeout=2.5):
|
|||
raise requests.exceptions.HTTPError(
|
||||
_('Server responded with invalid status code') + f': {response.status_code}'
|
||||
)
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise Exception(_('Exception occurred') + f': {exc!s}')
|
||||
|
||||
|
|
@ -288,6 +351,8 @@ def getModelsWithMixin(mixin_class) -> list:
|
|||
models_with_mixin = [
|
||||
x for x in db_models if x is not None and issubclass(x, mixin_class)
|
||||
]
|
||||
# sort to make resulting list deterministic (and easier to test)
|
||||
models_with_mixin.sort(key=lambda x: x._meta.label_lower)
|
||||
|
||||
# Store the result in the session cache
|
||||
set_session_cache(cache_key, models_with_mixin)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
"""Helpers for logging integrations."""
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
import structlog
|
||||
from django_structlog import signals
|
||||
|
||||
|
||||
@receiver(signals.update_failure_response)
|
||||
@receiver(signals.bind_extra_request_finished_metadata)
|
||||
def add_request_id_to_response(response, logger, **kwargs):
|
||||
"""Add the request ID to the response header, so that it can be traced through logs.
|
||||
|
||||
source: https://django-structlog.readthedocs.io/en/latest/how_tos.html#bind-request-id-to-response-s-header
|
||||
"""
|
||||
context = structlog.contextvars.get_merged_contextvars(logger)
|
||||
response['X-InvenTree-ReqId'] = context['request_id']
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
"""Custom management command to list all installed apps.
|
||||
|
||||
This is used to determine which apps are installed,
|
||||
including any apps which are defined for plugins.
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""List all installed apps."""
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""List all installed apps.
|
||||
|
||||
Note that this function outputs in a particular format,
|
||||
which is expected by the calling code in tasks.py
|
||||
"""
|
||||
from django.apps import apps
|
||||
|
||||
app_list = []
|
||||
|
||||
for app in apps.get_app_configs():
|
||||
app_list.append(app.name)
|
||||
|
||||
app_list.sort()
|
||||
|
||||
self.stdout.write(f'Installed Apps: {len(app_list)}')
|
||||
self.stdout.write('>>> ' + ','.join(app_list) + ' <<<')
|
||||
|
|
@ -99,7 +99,7 @@ class Command(BaseCommand):
|
|||
if kwargs['include_items']:
|
||||
icons[icon]['items'].append({
|
||||
'model': model.__name__.lower(),
|
||||
'id': item.id, # type: ignore
|
||||
'id': item.id,
|
||||
})
|
||||
|
||||
self.stdout.write(f'Writing icon map for {len(icons.keys())} icons')
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
- May be required after importing a new dataset, for example
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
|
|
@ -35,7 +34,7 @@ class Command(BaseCommand):
|
|||
img_paths.append(x.path)
|
||||
|
||||
if len(img_paths) > 0:
|
||||
if all(os.path.exists(path) for path in img_paths):
|
||||
if all(default_storage.exists(p) for p in img_paths):
|
||||
# All images exist - skip further work
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -13,25 +13,39 @@ class Command(BaseCommand):
|
|||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""Wait till the database is ready."""
|
||||
self.stdout.write('Waiting for database...')
|
||||
|
||||
connected = False
|
||||
verbose = int(kwargs.get('verbosity', 0)) > 0
|
||||
attempts = kwargs.get('attempts', 10)
|
||||
|
||||
while not connected:
|
||||
time.sleep(2)
|
||||
if verbose:
|
||||
self.stdout.write('Waiting for database connection...')
|
||||
self.stdout.flush()
|
||||
|
||||
while not connected and attempts > 0:
|
||||
attempts -= 1
|
||||
|
||||
try:
|
||||
connection.ensure_connection()
|
||||
|
||||
connected = True
|
||||
|
||||
except OperationalError as e:
|
||||
self.stdout.write(f'Could not connect to database: {e}')
|
||||
except ImproperlyConfigured as e:
|
||||
self.stdout.write(f'Improperly configured: {e}')
|
||||
except (OperationalError, ImproperlyConfigured):
|
||||
if verbose:
|
||||
self.stdout.write('Database connection failed, retrying ...')
|
||||
self.stdout.flush()
|
||||
else:
|
||||
if not connection.is_usable():
|
||||
self.stdout.write('Database configuration is not usable')
|
||||
if verbose:
|
||||
self.stdout.write('Database configuration is not usable')
|
||||
self.stdout.flush()
|
||||
|
||||
if connected:
|
||||
self.stdout.write('Database connection successful!')
|
||||
if verbose:
|
||||
self.stdout.write('Database connection successful!')
|
||||
self.stdout.flush()
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
if not connected:
|
||||
self.stderr.write('Failed to connect to database after multiple attempts')
|
||||
self.stderr.flush()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from django.http import Http404
|
|||
from django.urls import reverse
|
||||
|
||||
import structlog
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework import exceptions, permissions, serializers
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.metadata import SimpleMetadata
|
||||
from rest_framework.request import clone_request
|
||||
|
|
@ -131,10 +131,26 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||
|
||||
# Remove any HTTP methods that the user does not have permission for
|
||||
for method, permission in rolemap.items():
|
||||
# general model / role permission
|
||||
result = check_user_permission(user, self.model, permission) or (
|
||||
role_required and check_user_role(user, role_required, permission)
|
||||
)
|
||||
|
||||
# check if simple IsAuthenticated permission class is used
|
||||
if not result:
|
||||
result = (
|
||||
view.permission_classes
|
||||
and len(view.permission_classes) == 1
|
||||
and any(
|
||||
perm
|
||||
in [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.IsAuthenticatedOrReadScope,
|
||||
]
|
||||
for perm in view.permission_classes
|
||||
)
|
||||
)
|
||||
|
||||
if method in actions and not result:
|
||||
del actions[method]
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ from error_report.middleware import ExceptionProcessor
|
|||
from common.settings import get_global_setting
|
||||
from InvenTree.cache import create_session_cache, delete_session_cache
|
||||
from InvenTree.config import CONFIG_LOOKUPS, inventreeInstaller
|
||||
from InvenTree.version import (
|
||||
inventreeApiVersion,
|
||||
inventreePythonVersion,
|
||||
inventreeVersion,
|
||||
)
|
||||
from users.models import ApiToken
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
|
@ -393,3 +398,15 @@ class InvenTreeHostSettingsMiddleware(MiddlewareMixin):
|
|||
|
||||
# All checks passed
|
||||
return None
|
||||
|
||||
|
||||
class InvenTreeVersionHeaderMiddleware(MiddlewareMixin):
|
||||
"""Middleware to add the InvenTree version header to all responses."""
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Add the InvenTree version header to the response."""
|
||||
response['X-InvenTree-Version'] = inventreeVersion()
|
||||
response['X-InvenTree-API'] = inventreeApiVersion()
|
||||
response['X-InvenTree-Python'] = inventreePythonVersion()
|
||||
response['X-InvenTree-Installer'] = inventreeInstaller()
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from InvenTree.serializers import FilterableSerializerMixin
|
|||
|
||||
|
||||
class CleanMixin:
|
||||
"""Model mixin class which cleans inputs using the Mozilla bleach tools."""
|
||||
"""Model mixin class which cleans inputs using nh3."""
|
||||
|
||||
# Define a list of field names which will *not* be cleaned
|
||||
SAFE_FIELDS = []
|
||||
|
|
@ -52,16 +52,7 @@ class CleanMixin:
|
|||
return Response(serializer.data)
|
||||
|
||||
def clean_string(self, field: str, data: str) -> str:
|
||||
"""Clean / sanitize a single input string.
|
||||
|
||||
Note that this function will *allow* orphaned <>& characters,
|
||||
which would normally be escaped by bleach.
|
||||
|
||||
Nominally, the only thing that will be "cleaned" will be HTML tags
|
||||
|
||||
Ref: https://github.com/mozilla/bleach/issues/192
|
||||
|
||||
"""
|
||||
"""Clean / sanitize a single input string."""
|
||||
cleaned = data
|
||||
|
||||
# By default, newline characters are removed
|
||||
|
|
@ -101,7 +92,7 @@ class CleanMixin:
|
|||
def clean_data(self, data: dict) -> dict:
|
||||
"""Clean / sanitize data.
|
||||
|
||||
This uses Mozilla's bleach under the hood to disable certain html tags by
|
||||
This uses nh3 under the hood to disable certain html tags by
|
||||
encoding them - this leads to script tags etc. to not work.
|
||||
The results can be longer then the input; might make some character combinations
|
||||
`ugly`. Prevents XSS on the server-level.
|
||||
|
|
|
|||
|
|
@ -1410,32 +1410,22 @@ def after_failed_task(sender, instance: Task, created: bool, **kwargs):
|
|||
"""Callback when a new task failure log is generated."""
|
||||
from django.conf import settings
|
||||
|
||||
from InvenTree.exceptions import log_error
|
||||
|
||||
max_attempts = int(settings.Q_CLUSTER.get('max_attempts', 5))
|
||||
n = instance.attempt_count
|
||||
|
||||
# Only notify once the maximum number of attempts has been reached
|
||||
if not instance.success and n >= max_attempts:
|
||||
try:
|
||||
url = InvenTree.helpers_model.construct_absolute_url(
|
||||
reverse(
|
||||
'admin:django_q_failure_change', kwargs={'object_id': instance.pk}
|
||||
)
|
||||
)
|
||||
except (ValueError, NoReverseMatch):
|
||||
url = ''
|
||||
# Create a new Error object associated with this failed task
|
||||
# This will, in turn, trigger a notification to staff users via the Error post_save signal
|
||||
|
||||
# Function name
|
||||
f = instance.func
|
||||
|
||||
notify_staff_users_of_error(
|
||||
instance,
|
||||
'inventree.task_failure',
|
||||
{
|
||||
'failure': instance,
|
||||
'name': _('Task Failure'),
|
||||
'message': _(f"Background worker task '{f}' failed after {n} attempts"),
|
||||
'link': url,
|
||||
},
|
||||
log_error(
|
||||
'task_failure',
|
||||
scope='worker',
|
||||
error_name='Task Failure',
|
||||
error_info=f"Task '{instance.pk}' failed after {n} attempts",
|
||||
error_data=str(instance.result) if instance.result else '',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1495,7 +1485,7 @@ class InvenTreeImageMixin(models.Model):
|
|||
|
||||
def rename_image(self, filename):
|
||||
"""Rename the uploaded image file using the IMAGE_RENAME function."""
|
||||
return self.IMAGE_RENAME(filename) # type: ignore
|
||||
return self.IMAGE_RENAME(filename)
|
||||
|
||||
image = StdImageField(
|
||||
upload_to=rename_image,
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ def auth_exempt(view_func):
|
|||
def wrapped_view(*args, **kwargs):
|
||||
return view_func(*args, **kwargs)
|
||||
|
||||
wrapped_view.auth_exempt = True # type:ignore[unresolved-attribute]
|
||||
wrapped_view.auth_exempt = True
|
||||
return wraps(view_func)(wrapped_view)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -119,16 +119,29 @@ def isGeneratingSchema():
|
|||
'collectstatic',
|
||||
'makemessages',
|
||||
'wait_for_db',
|
||||
'list_apps',
|
||||
'gunicorn',
|
||||
'sqlflush',
|
||||
'qcluster',
|
||||
'check',
|
||||
'shell',
|
||||
'help',
|
||||
]
|
||||
|
||||
if any(cmd in sys.argv for cmd in excluded_commands):
|
||||
return False
|
||||
|
||||
if 'schema' in sys.argv:
|
||||
included_commands = [
|
||||
'schema',
|
||||
'spectactular',
|
||||
# schema adjacent calls
|
||||
'export_settings_definitions',
|
||||
'export_tags',
|
||||
'export_filters',
|
||||
'export_report_context',
|
||||
]
|
||||
|
||||
if any(cmd in sys.argv for cmd in included_commands):
|
||||
return True
|
||||
|
||||
# This is a very inefficient call - so we only use it as a last resort
|
||||
|
|
@ -175,11 +188,46 @@ def isInMainThread():
|
|||
return not isInWorkerThread()
|
||||
|
||||
|
||||
def readOnlyCommands():
|
||||
"""Return a list of read-only management commands which should not trigger database writes."""
|
||||
return [
|
||||
'help',
|
||||
'check',
|
||||
'shell',
|
||||
'sqlflush',
|
||||
'list_apps',
|
||||
'wait_for_db',
|
||||
'spectactular',
|
||||
'makemessages',
|
||||
'collectstatic',
|
||||
'showmigrations',
|
||||
'compilemessages',
|
||||
]
|
||||
|
||||
|
||||
def isReadOnlyCommand():
|
||||
"""Return True if the current command is a read-only command, which should not trigger any database writes."""
|
||||
if (
|
||||
isImportingData()
|
||||
or isRunningMigrations()
|
||||
or isRebuildingData()
|
||||
or isRunningBackup()
|
||||
):
|
||||
return True
|
||||
|
||||
return any(cmd in sys.argv for cmd in readOnlyCommands())
|
||||
|
||||
|
||||
def canAppAccessDatabase(
|
||||
allow_test: bool = False, allow_plugins: bool = False, allow_shell: bool = False
|
||||
):
|
||||
"""Returns True if the apps.py file can access database records.
|
||||
|
||||
Arguments:
|
||||
allow_test: If True, override checks and allow database access during testing mode
|
||||
allow_plugins: If True, override checks and allow database access during plugin loading
|
||||
allow_shell: If True, override checks and allow database access during shell sessions
|
||||
|
||||
There are some circumstances where we don't want the ready function in apps.py
|
||||
to touch the database
|
||||
"""
|
||||
|
|
@ -188,7 +236,7 @@ def canAppAccessDatabase(
|
|||
return False
|
||||
|
||||
# Prevent database access if we are importing data
|
||||
if isImportingData():
|
||||
if not allow_plugins and isImportingData():
|
||||
return False
|
||||
|
||||
# Prevent database access if we are rebuilding data
|
||||
|
|
@ -202,13 +250,13 @@ def canAppAccessDatabase(
|
|||
# If any of the following management commands are being executed,
|
||||
# prevent custom "on load" code from running!
|
||||
excluded_commands = [
|
||||
'check',
|
||||
'createsuperuser',
|
||||
'wait_for_db',
|
||||
'makemessages',
|
||||
'compilemessages',
|
||||
'spectactular',
|
||||
'createsuperuser',
|
||||
'collectstatic',
|
||||
'makemessages',
|
||||
'spectactular',
|
||||
'wait_for_db',
|
||||
'check',
|
||||
]
|
||||
|
||||
if not allow_shell:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,66 @@
|
|||
"""Functions to sanitize user input files."""
|
||||
|
||||
from bleach import clean
|
||||
from bleach.css_sanitizer import CSSSanitizer
|
||||
import nh3
|
||||
|
||||
# Allowed CSS properties for SVG sanitization (combines general CSS and SVG-specific properties)
|
||||
_SVG_ALLOWED_CSS_PROPERTIES = frozenset([
|
||||
# General CSS (matching bleach's original ALLOWED_CSS_PROPERTIES)
|
||||
'azimuth',
|
||||
'background-color',
|
||||
'border-bottom-color',
|
||||
'border-collapse',
|
||||
'border-color',
|
||||
'border-left-color',
|
||||
'border-right-color',
|
||||
'border-top-color',
|
||||
'clear',
|
||||
'color',
|
||||
'cursor',
|
||||
'direction',
|
||||
'display',
|
||||
'elevation',
|
||||
'float',
|
||||
'font',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-style',
|
||||
'font-variant',
|
||||
'font-weight',
|
||||
'height',
|
||||
'letter-spacing',
|
||||
'line-height',
|
||||
'overflow',
|
||||
'pause',
|
||||
'pause-after',
|
||||
'pause-before',
|
||||
'pitch',
|
||||
'pitch-range',
|
||||
'richness',
|
||||
'speak',
|
||||
'speak-header',
|
||||
'speak-numeral',
|
||||
'speak-punctuation',
|
||||
'speech-rate',
|
||||
'stress',
|
||||
'text-align',
|
||||
'text-decoration',
|
||||
'text-indent',
|
||||
'unicode-bidi',
|
||||
'vertical-align',
|
||||
'voice-family',
|
||||
'volume',
|
||||
'white-space',
|
||||
'width',
|
||||
# SVG-specific CSS (matching bleach's ALLOWED_SVG_PROPERTIES)
|
||||
'fill',
|
||||
'fill-opacity',
|
||||
'fill-rule',
|
||||
'stroke',
|
||||
'stroke-linecap',
|
||||
'stroke-linejoin',
|
||||
'stroke-opacity',
|
||||
'stroke-width',
|
||||
])
|
||||
|
||||
ALLOWED_ELEMENTS_SVG = [
|
||||
'a',
|
||||
|
|
@ -184,6 +243,74 @@ ALLOWED_ATTRIBUTES_SVG = [
|
|||
'style',
|
||||
]
|
||||
|
||||
# Default allowlists (matching bleach's original defaults)
|
||||
# TODO: I do not see us needing a bunch of these but I do not want to introduce a breaking change; we might want to narroy this down with the next breaking change
|
||||
DEFAULT_TAGS = frozenset([
|
||||
'a',
|
||||
'abbr',
|
||||
'acronym',
|
||||
'b',
|
||||
'blockquote',
|
||||
'code',
|
||||
'em',
|
||||
'i',
|
||||
'li',
|
||||
'ol',
|
||||
'strong',
|
||||
'ul',
|
||||
])
|
||||
DEAFAULT_ATTRS = {'a': {'href', 'title'}, 'abbr': {'title'}, 'acronym': {'title'}}
|
||||
DEFAULT_CSS = frozenset([
|
||||
'azimuth',
|
||||
'background-color',
|
||||
'border-bottom-color',
|
||||
'border-collapse',
|
||||
'border-color',
|
||||
'border-left-color',
|
||||
'border-right-color',
|
||||
'border-top-color',
|
||||
'clear',
|
||||
'color',
|
||||
'cursor',
|
||||
'direction',
|
||||
'display',
|
||||
'elevation',
|
||||
'float',
|
||||
'font',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-style',
|
||||
'font-variant',
|
||||
'font-weight',
|
||||
'height',
|
||||
'letter-spacing',
|
||||
'line-height',
|
||||
'overflow',
|
||||
'pause',
|
||||
'pause-after',
|
||||
'pause-before',
|
||||
'pitch',
|
||||
'pitch-range',
|
||||
'richness',
|
||||
'speak',
|
||||
'speak-header',
|
||||
'speak-numeral',
|
||||
'speak-punctuation',
|
||||
'speech-rate',
|
||||
'stress',
|
||||
'text-align',
|
||||
'text-decoration',
|
||||
'text-indent',
|
||||
'unicode-bidi',
|
||||
'vertical-align',
|
||||
'voice-family',
|
||||
'volume',
|
||||
'white-space',
|
||||
'width',
|
||||
])
|
||||
# TODO: We might want to respect the setting EXTRA_URL_SCHEMES here but that would be breaking
|
||||
DEFAULT_PROTOCOLS = frozenset(['http', 'https', 'mailto'])
|
||||
|
||||
|
||||
def sanitize_svg(
|
||||
file_data,
|
||||
|
|
@ -206,13 +333,16 @@ def sanitize_svg(
|
|||
if isinstance(file_data, bytes):
|
||||
file_data = file_data.decode('utf-8')
|
||||
|
||||
cleaned = clean(
|
||||
# nh3 requires attributes as dict[str, set[str]]; convert from list (allowed for all elements)
|
||||
attrs_dict = {elem: set(attributes) for elem in elements}
|
||||
|
||||
cleaned = nh3.clean(
|
||||
file_data,
|
||||
tags=elements,
|
||||
attributes=attributes,
|
||||
strip=strip,
|
||||
tags=set(elements),
|
||||
attributes=attrs_dict,
|
||||
filter_style_properties=_SVG_ALLOWED_CSS_PROPERTIES,
|
||||
strip_comments=strip,
|
||||
css_sanitizer=CSSSanitizer(),
|
||||
link_rel=None,
|
||||
)
|
||||
|
||||
return cleaned
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
"""Serializers used in various InvenTree apps."""
|
||||
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
|
@ -33,8 +32,6 @@ from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
|||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.helpers_model import getModelsWithMixin
|
||||
|
||||
from .setting.storages import StorageBackends
|
||||
|
||||
|
||||
# region path filtering
|
||||
class FilterableSerializerField:
|
||||
|
|
@ -689,9 +686,7 @@ class InvenTreeAttachmentSerializerField(serializers.FileField):
|
|||
if not value:
|
||||
return None
|
||||
|
||||
if settings.STORAGE_TARGET == StorageBackends.S3:
|
||||
return str(value.url)
|
||||
return os.path.join(str(settings.MEDIA_URL), str(value))
|
||||
return default_storage.url(str(value))
|
||||
|
||||
|
||||
class InvenTreeImageSerializerField(serializers.ImageField):
|
||||
|
|
@ -705,9 +700,7 @@ class InvenTreeImageSerializerField(serializers.ImageField):
|
|||
if not value:
|
||||
return None
|
||||
|
||||
if settings.STORAGE_TARGET == StorageBackends.S3:
|
||||
return str(value.url)
|
||||
return os.path.join(str(settings.MEDIA_URL), str(value))
|
||||
return default_storage.url(str(value))
|
||||
|
||||
|
||||
class InvenTreeDecimalField(serializers.FloatField):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
"""Configuration settings specific to a particular database backend."""
|
||||
|
||||
import structlog
|
||||
|
||||
from InvenTree.config import get_boolean_setting, get_setting
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
def set_db_options(engine: str, db_options: dict):
|
||||
"""Update database options based on the specified database backend.
|
||||
|
||||
Arguments:
|
||||
engine: The database engine (e.g. 'sqlite3', 'postgresql', etc.)
|
||||
db_options: The database options dictionary to update
|
||||
"""
|
||||
logger.debug('Setting database options: %s', engine)
|
||||
|
||||
if 'postgres' in engine:
|
||||
set_postgres_options(db_options)
|
||||
elif 'mysql' in engine:
|
||||
set_mysql_options(db_options)
|
||||
elif 'sqlite' in engine:
|
||||
set_sqlite_options(db_options)
|
||||
else:
|
||||
raise ValueError(f'Unknown database engine: {engine}')
|
||||
|
||||
|
||||
def set_postgres_options(db_options: dict):
|
||||
"""Set database options specific to postgres backend."""
|
||||
from django.db.backends.postgresql.psycopg_any import IsolationLevel
|
||||
|
||||
# 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 minimum allowed by libpq
|
||||
db_options['connect_timeout'] = int(
|
||||
get_setting('INVENTREE_DB_TIMEOUT', 'database.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. If 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(
|
||||
get_setting('INVENTREE_DB_TCP_KEEPALIVES', 'database.tcp_keepalives', 1)
|
||||
)
|
||||
|
||||
# Seconds after connection is idle to send keep alive
|
||||
if 'keepalives_idle' not in db_options:
|
||||
db_options['keepalives_idle'] = int(
|
||||
get_setting(
|
||||
'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.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(
|
||||
get_setting(
|
||||
'INVENTREE_DB_TCP_KEEPALIVES_INTERVAL',
|
||||
'database.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(
|
||||
get_setting(
|
||||
'INVENTREE_DB_TCP_KEEPALIVES_COUNT',
|
||||
'database.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 = get_boolean_setting(
|
||||
'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
|
||||
)
|
||||
db_options['isolation_level'] = (
|
||||
IsolationLevel.SERIALIZABLE
|
||||
if serializable
|
||||
else IsolationLevel.READ_COMMITTED
|
||||
)
|
||||
|
||||
|
||||
def set_mysql_options(db_options: dict):
|
||||
"""Set database options specific to mysql backend."""
|
||||
# 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 simultaneous 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 = get_boolean_setting(
|
||||
'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
|
||||
)
|
||||
db_options['isolation_level'] = (
|
||||
'serializable' if serializable else 'read committed'
|
||||
)
|
||||
|
||||
|
||||
def set_sqlite_options(db_options: dict):
|
||||
"""Set database options specific to sqlite backend.
|
||||
|
||||
References:
|
||||
- https://docs.djangoproject.com/en/5.0/ref/databases/#sqlite-notes
|
||||
- https://docs.djangoproject.com/en/6.0/ref/databases/#database-is-locked-errors
|
||||
"""
|
||||
import InvenTree.ready
|
||||
|
||||
# Specify minimum timeout behavior for SQLite connections
|
||||
if 'timeout' not in db_options:
|
||||
db_options['timeout'] = int(
|
||||
get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 10)
|
||||
)
|
||||
|
||||
# Specify the transaction mode for the database
|
||||
# For the backend worker thread, IMMEDIATE mode is used,
|
||||
# it has been determined to provide better protection against database locks in the worker thread
|
||||
db_options['transaction_mode'] = (
|
||||
'IMMEDIATE' if InvenTree.ready.isInWorkerThread() else 'DEFERRED'
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
if get_boolean_setting('INVENTREE_DB_WAL_MODE', 'database.wal_mode', True):
|
||||
# Specify that we want to use Write-Ahead Logging (WAL) mode for SQLite databases, as this allows for better concurrency and performance
|
||||
db_options['init_command'] = 'PRAGMA journal_mode=WAL;'
|
||||
|
|
@ -33,7 +33,7 @@ from InvenTree.version import checkMinPythonVersion, inventreeCommitHash
|
|||
from users.oauth2_scopes import oauth2_scopes
|
||||
|
||||
from . import config
|
||||
from .setting import locales, markdown, spectacular, storages
|
||||
from .setting import db_backend, locales, markdown, spectacular, storages
|
||||
|
||||
try:
|
||||
import django_stubs_ext
|
||||
|
|
@ -194,10 +194,18 @@ PLUGINS_MANDATORY = get_setting(
|
|||
'INVENTREE_PLUGINS_MANDATORY', 'plugins_mandatory', typecast=list, default_value=[]
|
||||
)
|
||||
|
||||
if PLUGINS_MANDATORY:
|
||||
logger.info('Mandatory plugins: %s', PLUGINS_MANDATORY)
|
||||
|
||||
PLUGINS_INSTALL_DISABLED = get_boolean_setting(
|
||||
'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
|
||||
)
|
||||
|
||||
if not PLUGINS_ENABLED:
|
||||
PLUGINS_INSTALL_DISABLED = (
|
||||
True # If plugins are disabled, also disable installation
|
||||
)
|
||||
|
||||
PLUGIN_FILE = config.get_plugin_file()
|
||||
|
||||
# Plugin test settings
|
||||
|
|
@ -372,6 +380,7 @@ MIDDLEWARE = CONFIG.get(
|
|||
'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
|
||||
'InvenTree.middleware.InvenTreeHostSettingsMiddleware', # Ensuring correct hosting/security settings
|
||||
'django_structlog.middlewares.RequestMiddleware', # Structured logging
|
||||
'InvenTree.middleware.InvenTreeVersionHeaderMiddleware',
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -483,7 +492,7 @@ if LDAP_AUTH: # pragma: no cover
|
|||
)
|
||||
AUTH_LDAP_USER_SEARCH = django_auth_ldap.config.LDAPSearch(
|
||||
get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'),
|
||||
ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute]
|
||||
ldap.SCOPE_SUBTREE,
|
||||
str(
|
||||
get_setting(
|
||||
'INVENTREE_LDAP_SEARCH_FILTER_STR',
|
||||
|
|
@ -519,7 +528,7 @@ if LDAP_AUTH: # pragma: no cover
|
|||
)
|
||||
AUTH_LDAP_GROUP_SEARCH = django_auth_ldap.config.LDAPSearch(
|
||||
get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'),
|
||||
ldap.SCOPE_SUBTREE, # type: ignore[unresolved-attribute]
|
||||
ldap.SCOPE_SUBTREE,
|
||||
f'(objectClass={AUTH_LDAP_GROUP_OBJECT_CLASS})',
|
||||
)
|
||||
AUTH_LDAP_GROUP_TYPE_CLASS = get_setting(
|
||||
|
|
@ -720,108 +729,8 @@ db_options = db_config.get('OPTIONS', db_config.get('options'))
|
|||
if db_options is None:
|
||||
db_options = {}
|
||||
|
||||
# Specific options for postgres backend
|
||||
if 'postgres' in DB_ENGINE: # pragma: no cover
|
||||
from django.db.backends.postgresql.psycopg_any import ( # type: ignore[unresolved-import]
|
||||
IsolationLevel,
|
||||
)
|
||||
|
||||
# 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 minimum allowed by libpq
|
||||
db_options['connect_timeout'] = int(
|
||||
get_setting('INVENTREE_DB_TIMEOUT', 'database.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(
|
||||
get_setting('INVENTREE_DB_TCP_KEEPALIVES', 'database.tcp_keepalives', 1)
|
||||
)
|
||||
|
||||
# Seconds after connection is idle to send keep alive
|
||||
if 'keepalives_idle' not in db_options:
|
||||
db_options['keepalives_idle'] = int(
|
||||
get_setting(
|
||||
'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.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(
|
||||
get_setting(
|
||||
'INVENTREE_DB_TCP_KEEPALIVES_INTERVAL',
|
||||
'database.tcp_keepalives_internal',
|
||||
'1',
|
||||
)
|
||||
)
|
||||
|
||||
# Number of missing ACKs before we close the connection
|
||||
if 'keepalives_count' not in db_options:
|
||||
db_options['keepalives_count'] = int(
|
||||
get_setting(
|
||||
'INVENTREE_DB_TCP_KEEPALIVES_COUNT',
|
||||
'database.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 = get_boolean_setting(
|
||||
'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
|
||||
)
|
||||
db_options['isolation_level'] = (
|
||||
IsolationLevel.SERIALIZABLE
|
||||
if serializable
|
||||
else IsolationLevel.READ_COMMITTED
|
||||
)
|
||||
|
||||
# Specific options for MySql / MariaDB backend
|
||||
elif '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 = get_boolean_setting(
|
||||
'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
|
||||
)
|
||||
db_options['isolation_level'] = (
|
||||
'serializable' if serializable else 'read committed'
|
||||
)
|
||||
|
||||
# Specific options for sqlite backend
|
||||
elif '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
|
||||
# Set database-specific options
|
||||
db_backend.set_db_options(DB_ENGINE, db_options)
|
||||
|
||||
# Provide OPTIONS dict back to the database configuration dict
|
||||
db_config['OPTIONS'] = db_options
|
||||
|
|
@ -930,10 +839,15 @@ GLOBAL_CACHE_ENABLED = is_global_cache_enabled()
|
|||
|
||||
CACHES = {'default': get_cache_config(GLOBAL_CACHE_ENABLED)}
|
||||
|
||||
_q_worker_timeout = int(
|
||||
BACKGROUND_WORKER_TIMEOUT = int(
|
||||
get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90)
|
||||
)
|
||||
|
||||
# Set the retry time for background workers to be slightly longer than the worker timeout, to ensure that workers have time to timeout before being retried
|
||||
BACKGROUND_WORKER_RETRY = max(
|
||||
int(get_setting('INVENTREE_BACKGROUND_RETRY', 'background.retry', 300)),
|
||||
BACKGROUND_WORKER_TIMEOUT + 120,
|
||||
)
|
||||
|
||||
# Prevent running multiple background workers if global cache is disabled
|
||||
# This is to prevent scheduling conflicts due to the lack of a shared cache
|
||||
|
|
@ -943,22 +857,39 @@ BACKGROUND_WORKER_COUNT = (
|
|||
else 1
|
||||
)
|
||||
|
||||
# If running with SQLite, limit background worker threads to 1 to prevent database locking issues
|
||||
if 'sqlite' in DB_ENGINE:
|
||||
BACKGROUND_WORKER_COUNT = 1
|
||||
|
||||
BACKGROUND_WORKER_ATTEMPTS = int(
|
||||
get_setting('INVENTREE_BACKGROUND_MAX_ATTEMPTS', 'background.max_attempts', 5)
|
||||
)
|
||||
|
||||
# Check if '--sync' was passed in the command line
|
||||
if '--sync' in sys.argv and '--noreload' in sys.argv and DEBUG:
|
||||
SYNC_TASKS = True
|
||||
else:
|
||||
SYNC_TASKS = False
|
||||
|
||||
# Clean up sys.argv so Django doesn't complain about an unknown argument
|
||||
if SYNC_TASKS:
|
||||
sys.argv.remove('--sync')
|
||||
|
||||
# django-q background worker configuration
|
||||
Q_CLUSTER = {
|
||||
'name': 'InvenTree',
|
||||
'label': 'Background Tasks',
|
||||
'workers': BACKGROUND_WORKER_COUNT,
|
||||
'timeout': _q_worker_timeout,
|
||||
'retry': max(120, _q_worker_timeout + 30),
|
||||
'max_attempts': int(
|
||||
get_setting('INVENTREE_BACKGROUND_MAX_ATTEMPTS', 'background.max_attempts', 5)
|
||||
),
|
||||
'timeout': BACKGROUND_WORKER_TIMEOUT,
|
||||
'retry': BACKGROUND_WORKER_RETRY,
|
||||
'max_attempts': BACKGROUND_WORKER_ATTEMPTS,
|
||||
'save_limit': 1000,
|
||||
'queue_limit': 50,
|
||||
'catch_up': False,
|
||||
'bulk': 10,
|
||||
'orm': 'default',
|
||||
'cache': 'default',
|
||||
'sync': False,
|
||||
'sync': SYNC_TASKS,
|
||||
'poll': 1.5,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ def check_provider(provider):
|
|||
if not app:
|
||||
return False
|
||||
|
||||
if allauth.app_settings.SITES_ENABLED: # type: ignore[unresolved-attribute]
|
||||
if allauth.app_settings.SITES_ENABLED:
|
||||
# At least one matching site must be specified
|
||||
if not app.sites.exists():
|
||||
logger.error('SocialApp %s has no sites configured', app)
|
||||
|
|
|
|||
|
|
@ -159,15 +159,71 @@ def record_task_success(task_name: str):
|
|||
set_global_setting(f'_{task_name}_SUCCESS', datetime.now().isoformat(), None)
|
||||
|
||||
|
||||
def check_existing_task(taskname, group: str, *args, **kwargs) -> Optional[str]:
|
||||
"""Test if an identical task is already registered with the worker.
|
||||
|
||||
This will only return true if the task name, group, args and kwargs all match an existing task.
|
||||
|
||||
Arguments:
|
||||
taskname: The name of the task to check for, in the format 'app.module.function'
|
||||
group: The group that the task belongs to
|
||||
*args: Positional arguments to match
|
||||
**kwargs: Keyword arguments to match
|
||||
|
||||
Returns:
|
||||
Optional[str]: The ID of the matching task, if found, otherwise None
|
||||
"""
|
||||
from django_q.models import OrmQ
|
||||
|
||||
task_id = None
|
||||
|
||||
# Iterate through all available tasks, with the most recent first
|
||||
for task in OrmQ.objects.all().order_by('-id'):
|
||||
if task.func() != taskname and task.task['func'] != taskname:
|
||||
# Task does not match
|
||||
continue
|
||||
|
||||
if task.group() != group:
|
||||
# Group does not match
|
||||
continue
|
||||
|
||||
if task.args() != args:
|
||||
# Task args do not match
|
||||
continue
|
||||
|
||||
if task.kwargs() != kwargs:
|
||||
# Task kwargs do not match
|
||||
continue
|
||||
|
||||
task_id = task.task_id()
|
||||
|
||||
break
|
||||
|
||||
return task_id
|
||||
|
||||
|
||||
def offload_task(
|
||||
taskname, *args, force_async=False, force_sync=False, **kwargs
|
||||
) -> bool:
|
||||
taskname,
|
||||
*args,
|
||||
force_async: bool = False,
|
||||
force_sync: bool = False,
|
||||
check_duplicates: bool = True,
|
||||
**kwargs,
|
||||
) -> str | bool:
|
||||
"""Create an AsyncTask if workers are running. This is different to a 'scheduled' task, in that it only runs once!
|
||||
|
||||
If workers are not running or force_sync flag, is set then the task is ran synchronously.
|
||||
|
||||
Arguments:
|
||||
taskname: The name of the task to be run, in the format 'app.module.function'
|
||||
*args: Positional arguments to be passed to the task function
|
||||
force_async: If True, force the task to be offloaded (even if workers are not running)
|
||||
force_sync: If True, force the task to be run synchronously (even if workers are running)
|
||||
check_duplicates: If True, check for existing identical tasks before offloading
|
||||
**kwargs: Keyword arguments to be passed to the task function
|
||||
|
||||
Returns:
|
||||
bool: True if the task was offloaded (or ran), False otherwise
|
||||
str | bool: Task ID if the task was offloaded, True if ran synchronously, False otherwise
|
||||
"""
|
||||
from InvenTree.exceptions import log_error
|
||||
|
||||
|
|
@ -198,11 +254,23 @@ def offload_task(
|
|||
force_sync = True
|
||||
|
||||
if force_async or (is_worker_running() and not force_sync):
|
||||
# Before offloading, check if a duplicate task exists
|
||||
if not force_sync and check_duplicates:
|
||||
if task_id := check_existing_task(taskname, group, *args, **kwargs):
|
||||
logger.debug(
|
||||
"Skipping duplicate task '%s' with ID '%s'", taskname, task_id
|
||||
)
|
||||
|
||||
return task_id
|
||||
|
||||
# Running as asynchronous task
|
||||
try:
|
||||
task = AsyncTask(taskname, *args, group=group, **kwargs)
|
||||
with tracer.start_as_current_span(f'async worker: {taskname}'):
|
||||
task.run()
|
||||
|
||||
# Return the ID of the offloaded task, so that it can be tracked if needed
|
||||
return task.id
|
||||
except ImportError:
|
||||
raise_warning(f"WARNING: '{taskname}' not offloaded - Function not found")
|
||||
return False
|
||||
|
|
@ -265,6 +333,40 @@ def offload_task(
|
|||
return True
|
||||
|
||||
|
||||
def get_queued_task(task_id: str):
|
||||
"""Find the task in the queue, if it exists.
|
||||
|
||||
Note that the OrmQ table does NOT keep the task ID as a database field,
|
||||
it is instead stored in the payload data.
|
||||
If there are a large number of pending tasks, this query may be inefficient,
|
||||
but there is no other way to find a queued task by ID.
|
||||
"""
|
||||
offset = 0
|
||||
limit = 500
|
||||
|
||||
if not task_id:
|
||||
# Return early if no task ID was provided
|
||||
return None
|
||||
|
||||
task_id = str(task_id)
|
||||
|
||||
from django_q.models import OrmQ
|
||||
|
||||
while True:
|
||||
queued_tasks = OrmQ.objects.all().order_by('id')[offset : offset + limit]
|
||||
if not queued_tasks:
|
||||
break
|
||||
|
||||
for task in queued_tasks:
|
||||
if task.task_id() == task_id:
|
||||
return task
|
||||
|
||||
offset += limit
|
||||
|
||||
# No matching task was discovered
|
||||
return None
|
||||
|
||||
|
||||
@dataclass()
|
||||
class ScheduledTask:
|
||||
"""A scheduled task.
|
||||
|
|
@ -286,7 +388,7 @@ class ScheduledTask:
|
|||
QUARTERLY: str = 'Q'
|
||||
YEARLY: str = 'Y'
|
||||
|
||||
TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY) # type: ignore[invalid-assignment]
|
||||
TYPE: tuple[str] = (MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY)
|
||||
|
||||
|
||||
class TaskRegister:
|
||||
|
|
|
|||
|
|
@ -278,8 +278,7 @@ class BulkDeleteTests(InvenTreeAPITestCase):
|
|||
response = self.delete(url, {}, expected_code=400)
|
||||
|
||||
self.assertIn(
|
||||
'List of items or filters must be provided for bulk operation',
|
||||
str(response.data),
|
||||
'List of items must be provided for bulk operation', str(response.data)
|
||||
)
|
||||
|
||||
# DELETE with invalid 'items'
|
||||
|
|
@ -287,11 +286,6 @@ class BulkDeleteTests(InvenTreeAPITestCase):
|
|||
|
||||
self.assertIn('Items must be provided as a list', str(response.data))
|
||||
|
||||
# DELETE with invalid 'filters'
|
||||
response = self.delete(url, {'filters': [1, 2, 3]}, expected_code=400)
|
||||
|
||||
self.assertIn('Filters must be provided as a dict', str(response.data))
|
||||
|
||||
|
||||
class SearchTests(InvenTreeAPITestCase):
|
||||
"""Unit tests for global search endpoint."""
|
||||
|
|
@ -574,7 +568,7 @@ class GeneralApiTests(InvenTreeAPITestCase):
|
|||
|
||||
self.assertIn('License file not found at', str(log.output))
|
||||
|
||||
with TemporaryDirectory() as tmp: # type: ignore[no-matching-overload]
|
||||
with TemporaryDirectory() as tmp:
|
||||
sample_file = Path(tmp, 'temp.txt')
|
||||
sample_file.write_text('abc', 'utf-8')
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from django.db.utils import NotSupportedError
|
|||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from django_q.models import Schedule, Task
|
||||
from django_q.models import OrmQ, Schedule, Task
|
||||
from error_report.models import Error
|
||||
|
||||
import InvenTree.tasks
|
||||
|
|
@ -234,11 +234,8 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase):
|
|||
|
||||
msg = NotificationMessage.objects.last()
|
||||
|
||||
self.assertEqual(msg.name, 'Task Failure')
|
||||
self.assertEqual(
|
||||
msg.message,
|
||||
"Background worker task 'InvenTree.tasks.failed_task' failed after 10 attempts",
|
||||
)
|
||||
self.assertEqual(msg.name, 'Server Error')
|
||||
self.assertEqual(msg.message, 'An error has been logged by the server.')
|
||||
|
||||
def test_delete_old_emails(self):
|
||||
"""Test the delete_old_emails task."""
|
||||
|
|
@ -297,3 +294,74 @@ class InvenTreeTaskTests(PluginRegistryMixin, TestCase):
|
|||
)
|
||||
msg.timestamp = timestamp
|
||||
msg.save()
|
||||
|
||||
def test_duplicate_tasks(self):
|
||||
"""Test for task duplication."""
|
||||
# Start with a blank slate
|
||||
OrmQ.objects.all().delete()
|
||||
|
||||
# Add some unique tasks
|
||||
for idx in range(10):
|
||||
InvenTree.tasks.offload_task(
|
||||
f'dummy_module.dummy_function_{idx}', force_async=True
|
||||
)
|
||||
|
||||
self.assertEqual(OrmQ.objects.count(), 10)
|
||||
|
||||
# Add some duplicate tasks
|
||||
for _idx in range(10):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function_x',
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
animal='cat',
|
||||
vegetable='carrot',
|
||||
force_async=True,
|
||||
)
|
||||
|
||||
# Only 1 extra task should have been added
|
||||
self.assertEqual(OrmQ.objects.count(), 11)
|
||||
|
||||
# Add some more duplicate tasks, but ignore duplication checks
|
||||
for _idx in range(10):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function_y',
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
animal='dog',
|
||||
vegetable='yam',
|
||||
force_async=True,
|
||||
check_duplicates=False,
|
||||
)
|
||||
|
||||
# 10 extra tasks should have been added
|
||||
self.assertEqual(OrmQ.objects.count(), 21)
|
||||
|
||||
# Add more tasks, which are *not* duplicates based on args
|
||||
for idx in range(10):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function',
|
||||
1,
|
||||
idx,
|
||||
3,
|
||||
animal='cat',
|
||||
vegetable='carrot',
|
||||
force_async=True,
|
||||
)
|
||||
|
||||
# Add more tasks, which are *not* duplicates based on kwargs
|
||||
for idx in range(10):
|
||||
InvenTree.tasks.offload_task(
|
||||
'dummy_module.dummy_function',
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
animal='cat',
|
||||
vegetable=f'vegetable_{idx}',
|
||||
force_async=True,
|
||||
)
|
||||
|
||||
# 20 more tasks should have been added
|
||||
self.assertEqual(OrmQ.objects.count(), 41)
|
||||
|
|
|
|||
|
|
@ -692,23 +692,26 @@ class TestHelpers(TestCase):
|
|||
self.assertFalse(helpers.isNull(s))
|
||||
|
||||
def testStaticUrl(self):
|
||||
"""Test static url helpers."""
|
||||
"""Test static URL helpers."""
|
||||
self.assertEqual(helpers.getStaticUrl('test.jpg'), '/static/test.jpg')
|
||||
self.assertEqual(helpers.getBlankImage(), '/static/img/blank_image.png')
|
||||
self.assertEqual(
|
||||
helpers.getBlankThumbnail(), '/static/img/blank_image.thumbnail.png'
|
||||
)
|
||||
|
||||
self.assertFalse(helpers.checkStaticFile('dummy', 'dir', 'test.jpg'))
|
||||
self.assertTrue(helpers.checkStaticFile('img', 'blank_image.png'))
|
||||
|
||||
def testMediaUrl(self):
|
||||
"""Test getMediaUrl."""
|
||||
# Str should not work
|
||||
with self.assertRaises(TypeError):
|
||||
helpers.getMediaUrl('xx/yy.png') # type: ignore
|
||||
helpers.getMediaUrl('xx/yy.png')
|
||||
|
||||
# Correct usage
|
||||
part = Part().image
|
||||
self.assertEqual(
|
||||
helpers.getMediaUrl(StdImageFieldFile(part, part, 'xx/yy.png')), # type: ignore
|
||||
helpers.getMediaUrl(StdImageFieldFile(part, part, 'xx/yy.png')), # ty:ignore[too-many-positional-arguments]
|
||||
'/media/xx/yy.png',
|
||||
)
|
||||
|
||||
|
|
@ -1604,11 +1607,11 @@ class SanitizerTest(TestCase):
|
|||
def test_svg_sanitizer(self):
|
||||
"""Test that SVGs are sanitized accordingly."""
|
||||
valid_string = """<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" height="400" width="400">{0}
|
||||
<path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91;"></path>
|
||||
<path id="path1" d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z" style="fill:#ddbc91"></path>
|
||||
</svg>"""
|
||||
dangerous_string = valid_string.format('<script>alert();</script>')
|
||||
|
||||
# Test that valid string
|
||||
# Test that valid string passes through unchanged
|
||||
self.assertEqual(valid_string, sanitize_svg(valid_string))
|
||||
|
||||
# Test that invalid string is cleaned
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import base64
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from opentelemetry import metrics, trace # type: ignore[import]
|
||||
from opentelemetry import metrics, trace
|
||||
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
||||
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import build.api
|
|||
import common.api
|
||||
import company.api
|
||||
import importer.api
|
||||
import InvenTree.logging # noqa: F401 - ensure logging handlers are registered
|
||||
import machine.api
|
||||
import order.api
|
||||
import part.api
|
||||
|
|
|
|||
|
|
@ -289,8 +289,10 @@ def inventreeBranch():
|
|||
return ' '.join(branch.splitlines())
|
||||
|
||||
if main_branch is None:
|
||||
return None
|
||||
return main_branch.decode('utf-8')
|
||||
return None # pragma: no cover - branch information may not be available in all environments
|
||||
return main_branch.decode(
|
||||
'utf-8'
|
||||
) # pragma: no cover - branch information may not be available in all environments
|
||||
|
||||
|
||||
def inventreeTarget():
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ import django_filters.rest_framework.filters as rest_filters
|
|||
from django_filters.rest_framework.filterset import FilterSet
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
import build.models as build_models
|
||||
import build.serializers
|
||||
import common.models
|
||||
import common.serializers
|
||||
import part.models as part_models
|
||||
import stock.models as stock_models
|
||||
import stock.serializers
|
||||
|
|
@ -145,8 +146,8 @@ class BuildFilter(FilterSet):
|
|||
def filter_overdue(self, queryset, name, value):
|
||||
"""Filter the queryset to either include or exclude orders which are overdue."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(Build.OVERDUE_FILTER)
|
||||
return queryset.exclude(Build.OVERDUE_FILTER)
|
||||
return queryset.filter(Build.get_overdue_filter())
|
||||
return queryset.exclude(Build.get_overdue_filter())
|
||||
|
||||
assigned_to_me = rest_filters.BooleanFilter(
|
||||
label=_('Assigned to me'), method='filter_assigned_to_me'
|
||||
|
|
@ -662,6 +663,13 @@ class BuildLineDetail(BuildLineMixin, OutputOptionsMixin, RetrieveUpdateDestroyA
|
|||
class BuildOrderContextMixin:
|
||||
"""Mixin class which adds build order as serializer context variable."""
|
||||
|
||||
def get_build(self):
|
||||
"""Return the Build object associated with this API endpoint."""
|
||||
try:
|
||||
return Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||
except (ValueError, Build.DoesNotExist):
|
||||
raise NotFound(_('Build not found'))
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add extra context information to the endpoint serializer."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
|
@ -670,8 +678,8 @@ class BuildOrderContextMixin:
|
|||
ctx['to_complete'] = True
|
||||
|
||||
try:
|
||||
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||
except Exception:
|
||||
ctx['build'] = self.get_build()
|
||||
except NotFound:
|
||||
pass
|
||||
|
||||
return ctx
|
||||
|
|
@ -764,6 +772,37 @@ class BuildAutoAllocate(BuildOrderContextMixin, CreateAPI):
|
|||
queryset = Build.objects.none()
|
||||
serializer_class = build.serializers.BuildAutoAllocationSerializer
|
||||
|
||||
@extend_schema(responses={200: common.serializers.TaskDetailSerializer})
|
||||
def post(self, *args, **kwargs):
|
||||
"""Override the POST method to handle auto allocation task.
|
||||
|
||||
As this is offloaded to the background task,
|
||||
we return information about the background task which is performing the auto allocation operation.
|
||||
"""
|
||||
from build.tasks import auto_allocate_build
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
build = self.get_build()
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
||||
# Offload the task to the background worker
|
||||
task_id = offload_task(
|
||||
auto_allocate_build,
|
||||
build.pk,
|
||||
location=data.get('location', None),
|
||||
exclude_location=data.get('exclude_location', None),
|
||||
interchangeable=data['interchangeable'],
|
||||
substitutes=data['substitutes'],
|
||||
optional_items=data['optional_items'],
|
||||
item_type=data.get('item_type', 'untracked'),
|
||||
group='build',
|
||||
)
|
||||
|
||||
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
|
||||
return Response(response, status=response['http_status'])
|
||||
|
||||
|
||||
class BuildAllocate(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint to allocate stock items to a build order.
|
||||
|
|
@ -786,6 +825,39 @@ class BuildConsume(BuildOrderContextMixin, CreateAPI):
|
|||
queryset = Build.objects.none()
|
||||
serializer_class = build.serializers.BuildConsumeSerializer
|
||||
|
||||
@extend_schema(responses={200: common.serializers.TaskDetailSerializer})
|
||||
def post(self, *args, **kwargs):
|
||||
"""Override the POST method to handle consume task.
|
||||
|
||||
As this is offloaded to the background task,
|
||||
we return information about the background task which is performing the consume operation.
|
||||
"""
|
||||
from build.tasks import consume_build_stock
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
build = self.get_build()
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
||||
# Extract the information we need to consume build stock
|
||||
items = data.get('items', [])
|
||||
lines = data.get('lines', [])
|
||||
notes = data.get('notes', '')
|
||||
|
||||
# Offload the task to the background worker
|
||||
task_id = offload_task(
|
||||
consume_build_stock,
|
||||
build.pk,
|
||||
lines=[line['build_line'].pk for line in lines],
|
||||
items={item['build_item'].pk: item['quantity'] for item in items},
|
||||
user_id=self.request.user.pk,
|
||||
notes=notes,
|
||||
)
|
||||
|
||||
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
|
||||
return Response(response, status=response['http_status'])
|
||||
|
||||
|
||||
class BuildIssue(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for issuing a BuildOrder."""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Build database model definitions."""
|
||||
|
||||
import decimal
|
||||
from typing import Optional
|
||||
from typing import Optional, TypedDict
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
|
|
@ -50,7 +50,7 @@ from stock.status_codes import StockHistoryCode, StockStatus
|
|||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
class BuildReportContext(report.mixins.BaseReportContext):
|
||||
class BuildReportContext(report.mixins.BaseReportContext, TypedDict):
|
||||
"""Context for the Build model.
|
||||
|
||||
Attributes:
|
||||
|
|
@ -132,11 +132,14 @@ class Build(
|
|||
TRACKED = 'tracked' # Tracked BOM items
|
||||
UNTRACKED = 'untracked' # Untracked BOM items
|
||||
|
||||
OVERDUE_FILTER = (
|
||||
Q(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||
& ~Q(target_date=None)
|
||||
& Q(target_date__lte=InvenTree.helpers.current_date())
|
||||
)
|
||||
@classmethod
|
||||
def get_overdue_filter(cls):
|
||||
"""Filter for determining if a build order is overdue."""
|
||||
return (
|
||||
Q(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||
& ~Q(target_date=None)
|
||||
& Q(target_date__lte=InvenTree.helpers.current_date())
|
||||
)
|
||||
|
||||
# Global setting for specifying reference pattern
|
||||
REFERENCE_PATTERN_SETTING = 'BUILDORDER_REFERENCE_PATTERN'
|
||||
|
|
@ -465,7 +468,7 @@ class Build(
|
|||
bool: Is the build overdue
|
||||
"""
|
||||
query = Build.objects.filter(pk=self.pk)
|
||||
query = query.filter(Build.OVERDUE_FILTER)
|
||||
query = query.filter(Build.get_overdue_filter())
|
||||
|
||||
return query.exists()
|
||||
|
||||
|
|
@ -1036,7 +1039,7 @@ class Build(
|
|||
lines = lines.exclude(bom_item__consumable=True)
|
||||
lines = lines.annotate(allocated=annotate_allocated_quantity())
|
||||
|
||||
for build_line in lines: # type: ignore[non-iterable]
|
||||
for build_line in lines:
|
||||
reduce_by = build_line.allocated - build_line.quantity
|
||||
|
||||
if reduce_by <= 0:
|
||||
|
|
@ -1509,10 +1512,10 @@ class Build(
|
|||
unallocated_quantity -= quantity
|
||||
|
||||
except (ValidationError, serializers.ValidationError) as exc:
|
||||
# Catch model errors and re-throw as DRF errors
|
||||
# Re-raise with a Django-compatible validation payload
|
||||
raise ValidationError(
|
||||
exc.message, detail=serializers.as_serializer_error(exc)
|
||||
)
|
||||
serializers.as_serializer_error(exc)
|
||||
) from exc
|
||||
|
||||
if unallocated_quantity <= 0:
|
||||
# We have now fully-allocated this BomItem - no need to continue!
|
||||
|
|
@ -1692,7 +1695,7 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
|||
instance.update_build_line_items()
|
||||
|
||||
|
||||
class BuildLineReportContext(report.mixins.BaseReportContext):
|
||||
class BuildLineReportContext(report.mixins.BaseReportContext, TypedDict):
|
||||
"""Context for the BuildLine model.
|
||||
|
||||
Attributes:
|
||||
|
|
@ -1742,6 +1745,7 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
|
|||
"""Return the API URL used to access this model."""
|
||||
return reverse('api-build-line-list')
|
||||
|
||||
# type
|
||||
def report_context(self) -> BuildLineReportContext:
|
||||
"""Generate custom report context for this BuildLine object."""
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
from rest_framework import serializers
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
import build.tasks
|
||||
import common.filters
|
||||
import common.settings
|
||||
import company.serializers
|
||||
|
|
@ -38,7 +37,6 @@ from InvenTree.serializers import (
|
|||
NotesFieldMixin,
|
||||
enable_filter,
|
||||
)
|
||||
from InvenTree.tasks import offload_task
|
||||
from stock.generators import generate_batch_code
|
||||
from stock.models import StockItem, StockLocation
|
||||
from stock.serializers import (
|
||||
|
|
@ -51,7 +49,6 @@ from users.serializers import OwnerSerializer, UserSerializer
|
|||
|
||||
from .models import Build, BuildItem, BuildLine
|
||||
from .status_codes import BuildStatus
|
||||
from .tasks import consume_build_item, consume_build_line
|
||||
|
||||
|
||||
class BuildSerializer(
|
||||
|
|
@ -168,7 +165,8 @@ class BuildSerializer(
|
|||
queryset = queryset.annotate(
|
||||
overdue=Case(
|
||||
When(
|
||||
Build.OVERDUE_FILTER, then=Value(True, output_field=BooleanField())
|
||||
Build.get_overdue_filter(),
|
||||
then=Value(True, output_field=BooleanField()),
|
||||
),
|
||||
default=Value(False, output_field=BooleanField()),
|
||||
)
|
||||
|
|
@ -1128,27 +1126,6 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
|||
help_text=_('Select item type to auto-allocate'),
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""Perform the auto-allocation step."""
|
||||
import InvenTree.tasks
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
build_order = self.context['build']
|
||||
|
||||
if not InvenTree.tasks.offload_task(
|
||||
build.tasks.auto_allocate_build,
|
||||
build_order.pk,
|
||||
location=data.get('location', None),
|
||||
exclude_location=data.get('exclude_location', None),
|
||||
interchangeable=data['interchangeable'],
|
||||
substitutes=data['substitutes'],
|
||||
optional_items=data['optional_items'],
|
||||
item_type=data.get('item_type', 'untracked'),
|
||||
group='build',
|
||||
):
|
||||
raise ValidationError(_('Failed to start auto-allocation task'))
|
||||
|
||||
|
||||
class BuildItemSerializer(
|
||||
FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer
|
||||
|
|
@ -1846,46 +1823,3 @@ class BuildConsumeSerializer(serializers.Serializer):
|
|||
raise ValidationError(_('At least one item or line must be provided'))
|
||||
|
||||
return data
|
||||
|
||||
@transaction.atomic
|
||||
def save(self):
|
||||
"""Perform the stock consumption step."""
|
||||
data = self.validated_data
|
||||
request = self.context.get('request')
|
||||
notes = data.get('notes', '')
|
||||
|
||||
# We may be passed either a list of BuildItem or BuildLine instances
|
||||
items = data.get('items', [])
|
||||
lines = data.get('lines', [])
|
||||
|
||||
with transaction.atomic():
|
||||
# Process the provided BuildItem objects
|
||||
for item in items:
|
||||
build_item = item['build_item']
|
||||
quantity = item['quantity']
|
||||
|
||||
if build_item.install_into:
|
||||
# If the build item is tracked into an output, we do not consume now
|
||||
# Instead, it gets consumed when the output is completed
|
||||
continue
|
||||
|
||||
# Offload a background task to consume this BuildItem
|
||||
offload_task(
|
||||
consume_build_item,
|
||||
build_item.pk,
|
||||
quantity,
|
||||
notes=notes,
|
||||
user_id=request.user.pk if request else None,
|
||||
)
|
||||
|
||||
# Process the provided BuildLine objects
|
||||
for line in lines:
|
||||
build_line = line['build_line']
|
||||
|
||||
# Offload a background task to consume this BuildLine
|
||||
offload_task(
|
||||
consume_build_line,
|
||||
build_line.pk,
|
||||
notes=notes,
|
||||
user_id=request.user.pk if request else None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import structlog
|
||||
|
|
@ -27,61 +29,53 @@ def auto_allocate_build(build_id: int, **kwargs):
|
|||
"""Run auto-allocation for a specified BuildOrder."""
|
||||
from build.models import Build
|
||||
|
||||
build_order = Build.objects.filter(pk=build_id).first()
|
||||
|
||||
if not build_order:
|
||||
logger.warning(
|
||||
'Could not auto-allocate BuildOrder <%s> - BuildOrder does not exist',
|
||||
build_id,
|
||||
)
|
||||
return
|
||||
|
||||
build_order = Build.objects.get(pk=build_id)
|
||||
build_order.auto_allocate_stock(**kwargs)
|
||||
|
||||
|
||||
@tracer.start_as_current_span('consume_build_item')
|
||||
def consume_build_item(
|
||||
item_id: str, quantity, notes: str = '', user_id: int | None = None
|
||||
@tracer.start_as_current_span('consume_build_stock')
|
||||
def consume_build_stock(
|
||||
build_id: int,
|
||||
lines: Optional[list[int]] = None,
|
||||
items: Optional[dict] = None,
|
||||
user_id: int | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Consume stock against a particular BuildOrderLineItem allocation."""
|
||||
from build.models import BuildItem
|
||||
"""Consume stock for the specified BuildOrder.
|
||||
|
||||
item = BuildItem.objects.filter(pk=item_id).first()
|
||||
Arguments:
|
||||
build_id: The ID of the BuildOrder to consume stock for
|
||||
lines: Optional list of BuildLine IDs to consume
|
||||
items: Optional dict of BuildItem IDs (and quantities)to consume
|
||||
user_id: The ID of the user who initiated the stock consumption
|
||||
"""
|
||||
from build.models import Build, BuildItem, BuildLine
|
||||
|
||||
if not item:
|
||||
logger.warning(
|
||||
'Could not consume stock for BuildItem <%s> - BuildItem does not exist',
|
||||
item_id,
|
||||
)
|
||||
return
|
||||
build = Build.objects.get(pk=build_id)
|
||||
user = User.objects.filter(pk=user_id).first() if user_id else None
|
||||
|
||||
item.complete_allocation(
|
||||
quantity=quantity,
|
||||
notes=notes,
|
||||
user=User.objects.filter(pk=user_id).first() if user_id else None,
|
||||
)
|
||||
lines = lines or []
|
||||
items = items or {}
|
||||
notes = kwargs.pop('notes', '')
|
||||
|
||||
# Extract the relevant BuildLine and BuildItem objects
|
||||
with transaction.atomic():
|
||||
# Consume each of the specified BuildLine objects
|
||||
for line_id in lines:
|
||||
if build_line := BuildLine.objects.filter(pk=line_id, build=build).first():
|
||||
for item in build_line.allocations.all():
|
||||
item.complete_allocation(
|
||||
quantity=item.quantity, notes=notes, user=user
|
||||
)
|
||||
|
||||
@tracer.start_as_current_span('consume_build_line')
|
||||
def consume_build_line(line_id: int, notes: str = '', user_id: int | None = None):
|
||||
"""Consume stock against a particular BuildOrderLineItem."""
|
||||
from build.models import BuildLine
|
||||
|
||||
line_item = BuildLine.objects.filter(pk=line_id).first()
|
||||
|
||||
if not line_item:
|
||||
logger.warning(
|
||||
'Could not consume stock for LineItem <%s> - LineItem does not exist',
|
||||
line_id,
|
||||
)
|
||||
return
|
||||
|
||||
for item in line_item.allocations.all():
|
||||
item.complete_allocation(
|
||||
quantity=item.quantity,
|
||||
notes=notes,
|
||||
user=User.objects.filter(pk=user_id).first() if user_id else None,
|
||||
)
|
||||
# Consume each of the specified BuildItem objects
|
||||
for item_id, quantity in items.items():
|
||||
if build_item := BuildItem.objects.filter(
|
||||
pk=item_id, build_line__build=build
|
||||
).first():
|
||||
build_item.complete_allocation(
|
||||
quantity=quantity, notes=notes, user=user
|
||||
)
|
||||
|
||||
|
||||
@tracer.start_as_current_span('complete_build_allocations')
|
||||
|
|
@ -89,7 +83,7 @@ def complete_build_allocations(build_id: int, user_id: int):
|
|||
"""Complete build allocations for a specified BuildOrder."""
|
||||
from build.models import Build
|
||||
|
||||
build_order = Build.objects.filter(pk=build_id).first()
|
||||
build_order = Build.objects.get(pk=build_id)
|
||||
|
||||
if user_id:
|
||||
try:
|
||||
|
|
@ -103,13 +97,6 @@ def complete_build_allocations(build_id: int, user_id: int):
|
|||
else:
|
||||
user = None
|
||||
|
||||
if not build_order:
|
||||
logger.warning(
|
||||
'Could not complete build allocations for BuildOrder <%s> - BuildOrder does not exist',
|
||||
build_id,
|
||||
)
|
||||
return
|
||||
|
||||
build_order.complete_allocations(user)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -970,12 +970,12 @@ class BuildAllocationTest(BuildAPITest):
|
|||
url = reverse('api-build-auto-allocate', kwargs={'pk': build.pk})
|
||||
|
||||
# Allocate only 'untracked' items - this should not allocate our tracked item
|
||||
self.post(url, data={'item_type': 'untracked'})
|
||||
self.post(url, data={'item_type': 'untracked'}, expected_code=200)
|
||||
|
||||
self.assertEqual(N, BuildItem.objects.count())
|
||||
|
||||
# Allocate 'tracked' items - this should allocate our tracked item
|
||||
self.post(url, data={'item_type': 'tracked'})
|
||||
self.post(url, data={'item_type': 'tracked'}, expected_code=200)
|
||||
|
||||
# A new BuildItem should have been created
|
||||
self.assertEqual(N + 1, BuildItem.objects.count())
|
||||
|
|
@ -1564,12 +1564,13 @@ class BuildLineTests(BuildAPITest):
|
|||
|
||||
# Filter by 'available' status
|
||||
# Note: The max_query_time is bumped up here, as postgresql backend has some strange issues (only during testing)
|
||||
response = self.get(url, data={'available': True}, max_query_time=15)
|
||||
# TODO: This needs to be addressed in the future, as 25 seconds is an unacceptably long time for a query to take in testing
|
||||
response = self.get(url, data={'available': True}, max_query_time=25)
|
||||
n_t = len(response.data)
|
||||
self.assertGreater(n_t, 0)
|
||||
|
||||
# Note: The max_query_time is bumped up here, as postgresql backend has some strange issues (only during testing)
|
||||
response = self.get(url, data={'available': False}, max_query_time=15)
|
||||
response = self.get(url, data={'available': False}, max_query_time=25)
|
||||
n_f = len(response.data)
|
||||
self.assertGreater(n_f, 0)
|
||||
|
||||
|
|
@ -1735,7 +1736,7 @@ class BuildConsumeTest(BuildAPITest):
|
|||
'lines': [{'build_line': line.pk} for line in self.build.build_lines.all()]
|
||||
}
|
||||
|
||||
self.post(url, data, expected_code=201)
|
||||
self.post(url, data, expected_code=200)
|
||||
|
||||
self.assertEqual(self.build.allocated_stock.count(), 0)
|
||||
self.assertEqual(self.build.consumed_stock.count(), 3)
|
||||
|
|
@ -1758,7 +1759,7 @@ class BuildConsumeTest(BuildAPITest):
|
|||
]
|
||||
}
|
||||
|
||||
self.post(url, data, expected_code=201)
|
||||
self.post(url, data, expected_code=200)
|
||||
|
||||
self.assertEqual(self.build.allocated_stock.count(), 0)
|
||||
self.assertEqual(self.build.consumed_stock.count(), 3)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,24 @@ class ParameterAdmin(admin.ModelAdmin):
|
|||
search_fields = ('template__name', 'data', 'note')
|
||||
|
||||
|
||||
class SelectionListEntryInlineAdmin(admin.StackedInline):
|
||||
"""Inline admin class for the SelectionListEntry model."""
|
||||
|
||||
model = common.models.SelectionListEntry
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(common.models.SelectionList)
|
||||
class SelectionListAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for SelectionList objects."""
|
||||
|
||||
list_display = ('name', 'description', 'active', 'locked')
|
||||
search_fields = ('name', 'description')
|
||||
list_filter = ('active', 'locked')
|
||||
|
||||
inlines = [SelectionListEntryInlineAdmin]
|
||||
|
||||
|
||||
@admin.register(common.models.Attachment)
|
||||
class AttachmentAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for Attachment objects."""
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ from django.views.decorators.csrf import csrf_exempt
|
|||
|
||||
import django_filters.rest_framework.filters as rest_filters
|
||||
import django_q.models
|
||||
import django_q.tasks
|
||||
from django_filters.rest_framework.filterset import FilterSet
|
||||
from django_q.tasks import async_task
|
||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from error_report.models import Error
|
||||
|
|
@ -115,7 +115,7 @@ class WebhookView(CsrfExemptMixin, APIView):
|
|||
# process data
|
||||
message = self.webhook.save_data(payload, headers, request)
|
||||
if self.run_async:
|
||||
async_task(self._process_payload, message.id)
|
||||
django_q.tasks.async_task(self._process_payload, message.id)
|
||||
else:
|
||||
self._process_result(
|
||||
self.webhook.process_payload(message, payload, headers), message
|
||||
|
|
@ -564,13 +564,29 @@ class ErrorMessageDetail(RetrieveUpdateDestroyAPI):
|
|||
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
|
||||
|
||||
|
||||
class BackgroundTaskDetail(APIView):
|
||||
"""Detail view for a single background task."""
|
||||
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
@extend_schema(responses={200: common.serializers.TaskDetailSerializer})
|
||||
def get(self, request, task_id, *args, **kwargs):
|
||||
"""Fetch information regarding a particular background task ID."""
|
||||
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
|
||||
|
||||
return Response(response, status=response['http_status'])
|
||||
|
||||
|
||||
class BackgroundTaskOverview(APIView):
|
||||
"""Provides an overview of the background task queue status."""
|
||||
|
||||
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
|
||||
serializer_class = None
|
||||
|
||||
@extend_schema(responses={200: common.serializers.TaskOverviewSerializer})
|
||||
@extend_schema(
|
||||
operation_id='background_task_overview',
|
||||
responses={200: common.serializers.TaskOverviewSerializer},
|
||||
)
|
||||
def get(self, request, fmt=None):
|
||||
"""Return information about the current status of the background task queue."""
|
||||
import django_q.models as q_models
|
||||
|
|
@ -608,7 +624,7 @@ class ScheduledTaskList(ListAPI):
|
|||
|
||||
ordering_fields = ['pk', 'func', 'last_run', 'next_run']
|
||||
|
||||
search_fields = ['func']
|
||||
search_fields = ['func', 'name']
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return annotated queryset."""
|
||||
|
|
@ -1150,6 +1166,14 @@ class EntryMixin:
|
|||
class SelectionEntryList(EntryMixin, ListCreateAPI):
|
||||
"""List view for SelectionEntry objects."""
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
ordering_fields = ['list', 'label', 'active']
|
||||
|
||||
search_fields = ['label', 'description']
|
||||
|
||||
filterset_fields = ['active', 'value', 'list']
|
||||
|
||||
|
||||
class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI):
|
||||
"""Detail view for a SelectionEntry object."""
|
||||
|
|
@ -1162,6 +1186,22 @@ class DataOutputEndpointMixin:
|
|||
serializer_class = common.serializers.DataOutputSerializer
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the set of DataOutput objects which the user has permission to view."""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
try:
|
||||
user = self.request.user
|
||||
except AttributeError:
|
||||
raise PermissionDenied('User information is not available')
|
||||
|
||||
# Allow staff users access to all DataOutput objects
|
||||
if user.is_staff:
|
||||
return queryset
|
||||
|
||||
# All other users are limited to viewing their own DataOutput objects
|
||||
return queryset.filter(user=user)
|
||||
|
||||
|
||||
class DataOutputList(DataOutputEndpointMixin, BulkDeleteMixin, ListAPI):
|
||||
"""List view for DataOutput objects."""
|
||||
|
|
@ -1396,6 +1436,9 @@ common_api_urls = [
|
|||
name='api-scheduled-task-list',
|
||||
),
|
||||
path('failed/', FailedTaskList.as_view(), name='api-failed-task-list'),
|
||||
path(
|
||||
'<str:task_id>/', BackgroundTaskDetail.as_view(), name='api-task-detail'
|
||||
),
|
||||
path('', BackgroundTaskOverview.as_view(), name='api-task-overview'),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -230,6 +230,6 @@ def get_price(
|
|||
quantity = decimal.Decimal(f'{quantity}')
|
||||
|
||||
if pb_found:
|
||||
cost = pb_cost * quantity
|
||||
cost = decimal.Decimal(pb_cost) * quantity
|
||||
return InvenTree.helpers.normalize(cost + instance.base_cost)
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import json
|
|||
import math
|
||||
import os
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta, timezone
|
||||
from email.utils import make_msgid
|
||||
from enum import Enum
|
||||
|
|
@ -85,8 +86,8 @@ class RenderMeta(enums.ChoicesType):
|
|||
return []
|
||||
|
||||
|
||||
class RenderChoices(models.TextChoices, metaclass=RenderMeta): # type: ignore
|
||||
"""Class for creating enumerated string choices for schema rendering."""
|
||||
class RenderChoices(models.TextChoices, metaclass=RenderMeta):
|
||||
"""Class for creating enumerated string choices for schema rendering.""" # ty:ignore[conflicting-metaclass]
|
||||
|
||||
|
||||
class MetaMixin(models.Model):
|
||||
|
|
@ -363,9 +364,15 @@ class BaseInvenTreeSetting(models.Model):
|
|||
|
||||
# Specify any "default" values which are not in the database
|
||||
settings_definition = settings_definition or cls.SETTINGS
|
||||
|
||||
all_settings = OrderedDict()
|
||||
|
||||
for key, setting in settings_definition.items():
|
||||
if key.upper() not in settings:
|
||||
settings[key.upper()] = cls(
|
||||
# If the setting is already in the database, use that value
|
||||
if key.upper() in settings:
|
||||
all_settings[key] = settings[key.upper()]
|
||||
else:
|
||||
all_settings[key.upper()] = cls(
|
||||
key=key.upper(),
|
||||
value=cls.get_setting_default(key, **filters),
|
||||
**filters,
|
||||
|
|
@ -373,10 +380,10 @@ class BaseInvenTreeSetting(models.Model):
|
|||
|
||||
# remove any hidden settings
|
||||
if exclude_hidden and setting.get('hidden', False):
|
||||
del settings[key.upper()]
|
||||
del all_settings[key.upper()]
|
||||
|
||||
# format settings values and remove protected
|
||||
for key, setting in settings.items():
|
||||
for key, setting in all_settings.items():
|
||||
validator = cls.get_setting_validator(key, **filters)
|
||||
|
||||
if cls.is_protected(key, **filters) and setting.value != '':
|
||||
|
|
@ -389,7 +396,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||
except ValueError:
|
||||
setting.value = cls.get_setting_default(key, **filters)
|
||||
|
||||
return settings
|
||||
return all_settings
|
||||
|
||||
@classmethod
|
||||
def allValues(
|
||||
|
|
@ -1084,7 +1091,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||
|
||||
return self.__class__.validator_is_bool(validator)
|
||||
|
||||
def as_bool(self):
|
||||
def as_bool(self) -> bool:
|
||||
"""Return the value of this setting converted to a boolean value.
|
||||
|
||||
Warning: Only use on values where is_bool evaluates to true!
|
||||
|
|
@ -2313,11 +2320,39 @@ class SelectionList(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeMo
|
|||
"""Return the API URL associated with the SelectionList model."""
|
||||
return reverse('api-selectionlist-list')
|
||||
|
||||
def get_choices(self):
|
||||
"""Return the choices for the selection list."""
|
||||
choices = self.entries.filter(active=True)
|
||||
def get_choices(self, active: Optional[bool] = True):
|
||||
"""Return the choices for the selection list.
|
||||
|
||||
Arguments:
|
||||
active: If specified, filter choices by active status
|
||||
|
||||
Returns:
|
||||
List of choice values for this selection list
|
||||
"""
|
||||
choices = self.entries.all()
|
||||
|
||||
if active is not None:
|
||||
choices = choices.filter(active=active)
|
||||
|
||||
return [c.value for c in choices]
|
||||
|
||||
def has_choice(self, value: str, active: Optional[bool] = None):
|
||||
"""Check if the selection list has a particular choice.
|
||||
|
||||
Arguments:
|
||||
value: The value to check for
|
||||
active: If specified, filter choices by active status
|
||||
|
||||
Returns:
|
||||
True if the choice exists in the selection list, False otherwise
|
||||
"""
|
||||
choices = self.entries.all()
|
||||
|
||||
if active is not None:
|
||||
choices = choices.filter(active=active)
|
||||
|
||||
return choices.filter(value=value).exists()
|
||||
|
||||
|
||||
class SelectionListEntry(models.Model):
|
||||
"""Class which represents a single entry in a SelectionList.
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class SettingsValueField(serializers.Field):
|
|||
"""Return the object instance, not the attribute value."""
|
||||
return instance
|
||||
|
||||
def to_representation(self, instance: common_models.InvenTreeSetting) -> str:
|
||||
def to_representation(self, instance: common_models.InvenTreeSetting):
|
||||
"""Return the value of the setting.
|
||||
|
||||
Protected settings are returned as '***'
|
||||
|
|
@ -381,7 +381,7 @@ class ConfigSerializer(serializers.Serializer):
|
|||
"""Return the configuration data as a dictionary."""
|
||||
if not isinstance(instance, str):
|
||||
instance = list(instance.keys())[0]
|
||||
return {'key': instance, **self.instance[instance]}
|
||||
return {'key': instance, **self.instance.get(instance)}
|
||||
|
||||
|
||||
class NotesImageSerializer(InvenTreeModelSerializer):
|
||||
|
|
@ -457,7 +457,7 @@ class FlagSerializer(serializers.Serializer):
|
|||
data = {'key': instance, 'state': flag_state(instance, request=request)}
|
||||
|
||||
if request and request.user.is_superuser:
|
||||
data['conditions'] = self.instance[instance]
|
||||
data['conditions'] = self.instance.get(instance)
|
||||
|
||||
return data
|
||||
|
||||
|
|
@ -522,6 +522,78 @@ class ErrorMessageSerializer(InvenTreeModelSerializer):
|
|||
read_only_fields = ['when', 'info', 'data', 'path', 'pk']
|
||||
|
||||
|
||||
class TaskDetailSerializer(serializers.Serializer):
|
||||
"""Serializer for a background task detail."""
|
||||
|
||||
task_id = serializers.CharField(read_only=True)
|
||||
exists = serializers.BooleanField(read_only=True)
|
||||
pending = serializers.BooleanField(read_only=True)
|
||||
complete = serializers.BooleanField(read_only=True)
|
||||
success = serializers.BooleanField(read_only=True)
|
||||
http_status = serializers.IntegerField(read_only=True)
|
||||
|
||||
@classmethod
|
||||
def from_task(cls, task_id: str | bool | None) -> 'TaskDetailSerializer':
|
||||
"""Create a TaskDetailSerializer instance from a django_q Task.
|
||||
|
||||
Arguments:
|
||||
task_id: The ID of the task to retrieve details for.
|
||||
|
||||
Returns:
|
||||
An instance of TaskDetailSerializer with the task details.
|
||||
|
||||
Notes:
|
||||
- If the provided task_id is None, the task has not been run, or has errored out
|
||||
- If the provided task_id is a boolean, the task has been run synchronously, and the boolean value indicates success or failure
|
||||
- If the provided task_id is a string, the task has been offloaded to the background worker, and the details can be from the database
|
||||
|
||||
"""
|
||||
from InvenTree.tasks import get_queued_task
|
||||
|
||||
if task_id is None or type(task_id) is bool:
|
||||
# If the task_id is a boolean, the task has been run synchronously
|
||||
return cls({
|
||||
'task_id': '',
|
||||
'exists': False,
|
||||
'pending': False,
|
||||
'complete': task_id is not None,
|
||||
'success': False if task_id is None else bool(task_id),
|
||||
'http_status': 404 if task_id is None else 200,
|
||||
})
|
||||
|
||||
# A non-boolean result indicates that the task has been offloaded to the background worker
|
||||
success = django_q.models.Success.objects.filter(id=task_id).first()
|
||||
failure = django_q.models.Failure.objects.filter(id=task_id).first()
|
||||
task = (
|
||||
success
|
||||
or failure
|
||||
or django_q.models.Task.objects.filter(id=task_id).first()
|
||||
)
|
||||
queued = False
|
||||
|
||||
exists = bool(success or failure or task)
|
||||
|
||||
if not exists:
|
||||
# If the task has not been started yet, it may be present in the queue
|
||||
queued = bool(get_queued_task(task_id))
|
||||
|
||||
complete = bool(success) or bool(failure)
|
||||
|
||||
# Determine the http_status code for the task
|
||||
# - 200: Task exists and has been completed
|
||||
# - 404: Task does not exist
|
||||
http_status = 200 if exists or queued else 404
|
||||
|
||||
return cls({
|
||||
'task_id': task_id,
|
||||
'exists': exists or queued,
|
||||
'pending': queued,
|
||||
'complete': complete,
|
||||
'success': bool(success),
|
||||
'http_status': http_status,
|
||||
})
|
||||
|
||||
|
||||
class TaskOverviewSerializer(serializers.Serializer):
|
||||
"""Serializer for background task overview."""
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ def validate_part_name_format(value):
|
|||
})
|
||||
|
||||
# Attempt to render the template with a dummy Part instance
|
||||
p = Part(name='test part', description='some test part')
|
||||
# Use pk=1 to ensure conditional checks like {% if part.pk %} are evaluated
|
||||
p = Part(pk=1, name='test part', description='some test part')
|
||||
|
||||
try:
|
||||
SandboxedEnvironment().from_string(value).render({'part': p})
|
||||
|
|
@ -234,6 +235,18 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||
'validator': bool,
|
||||
'default': False,
|
||||
},
|
||||
'INVENTREE_SHOW_SUPERUSER_BANNER': {
|
||||
'name': _('Show superuser banner'),
|
||||
'description': _('Show a warning banner in the UI when logged in as superuser'),
|
||||
'validator': bool,
|
||||
'default': True,
|
||||
},
|
||||
'INVENTREE_SHOW_ADMIN_BANNER': {
|
||||
'name': _('Show admin banner'),
|
||||
'description': _('Show a warning banner in the UI when logged in as admin'),
|
||||
'validator': bool,
|
||||
'default': False,
|
||||
},
|
||||
'INVENTREE_COMPANY_NAME': {
|
||||
'name': _('Company name'),
|
||||
'description': _('Internal company name'),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class SettingsKeyType(TypedDict, total=False):
|
|||
validator: Validation function/list of functions for the setting (optional, default: None, e.g: bool, int, str, MinValueValidator, ...)
|
||||
default: Default value or function that returns default value (optional)
|
||||
choices: Function that returns or value of list[tuple[str: key, str: display value]] (optional)
|
||||
model_filters: Filters to apply when querying the associated model (optional)
|
||||
hidden: Hide this setting from settings page (optional)
|
||||
before_save: Function that gets called after save with *args, **kwargs (optional)
|
||||
after_save: Function that gets called after save with *args, **kwargs (optional)
|
||||
|
|
@ -42,6 +43,7 @@ class SettingsKeyType(TypedDict, total=False):
|
|||
validator: Callable | list[Callable] | tuple[Callable]
|
||||
default: Callable | Any
|
||||
choices: list[tuple[str, str]] | Callable[[], list[tuple[str, str]]]
|
||||
model_filters: dict[str, Any]
|
||||
hidden: bool
|
||||
before_save: Callable[..., None]
|
||||
after_save: Callable[..., None]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ def global_setting_overrides() -> dict:
|
|||
|
||||
def get_global_setting(key, backup_value=None, environment_key=None, **kwargs):
|
||||
"""Return the value of a global setting using the provided key."""
|
||||
import InvenTree.ready
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
if environment_key:
|
||||
|
|
@ -38,6 +39,10 @@ def get_global_setting(key, backup_value=None, environment_key=None, **kwargs):
|
|||
if backup_value is not None:
|
||||
kwargs['backup_value'] = backup_value
|
||||
|
||||
# Prevent database writes if we are in a read-only command
|
||||
if InvenTree.ready.isReadOnlyCommand():
|
||||
kwargs['create'] = False
|
||||
|
||||
return InvenTreeSetting.get_setting(key, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -53,6 +58,12 @@ def set_global_setting(key, value, change_user=None, create=True, **kwargs):
|
|||
kwargs['change_user'] = change_user
|
||||
kwargs['create'] = create
|
||||
|
||||
if get_global_setting(key, create=False, cache=False) == value:
|
||||
logger.debug(
|
||||
f'Global setting "{key}" already has the desired value, no update needed'
|
||||
)
|
||||
return True
|
||||
|
||||
return InvenTreeSetting.set_setting(key, value, **kwargs)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,44 @@ import common.models
|
|||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
|
||||
|
||||
class DataOutputAPITests(InvenTreeAPITestCase):
|
||||
"""API tests for the DataOutput endpoint."""
|
||||
|
||||
roles = 'all'
|
||||
|
||||
def setUp(self):
|
||||
"""Set up some test data for DataOutput API testing."""
|
||||
from report.models import DataOutput
|
||||
|
||||
super().setUp()
|
||||
|
||||
for ii in range(5):
|
||||
DataOutput.objects.create(
|
||||
output_type='test_output',
|
||||
user=self.user if ii % 2 == 0 else None,
|
||||
complete=ii % 2 == 1,
|
||||
)
|
||||
|
||||
def test_data_output_list(self):
|
||||
"""Test the DataOutput API list endpoint."""
|
||||
url = reverse('api-data-output-list')
|
||||
|
||||
# Non-staff user should only see outputs which are either enabled for all users, or created by themselves
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
response = self.get(url)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
for output in response.data:
|
||||
self.assertEqual(output['user'], self.user.pk)
|
||||
|
||||
# Set staff access = True, so we should see all outputs
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
response = self.get(url)
|
||||
self.assertEqual(len(response.data), 5)
|
||||
|
||||
|
||||
class ParameterAPITests(InvenTreeAPITestCase):
|
||||
"""Tests for the Parameter API."""
|
||||
|
||||
|
|
|
|||
|
|
@ -1107,6 +1107,41 @@ class TaskListApiTests(InvenTreeAPITestCase):
|
|||
for task in response.data:
|
||||
self.assertEqual(task['name'], 'time.sleep')
|
||||
|
||||
def test_task_detail(self):
|
||||
"""Test the BackgroundTaskDetail API endpoint."""
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
# Force run a task
|
||||
result = offload_task('fake_module.test_task', force_sync=True)
|
||||
self.assertFalse(result)
|
||||
self.assertEqual(type(result), bool)
|
||||
|
||||
# Schedule a dummy task - and ensure it offloads to the worker
|
||||
task_id = offload_task('fake_module.test_task', force_async=True)
|
||||
self.assertIsNotNone(task_id)
|
||||
self.assertEqual(type(task_id), str)
|
||||
|
||||
url = reverse('api-task-detail', kwargs={'task_id': task_id})
|
||||
|
||||
data = self.get(url, expected_code=200).data
|
||||
|
||||
self.assertEqual(data['task_id'], task_id)
|
||||
self.assertTrue(data['exists'])
|
||||
self.assertTrue(data['pending'])
|
||||
self.assertFalse(data['complete'])
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
# Perform a lookup for a non-existent task
|
||||
url = reverse('api-task-detail', kwargs={'task_id': 'doesnotexist'})
|
||||
|
||||
data = self.get(url, expected_code=404).data
|
||||
|
||||
self.assertEqual(data['task_id'], 'doesnotexist')
|
||||
self.assertFalse(data['exists'])
|
||||
self.assertFalse(data['pending'])
|
||||
self.assertFalse(data['complete'])
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
|
||||
class WebhookMessageTests(TestCase):
|
||||
"""Tests for webhooks."""
|
||||
|
|
@ -1317,12 +1352,18 @@ class NotificationTest(InvenTreeAPITestCase):
|
|||
|
||||
# Now, let's bulk delete all 'unread' notifications via the API,
|
||||
# but only associated with the logged in user
|
||||
response = self.delete(url, {'filters': {'read': False}}, expected_code=200)
|
||||
read_notifications = NotificationMessage.objects.filter(read=True)
|
||||
response = self.delete(
|
||||
url, {'items': [ntf.pk for ntf in read_notifications]}, expected_code=200
|
||||
)
|
||||
|
||||
# Only 7 notifications should have been deleted,
|
||||
# Only 3 notifications should have been deleted,
|
||||
# as the notifications associated with other users must remain untouched
|
||||
self.assertEqual(NotificationMessage.objects.count(), 13)
|
||||
self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3)
|
||||
self.assertEqual(NotificationMessage.objects.count(), 17)
|
||||
self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 7)
|
||||
self.assertEqual(
|
||||
NotificationMessage.objects.filter(user=self.user, read=True).count(), 0
|
||||
)
|
||||
|
||||
def test_simple(self):
|
||||
"""Test that a simple notification can be created."""
|
||||
|
|
@ -1534,9 +1575,14 @@ class CurrencyAPITests(InvenTreeAPITestCase):
|
|||
|
||||
# Updating via the external exchange may not work every time
|
||||
for _idx in range(5):
|
||||
self.post(
|
||||
reverse('api-currency-refresh'), expected_code=200, max_query_time=30
|
||||
)
|
||||
try:
|
||||
self.post(
|
||||
reverse('api-currency-refresh'),
|
||||
expected_code=200,
|
||||
max_query_time=30,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# There should be some new exchange rate objects now
|
||||
if Rate.objects.all().exists():
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ class ManufacturerPartMixin(SerializerContextMixin):
|
|||
|
||||
|
||||
class ManufacturerPartList(
|
||||
DataExportViewMixin,
|
||||
ManufacturerPartMixin,
|
||||
SerializerContextMixin,
|
||||
OutputOptionsMixin,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
from typing import TypedDict
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
|
|
@ -53,7 +54,7 @@ def rename_company_image(instance, filename):
|
|||
return os.path.join(base, fn)
|
||||
|
||||
|
||||
class CompanyReportContext(report.mixins.BaseReportContext):
|
||||
class CompanyReportContext(report.mixins.BaseReportContext, TypedDict):
|
||||
"""Report context for the Company model.
|
||||
|
||||
Attributes:
|
||||
|
|
@ -243,7 +244,11 @@ class Company(
|
|||
# We may have a pre-fetched primary address list
|
||||
if hasattr(self, 'primary_address_list'):
|
||||
addresses = self.primary_address_list
|
||||
return addresses[0] if len(addresses) > 0 else None
|
||||
return (
|
||||
addresses[0]
|
||||
if len(addresses) > 0 and isinstance(addresses, list)
|
||||
else None
|
||||
)
|
||||
|
||||
# Otherwise, query the database
|
||||
return self.addresses.filter(primary=True).first()
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ class InvenTreeCustomStatusSerializerMixin:
|
|||
"""Ensure the custom field is updated if the leader was changed."""
|
||||
self.gather_custom_fields()
|
||||
# Mirror values from leader to follower
|
||||
for field in self._custom_fields_leader:
|
||||
for field in self._custom_fields_leader or []:
|
||||
follower_field_name = f'{field}_custom_key'
|
||||
if (
|
||||
field in self.initial_data
|
||||
|
|
@ -205,7 +205,7 @@ class InvenTreeCustomStatusSerializerMixin:
|
|||
setattr(self.instance, follower_field_name, self.initial_data[field])
|
||||
|
||||
# Mirror values from follower to leader
|
||||
for field in self._custom_fields_follower:
|
||||
for field in self._custom_fields_follower or []:
|
||||
leader_field_name = field.replace('_custom_key', '')
|
||||
if field in validated_data and leader_field_name not in self.initial_data:
|
||||
try:
|
||||
|
|
@ -276,7 +276,7 @@ class InvenTreeCustomStatusSerializerMixin:
|
|||
|
||||
# Inherit choices from leader
|
||||
self.gather_custom_fields()
|
||||
if field_name in self._custom_fields:
|
||||
if self._custom_fields and field_name in self._custom_fields:
|
||||
leader_field_name = field_name.replace('_custom_key', '')
|
||||
leader_field = self.fields[leader_field_name]
|
||||
if hasattr(leader_field, 'choices'):
|
||||
|
|
|
|||
|
|
@ -73,6 +73,22 @@ class DataImportSessionMixin:
|
|||
serializer_class = importer.serializers.DataImportSessionSerializer
|
||||
permission_classes = [InvenTree.permissions.DataImporterPermission]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the set of DataImportSession objects that the user has permission to view."""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
try:
|
||||
user = self.request.user
|
||||
except AttributeError:
|
||||
raise PermissionDenied('User information is not available')
|
||||
|
||||
# Allow staff users access to all DataImportSession objects
|
||||
if user.is_staff:
|
||||
return queryset
|
||||
|
||||
# For non-staff users, only allow access to sessions that they have created
|
||||
return queryset.filter(user=user)
|
||||
|
||||
|
||||
class DataImportSessionList(BulkDeleteMixin, DataImportSessionMixin, ListCreateAPI):
|
||||
"""API endpoint for accessing a list of DataImportSession objects."""
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ class DataImportSerializerMixin:
|
|||
Determine if the serializer is being used for data import,
|
||||
and if so, adjust the serializer fields accordingly.
|
||||
"""
|
||||
importing = kwargs.pop('importing', False)
|
||||
self._is_importing = kwargs.pop('importing', False)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if importing:
|
||||
if self._is_importing:
|
||||
# Exclude any fields which are not able to be imported
|
||||
importable_field_names = list(self.get_importable_fields().keys())
|
||||
field_names = list(self.fields.keys())
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class DataImportSession(models.Model):
|
|||
|
||||
return supported_models().get(self.model_type, None)
|
||||
|
||||
def get_related_model(self, field_name: str) -> models.Model:
|
||||
def get_related_model(self, field_name: str) -> Optional[models.Model]:
|
||||
"""Return the related model for a given field name.
|
||||
|
||||
Arguments:
|
||||
|
|
@ -699,7 +699,7 @@ class DataImportRow(models.Model):
|
|||
if commit:
|
||||
self.save()
|
||||
|
||||
def convert_date_field(self, value: str) -> str:
|
||||
def convert_date_field(self, value: str) -> Optional[str]:
|
||||
"""Convert an incoming date field to the correct format for the database."""
|
||||
if value in [None, '']:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ class DataImportSerializerRegister:
|
|||
def register(self, serializer) -> None:
|
||||
"""Register a new serializer with the importer registry."""
|
||||
if not issubclass(serializer, DataImportSerializerMixin):
|
||||
logger.debug('Invalid serializer class: %s', type(serializer))
|
||||
logger.debug('Invalid serializer class: %s', serializer.__name__)
|
||||
return
|
||||
|
||||
if not issubclass(serializer, Serializer):
|
||||
logger.debug('Invalid serializer class: %s', type(serializer))
|
||||
logger.debug('Invalid serializer class: %s', serializer.__name__)
|
||||
return
|
||||
|
||||
logger.debug('Registering serializer class for import: %s', type(serializer))
|
||||
logger.debug('Registering serializer class for import: %s', serializer.__name__)
|
||||
|
||||
if serializer not in self.supported_serializers:
|
||||
self.supported_serializers.append(serializer)
|
||||
|
|
|
|||
|
|
@ -174,6 +174,36 @@ class ImportAPITest(ImporterMixin, InvenTreeAPITestCase):
|
|||
# Check that there are new database records
|
||||
self.assertEqual(PartCategory.objects.count(), N + 4)
|
||||
|
||||
def test_session_list(self):
|
||||
"""Test API endpoint which details the list of import sessions."""
|
||||
url = reverse('api-importer-session-list')
|
||||
|
||||
# Construct a dummy file
|
||||
f = self.helper_file('companies.csv')
|
||||
|
||||
for ii in range(5):
|
||||
DataImportSession.objects.create(
|
||||
data_file=f,
|
||||
model_type='company',
|
||||
user=self.user if ii % 2 == 0 else None,
|
||||
)
|
||||
|
||||
# Staff user should see all sessions
|
||||
self.user.is_staff = True
|
||||
self.user.save()
|
||||
|
||||
response = self.get(url)
|
||||
self.assertEqual(len(response.data), 5)
|
||||
|
||||
# Non-staff user should only see sessions which they own
|
||||
self.user.is_staff = False
|
||||
self.user.save()
|
||||
|
||||
response = self.get(url)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
for session in response.data:
|
||||
self.assertEqual(session['user'], self.user.pk)
|
||||
|
||||
|
||||
class AdminTest(ImporterMixin, AdminTestCase):
|
||||
"""Tests for the admin interface integration."""
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue