Merge branch 'master' into pr/ChristianSchindler/6305

This commit is contained in:
Oliver 2024-11-05 16:02:23 +11:00 committed by GitHub
commit c46fd85ad0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
662 changed files with 331635 additions and 236499 deletions

View File

@ -11,10 +11,10 @@ python3 -m venv /home/inventree/dev/venv --system-site-packages --upgrade-deps
invoke update -s
# Configure dev environment
invoke setup-dev
invoke dev.setup-dev
# Install required frontend packages
invoke frontend-install
invoke dev.frontend-install
# remove existing gitconfig created by "Avoiding Dubious Ownership" step
# so that it gets copied from host to the container to have your global

View File

@ -9,7 +9,7 @@ runs:
shell: bash
run: |
invoke migrate
invoke import-fixtures
invoke dev.import-fixtures
invoke export-records -f data.json
python3 ./src/backend/InvenTree/manage.py flush --noinput
invoke migrate

View File

@ -71,7 +71,7 @@ def check_prohibited_tags(data):
for filename in pathlib.Path(js_i18n_dir).rglob('*.js'):
print(f"Checking file 'translated/{os.path.basename(filename)}':")
with open(filename, 'r') as js_file:
with open(filename, encoding='utf-8') as js_file:
data = js_file.readlines()
errors += check_invalid_tag(data)
@ -81,7 +81,7 @@ for filename in pathlib.Path(js_dynamic_dir).rglob('*.js'):
print(f"Checking file 'dynamic/{os.path.basename(filename)}':")
# Check that the 'dynamic' files do not contains any translated strings
with open(filename, 'r') as js_file:
with open(filename, encoding='utf-8') as js_file:
data = js_file.readlines()
invalid_tags = ['blocktrans', 'blocktranslate', 'trans', 'translate']

View File

@ -20,9 +20,9 @@ for line in str(out.decode()).split('\n'):
if len(migrations) == 0:
sys.exit(0)
print('There are {n} unstaged migration files:'.format(n=len(migrations)))
print(f'There are {len(migrations)} unstaged migration files:')
for m in migrations:
print(' - {m}'.format(m=m))
print(f' - {m}')
sys.exit(len(migrations))

100
.github/scripts/check_source_strings.py vendored Normal file
View File

@ -0,0 +1,100 @@
"""Script to check source strings for translations."""
import argparse
import os
import rapidfuzz
BACKEND_SOURCE_FILE = [
'..',
'..',
'src',
'backend',
'InvenTree',
'locale',
'en',
'LC_MESSAGES',
'django.po',
]
FRONTEND_SOURCE_FILE = [
'..',
'..',
'src',
'frontend',
'src',
'locales',
'en',
'messages.po',
]
def extract_source_strings(file_path):
"""Extract source strings from the provided file."""
here = os.path.abspath(os.path.dirname(__file__))
abs_file_path = os.path.abspath(os.path.join(here, *file_path))
sources = []
with open(abs_file_path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('msgid '):
msgid = line[6:].strip()
if msgid in sources:
print(f'Duplicate source string: {msgid}')
else:
sources.append(msgid)
return sources
def compare_source_strings(sources, threshold):
"""Compare source strings to find duplicates (or close matches)."""
issues = 0
for i, source in enumerate(sources):
for other in sources[i + 1 :]:
if other.lower() == source.lower():
print(f'- Duplicate: {source} ~ {other}')
issues += 1
continue
ratio = rapidfuzz.fuzz.ratio(source, other)
if ratio > threshold:
print(f'- Close match: {source} ~ {other} ({ratio:.1f}%)')
issues += 1
if issues:
print(f' - Found {issues} issues.')
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Check source strings for translations.'
)
parser.add_argument(
'--backend', action='store_true', help='Check backend source strings'
)
parser.add_argument(
'--frontend', action='store_true', help='Check frontend source strings'
)
parser.add_argument(
'--threshold',
type=int,
help='Set the threshold for string comparison',
default=99,
)
args = parser.parse_args()
if args.backend:
backend_sources = extract_source_strings(BACKEND_SOURCE_FILE)
print('Backend source strings:', len(backend_sources))
compare_source_strings(backend_sources, args.threshold)
if args.frontend:
frontend_sources = extract_source_strings(FRONTEND_SOURCE_FILE)
print('Frontend source strings:', len(frontend_sources))
compare_source_strings(frontend_sources, args.threshold)

View File

@ -89,7 +89,7 @@ def check_version_number(version_string, allow_duplicate=False):
if release > version_tuple:
highest_release = False
print(f'Found newer release: {str(release)}')
print(f'Found newer release: {release!s}')
return highest_release
@ -134,7 +134,7 @@ if __name__ == '__main__':
version = None
with open(version_file, 'r') as f:
with open(version_file, encoding='utf-8') as f:
text = f.read()
# Extract the InvenTree software version
@ -175,10 +175,7 @@ if __name__ == '__main__':
print(f"Version number '{version}' does not match tag '{version_tag}'")
sys.exit
if highest_release:
docker_tags = [version_tag, 'stable']
else:
docker_tags = [version_tag]
docker_tags = [version_tag, 'stable'] if highest_release else [version_tag]
elif GITHUB_REF_TYPE == 'branch':
# Otherwise we know we are targeting the 'master' branch
@ -202,7 +199,7 @@ if __name__ == '__main__':
target_repos = [REPO.lower(), f'ghcr.io/{REPO.lower()}']
# Ref: https://getridbug.com/python/how-to-set-environment-variables-in-github-actions-using-python/
with open(os.getenv('GITHUB_ENV'), 'a') as env_file:
with open(os.getenv('GITHUB_ENV'), 'a', encoding='utf-8') as env_file:
# Construct tag string
tag_list = [[f'{r}:{t}' for t in docker_tags] for r in target_repos]
tags = ','.join(itertools.chain(*tag_list))

View File

@ -25,7 +25,7 @@ jobs:
)
steps:
- name: Backport Action
uses: sqren/backport-github-action@f54e19901f2a57f8b82360f2490d47ee82ec82c6 # pin@v9.2.2
uses: sqren/backport-github-action@ad888e978060bc1b2798690dd9d03c4036560947 # pin@v9.2.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
auto_backport_label_prefix: backport-to-

View File

@ -30,13 +30,16 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
install: true
apt-dependency: gettext
- name: Test Translations
run: invoke translate
run: invoke dev.translate
- name: Check for Duplicates
run: |
python ./.github/scripts/check_source_strings.py --frontend --backend
- name: Check Migration Files
run: python3 .github/scripts/check_migration_files.py

View File

@ -39,7 +39,7 @@ jobs:
docker: ${{ steps.filter.outputs.docker }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
id: filter
with:
@ -66,9 +66,9 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Set Up Python ${{ env.python_version }}
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # pin@v5.2.0
with:
python-version: ${{ env.python_version }}
- name: Version Check
@ -97,7 +97,7 @@ jobs:
run: |
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke install
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke update
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke setup-dev
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke dev.setup-dev
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml up -d
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run inventree-dev-server invoke wait
- name: Check Data Directory
@ -115,10 +115,10 @@ jobs:
- name: Run Unit Tests
run: |
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> contrib/container/docker.dev.env
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke test --disable-pty
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke dev.test --disable-pty
- name: Run Migration Tests
run: |
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke test --migrations
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke dev.test --migrations
- name: Clean up test folder
run: |
rm -rf InvenTree/_testfolder
@ -127,10 +127,10 @@ jobs:
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # pin@v3.2.0
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # pin@v3.6.1
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # pin@v3.7.1
- name: Set up cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # pin@v3.6.0
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # pin@v3.7.0
- name: Check if Dockerhub login is required
id: docker_login
run: |
@ -166,7 +166,7 @@ jobs:
- name: Push Docker Images
id: push-docker
if: github.event_name != 'pull_request'
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # pin@v6.7.0
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # pin@v6.9.0
with:
context: .
file: ./contrib/container/Dockerfile

View File

@ -38,7 +38,7 @@ jobs:
force: ${{ steps.force.outputs.force }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # pin@v3.0.2
id: filter
with:
@ -70,7 +70,7 @@ jobs:
needs: ["pre-commit"]
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -92,9 +92,9 @@ jobs:
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.force == 'true'
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # pin@v5.2.0
with:
python-version: ${{ env.python_version }}
cache: "pip"
@ -113,9 +113,9 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Set up Python ${{ env.python_version }}
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # pin@v5.1.1
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # pin@v5.2.0
with:
python-version: ${{ env.python_version }}
- name: Check Config
@ -149,7 +149,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -157,9 +157,9 @@ jobs:
dev-install: true
update: true
- name: Export API Documentation
run: invoke schema --ignore-warnings --filename src/backend/InvenTree/schema.yml
run: invoke dev.schema --ignore-warnings --filename src/backend/InvenTree/schema.yml
- name: Upload schema
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # pin@v4.3.6
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # pin@v4.4.3
with:
name: schema.yml
path: src/backend/InvenTree/schema.yml
@ -177,7 +177,7 @@ jobs:
echo "Downloaded api.yaml"
- name: Running OpenAPI Spec diff action
id: breaking_changes
uses: oasdiff/oasdiff-action/diff@a2ff6682b27d175162a74c09ace8771bd3d512f8 # pin@main
uses: oasdiff/oasdiff-action/diff@1c611ffb1253a72924624aa4fb662e302b3565d3 # pin@main
with:
base: 'api.yaml'
revision: 'src/backend/InvenTree/schema.yml'
@ -191,7 +191,7 @@ jobs:
diff --color -u src/backend/InvenTree/schema.yml api.yaml
diff -u src/backend/InvenTree/schema.yml api.yaml && echo "no difference in API schema " || exit 2
- name: Check schema - including warnings
run: invoke schema
run: invoke dev.schema
continue-on-error: true
- name: Extract version for publishing
id: version
@ -211,7 +211,7 @@ jobs:
version: ${{ needs.schema.outputs.version }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
name: Checkout Code
with:
repository: inventree/schema
@ -250,7 +250,7 @@ jobs:
INVENTREE_SITE_URL: http://127.0.0.1:12345
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -262,9 +262,9 @@ jobs:
run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
- name: Start InvenTree Server
run: |
invoke delete-data -f
invoke import-fixtures
invoke server -a 127.0.0.1:12345 &
invoke dev.delete-data -f
invoke dev.import-fixtures
invoke dev.server -a 127.0.0.1:12345 &
invoke wait
- name: Run Tests For `${{ env.wrapper_name }}`
run: |
@ -292,7 +292,7 @@ jobs:
python_version: ${{ matrix.python_version }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -302,13 +302,13 @@ jobs:
- name: Data Export Test
uses: ./.github/actions/migration
- name: Test Translations
run: invoke translate
run: invoke dev.translate
- name: Check Migration Files
run: python3 .github/scripts/check_migration_files.py
- name: Coverage Tests
run: invoke test --coverage
run: invoke dev.test --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@v4.5.0
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # pin@v4.6.0
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -346,7 +346,7 @@ jobs:
- 6379:6379
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -355,7 +355,7 @@ jobs:
dev-install: true
update: true
- name: Run Tests
run: invoke test
run: invoke dev.test
- name: Data Export Test
uses: ./.github/actions/migration
@ -390,7 +390,7 @@ jobs:
- 3306:3306
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -399,7 +399,7 @@ jobs:
dev-install: true
update: true
- name: Run Tests
run: invoke test
run: invoke dev.test
- name: Data Export Test
uses: ./.github/actions/migration
@ -429,7 +429,7 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -438,9 +438,9 @@ jobs:
dev-install: true
update: true
- name: Run Tests
run: invoke test --migrations --report --coverage
run: invoke dev.test --migrations --report --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@v4.5.0
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # pin@v4.6.0
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -460,7 +460,7 @@ jobs:
INVENTREE_PLUGINS_ENABLED: false
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
name: Checkout Code
- name: Environment Setup
uses: ./.github/actions/setup
@ -517,7 +517,7 @@ jobs:
VITE_COVERAGE: true
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -525,17 +525,17 @@ jobs:
install: true
update: true
- name: Set up test data
run: invoke setup-test -i
run: invoke dev.setup-test -i
- name: Rebuild thumbnails
run: invoke rebuild-thumbnails
run: invoke int.rebuild-thumbnails
- name: Install dependencies
run: inv frontend-compile
run: invoke int.frontend-compile
- name: Install Playwright Browsers
run: cd src/frontend && npx playwright install --with-deps
- name: Run Playwright tests
id: tests
run: cd src/frontend && npx nyc playwright test
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # pin@v4
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # pin@v4
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
with:
name: playwright-report
@ -545,12 +545,19 @@ jobs:
if: always()
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@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # pin@v4.5.0
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # pin@v4.6.0
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: inventree/InvenTree
flags: pui
- name: Upload bundler info
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: |
cd src/frontend
yarn install
yarn run build
platform_ui_build:
name: Build - UI Platform
@ -558,7 +565,7 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -573,7 +580,7 @@ jobs:
run: |
cd src/backend/InvenTree/web/static
zip -r frontend-build.zip web/ web/.vite
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # pin@v4.3.6
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # pin@v4.4.3
with:
name: frontend-build
path: src/backend/InvenTree/web/static/web

View File

@ -18,7 +18,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Version Check
run: |
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
@ -39,7 +39,7 @@ jobs:
contents: write
attestations: write
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -49,7 +49,7 @@ jobs:
- name: Build frontend
run: cd src/frontend && npm run compile && npm run build
- name: Create SBOM for frontend
uses: anchore/sbom-action@v0
uses: anchore/sbom-action@8d0a6505bf28ced3e85154d13dc6af83299e13f1 # pin@v0
with:
artifact-name: frontend-build.spdx
path: src/frontend
@ -63,7 +63,7 @@ jobs:
zip -r ../frontend-build.zip * .vite
- name: Attest Build Provenance
id: attest
uses: actions/attest-build-provenance@v1
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # pin@v1
with:
subject-path: "${{ github.workspace }}/src/backend/InvenTree/web/static/frontend-build.zip"

View File

@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
persist-credentials: false
@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: SARIF file
path: results.sarif
@ -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@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
with:
sarif_file: results.sarif

View File

@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # pin@v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # pin@v4.2.1
- name: Environment Setup
uses: ./.github/actions/setup
with:
@ -39,9 +39,19 @@ jobs:
npm: true
apt-dependency: gettext
- name: Make Translations
run: invoke translate
run: invoke dev.translate
- name: Remove compiled static files
run: rm -rf src/backend/InvenTree/static
- name: Remove all local changes that are not *.po files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add src/backend/InvenTree/locale/en/LC_MESSAGES/django.po src/frontend/src/locales/en/messages.po
git commit -m "add translations" || true
git reset --hard
git reset HEAD~
- name: crowdin action
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # pin@v2
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # pin@v2
with:
upload_sources: true
upload_translations: false
@ -50,7 +60,7 @@ jobs:
create_pull_request: true
pull_request_title: 'New Crowdin updates'
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'l10'
pull_request_base_branch_name: 'master'
pull_request_labels: 'translations'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -31,6 +31,7 @@ var/
# Django stuff:
*.log
local_settings.py
*.sqlite
*.sqlite3
*.sqlite3-journal
*.backup

View File

@ -17,17 +17,18 @@ repos:
- id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.1
rev: v0.7.0
hooks:
- id: ruff-format
args: [--preview]
- id: ruff
args: [
--fix,
# --unsafe-fixes,
--preview
]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.2.13
rev: 0.4.24
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
@ -50,7 +51,7 @@ repos:
args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, --no-strip-extras, --generate-hashes]
files: contrib/container/requirements\.(in|txt)$
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.34.1
rev: v1.35.2
hooks:
- id: djlint-django
- repo: https://github.com/codespell-project/codespell
@ -77,7 +78,7 @@ repos:
- "prettier@^2.4.1"
- "@trivago/prettier-plugin-sort-imports"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v9.6.0"
rev: "v9.12.0"
hooks:
- id: eslint
additional_dependencies:
@ -89,7 +90,7 @@ repos:
- "@typescript-eslint/parser"
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
rev: v8.21.0
hooks:
- id: gitleaks
#- repo: https://github.com/jumanjihouse/pre-commit-hooks

20
.vscode/tasks.json vendored
View File

@ -9,61 +9,61 @@
{
"label": "worker",
"type": "shell",
"command": "inv worker",
"command": "invoke worker",
"problemMatcher": [],
},
{
"label": "clean-settings",
"type": "shell",
"command": "inv clean-settings",
"command": "invoke int.clean-settings",
"problemMatcher": [],
},
{
"label": "delete-data",
"type": "shell",
"command": "inv delete-data",
"command": "invoke dev.delete-data",
"problemMatcher": [],
},
{
"label": "migrate",
"type": "shell",
"command": "inv migrate",
"command": "invoke migrate",
"problemMatcher": [],
},
{
"label": "server",
"type": "shell",
"command": "inv server",
"command": "invoke dev.server",
"problemMatcher": [],
},
{
"label": "setup-dev",
"type": "shell",
"command": "inv setup-dev",
"command": "invoke dev.setup-dev",
"problemMatcher": [],
},
{
"label": "setup-test",
"type": "shell",
"command": "inv setup-test -i --path dev/inventree-demo-dataset",
"command": "invoke dev.setup-test -i --path dev/inventree-demo-dataset",
"problemMatcher": [],
},
{
"label": "superuser",
"type": "shell",
"command": "inv superuser",
"command": "invoke superuser",
"problemMatcher": [],
},
{
"label": "test",
"type": "shell",
"command": "inv test",
"command": "invoke dev.test",
"problemMatcher": [],
},
{
"label": "update",
"type": "shell",
"command": "inv update",
"command": "invoke update",
"problemMatcher": [],
},
]

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2022 InvenTree
Copyright (c) 2017 - InvenTree Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -4,7 +4,7 @@
<p>Open Source Inventory Management System </p>
<!-- Badges -->
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/inventree/inventree)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/MIT)![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/inventree/inventree)
![CI](https://github.com/inventree/inventree/actions/workflows/qc_checks.yaml/badge.svg)
[![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest)
![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
@ -81,7 +81,7 @@ InvenTree is designed to be **extensible**, and provides multiple options for **
</details>
<details>
<summary>Client</summary>
<summary>Client - CUI</summary>
<ul>
<li><a href="https://getbootstrap.com/">Bootstrap</a></li>
<li><a href="https://jquery.com/">jQuery</a></li>
@ -89,13 +89,27 @@ InvenTree is designed to be **extensible**, and provides multiple options for **
</ul>
</details>
<details>
<summary>Client - PUI</summary>
<ul>
<li><a href="https://react.dev/">React</a></li>
<li><a href="https://lingui.dev/">Lingui</a></li>
<li><a href="https://reactrouter.com/">React Router</a></li>
<li><a href="https://tanstack.com/query/">TanStack Query</a></li>
<li><a href="https://github.com/pmndrs/zustand">Zustand</a></li>
<li><a href="https://mantine.dev/">Mantine</a></li>
<li><a href="https://icflorescu.github.io/mantine-datatable/">Mantine Data Table</a></li>
<li><a href="https://codemirror.net/">CodeMirror</a></li>
</ul>
</details>
<details>
<summary>DevOps</summary>
<ul>
<li><a href="https://hub.docker.com/r/inventree/inventree">Docker</a></li>
<li><a href="https://crowdin.com/project/inventree">Crowdin</a></li>
<li><a href="https://app.codecov.io/gh/inventree/InvenTree">Codecov</a></li>
<li><a href="https://app.deepsource.com/gh/inventree/InvenTree">DeepSource</a></li>
<li><a href="https://sonarcloud.io/project/overview?id=inventree_InvenTree">SonarCloud</a></li>
<li><a href="https://packager.io/gh/inventree/InvenTree">Packager.io</a></li>
</ul>
</details>

View File

@ -27,3 +27,11 @@ flag_management:
statuses:
- type: project
target: 45%
comment:
require_bundle_changes: True
bundle_change_threshold: "1Kb"
bundle_analysis:
warning_threshold: "5%"
status: "informational"

View File

@ -115,7 +115,7 @@ RUN apk add --no-cache --update nodejs npm yarn
RUN yarn config set network-timeout 600000 -g
COPY src ${INVENTREE_HOME}/src
COPY tasks.py ${INVENTREE_HOME}/tasks.py
RUN cd ${INVENTREE_HOME} && inv frontend-compile
RUN cd ${INVENTREE_HOME} && invoke int.frontend-compile
COPY contrib/container/execute.sh ${INVENTREE_HOME}/execute.sh
RUN chmod +x ${INVENTREE_HOME}/execute.sh

View File

@ -17,6 +17,7 @@ gunicorn>=22.0.0
# LDAP required packages
django-auth-ldap # Django integration for ldap auth
python-ldap # LDAP auth support
django<5.0 # Force lower to match main project
# Upgraded python package installer
uv

View File

@ -4,17 +4,19 @@ asgiref==3.8.1 \
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
# via django
django==4.2.15 \
--hash=sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30 \
--hash=sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a
# via django-auth-ldap
django==4.2.16 \
--hash=sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898 \
--hash=sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad
# via
# -r contrib/container/requirements.in
# django-auth-ldap
django-auth-ldap==4.8.0 \
--hash=sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce \
--hash=sha256:604250938ddc9fda619f247c7a59b0b2f06e53a7d3f46a156f28aa30dd71a738
# via -r contrib/container/requirements.in
gunicorn==22.0.0 \
--hash=sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9 \
--hash=sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63
gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via -r contrib/container/requirements.in
invoke==2.2.0 \
--hash=sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820 \
@ -33,200 +35,201 @@ mariadb==1.1.10 \
--hash=sha256:d7b09ec4abd02ed235257feb769f90cd4066e8f536b55b92f5166103d5b66a63 \
--hash=sha256:dff8b28ce4044574870d7bdd2d9f9f5da8e5f95a7ff6d226185db733060d1a93
# via -r contrib/container/requirements.in
mysqlclient==2.2.4 \
--hash=sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711 \
--hash=sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41 \
--hash=sha256:3c318755e06df599338dad7625f884b8a71fcf322a9939ef78c9b3db93e1de7a \
--hash=sha256:4e80dcad884dd6e14949ac6daf769123223a52a6805345608bf49cdaf7bc8b3a \
--hash=sha256:9d3310295cb682232cadc28abd172f406c718b9ada41d2371259098ae37779d3 \
--hash=sha256:9d4c015480c4a6b2b1602eccd9846103fc70606244788d04aa14b31c4bd1f0e2 \
--hash=sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5 \
--hash=sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54 \
--hash=sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab
mysqlclient==2.2.5 \
--hash=sha256:1d2e2ca0fe8405d8d6464edd01bf059951279e4bc27284d39341bd4737b2bc64 \
--hash=sha256:3f9625bea2b9bcde0ace76b32708762d44597491092c819fd1bff5b4e27f709b \
--hash=sha256:8012c633aab8c91ea8172ac479807135b171501b9cad1a7cd9b58c4dc8dcdab5 \
--hash=sha256:add8643c32f738014d252d2bdebb478623b04802e8396d5903905db36474d3ff \
--hash=sha256:aee14f1872114865679fcb09aac3772de4595fa7dcf2f83a4c7afee15e508854 \
--hash=sha256:b54511648c1455b43ac28f8b4c1f732c5b0c343e87f7a3bd6fc9f9fe0f91934e \
--hash=sha256:b78438314199504c64f69e1e3521f2c9b419f19fcd85158b44c997b64409a6af \
--hash=sha256:e871ede4261d0d42b8ed20a2459db411c7deafedd8e77b7e4ba760be4a6a752b
# via -r contrib/container/requirements.in
packaging==24.0 \
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
# via
# gunicorn
# mariadb
psycopg[binary, pool]==3.1.18 \
--hash=sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b \
--hash=sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e
psycopg[binary, pool]==3.2.3 \
--hash=sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907 \
--hash=sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2
# via -r contrib/container/requirements.in
psycopg-binary==3.1.18 \
--hash=sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7 \
--hash=sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c \
--hash=sha256:13bcd3742112446037d15e360b27a03af4b5afcf767f5ee374ef8f5dd7571b31 \
--hash=sha256:1729d0e3dfe2546d823841eb7a3d003144189d6f5e138ee63e5227f8b75276a5 \
--hash=sha256:1859aeb2133f5ecdd9cbcee155f5e38699afc06a365f903b1512c765fd8d457e \
--hash=sha256:1c9b6bd7fb5c6638cb32469674707649b526acfe786ba6d5a78ca4293d87bae4 \
--hash=sha256:247474af262bdd5559ee6e669926c4f23e9cf53dae2d34c4d991723c72196404 \
--hash=sha256:258d2f0cb45e4574f8b2fe7c6d0a0e2eb58903a4fd1fbaf60954fba82d595ab7 \
--hash=sha256:2e2484ae835dedc80cdc7f1b1a939377dc967fed862262cfd097aa9f50cade46 \
--hash=sha256:320047e3d3554b857e16c2b6b615a85e0db6a02426f4d203a4594a2f125dfe57 \
--hash=sha256:39242546383f6b97032de7af30edb483d237a0616f6050512eee7b218a2aa8ee \
--hash=sha256:3c2b039ae0c45eee4cd85300ef802c0f97d0afc78350946a5d0ec77dd2d7e834 \
--hash=sha256:3c7afcd6f1d55992f26d9ff7b0bd4ee6b475eb43aa3f054d67d32e09f18b0065 \
--hash=sha256:3e4b0bb91da6f2238dbd4fbb4afc40dfb4f045bb611b92fce4d381b26413c686 \
--hash=sha256:3e7ce4d988112ca6c75765c7f24c83bdc476a6a5ce00878df6c140ca32c3e16d \
--hash=sha256:4085f56a8d4fc8b455e8f44380705c7795be5317419aa5f8214f315e4205d804 \
--hash=sha256:4575da95fc441244a0e2ebaf33a2b2f74164603341d2046b5cde0a9aa86aa7e2 \
--hash=sha256:489aa4fe5a0b653b68341e9e44af247dedbbc655326854aa34c163ef1bcb3143 \
--hash=sha256:4e4de16a637ec190cbee82e0c2dc4860fed17a23a35f7a1e6dc479a5c6876722 \
--hash=sha256:531381f6647fc267383dca88dbe8a70d0feff433a8e3d0c4939201fea7ae1b82 \
--hash=sha256:55ff0948457bfa8c0d35c46e3a75193906d1c275538877ba65907fd67aa059ad \
--hash=sha256:59701118c7d8842e451f1e562d08e8708b3f5d14974eefbce9374badd723c4ae \
--hash=sha256:5c323103dfa663b88204cf5f028e83c77d7a715f9b6f51d2bbc8184b99ddd90a \
--hash=sha256:5d6e860edf877d4413e4a807e837d55e3a7c7df701e9d6943c06e460fa6c058f \
--hash=sha256:639dd78ac09b144b0119076783cb64e1128cc8612243e9701d1503c816750b2e \
--hash=sha256:6432047b8b24ef97e3fbee1d1593a0faaa9544c7a41a2c67d1f10e7621374c83 \
--hash=sha256:67284e2e450dc7a9e4d76e78c0bd357dc946334a3d410defaeb2635607f632cd \
--hash=sha256:6ebecbf2406cd6875bdd2453e31067d1bd8efe96705a9489ef37e93b50dc6f09 \
--hash=sha256:7121acc783c4e86d2d320a7fb803460fab158a7f0a04c5e8c5d49065118c1e73 \
--hash=sha256:74e498586b72fb819ca8ea82107747d0cb6e00ae685ea6d1ab3f929318a8ce2d \
--hash=sha256:780a90bcb69bf27a8b08bc35b958e974cb6ea7a04cdec69e737f66378a344d68 \
--hash=sha256:7ac1785d67241d5074f8086705fa68e046becea27964267ab3abd392481d7773 \
--hash=sha256:812726266ab96de681f2c7dbd6b734d327f493a78357fcc16b2ac86ff4f4e080 \
--hash=sha256:824a1bfd0db96cc6bef2d1e52d9e0963f5bf653dd5bc3ab519a38f5e6f21c299 \
--hash=sha256:87dd9154b757a5fbf6d590f6f6ea75f4ad7b764a813ae04b1d91a70713f414a1 \
--hash=sha256:887f8d856c91510148be942c7acd702ccf761a05f59f8abc123c22ab77b5a16c \
--hash=sha256:888a72c2aca4316ca6d4a619291b805677bae99bba2f6e31a3c18424a48c7e4d \
--hash=sha256:8f54978c4b646dec77fefd8485fa82ec1a87807f334004372af1aaa6de9539a5 \
--hash=sha256:91074f78a9f890af5f2c786691575b6b93a4967ad6b8c5a90101f7b8c1a91d9c \
--hash=sha256:9d684227ef8212e27da5f2aff9d4d303cc30b27ac1702d4f6881935549486dd5 \
--hash=sha256:9e24e7b6a68a51cc3b162d0339ae4e1263b253e887987d5c759652f5692b5efe \
--hash=sha256:9ffcbbd389e486d3fd83d30107bbf8b27845a295051ccabde240f235d04ed921 \
--hash=sha256:a87e9eeb80ce8ec8c2783f29bce9a50bbcd2e2342a340f159c3326bf4697afa1 \
--hash=sha256:ad35ac7fd989184bf4d38a87decfb5a262b419e8ba8dcaeec97848817412c64a \
--hash=sha256:b15e3653c82384b043d820fc637199b5c6a36b37fa4a4943e0652785bb2bad5d \
--hash=sha256:b293e01057e63c3ac0002aa132a1071ce0fdb13b9ee2b6b45d3abdb3525c597d \
--hash=sha256:b2f7f95746efd1be2dc240248cc157f4315db3fd09fef2adfcc2a76e24aa5741 \
--hash=sha256:bd27f713f2e5ef3fd6796e66c1a5203a27a30ecb847be27a78e1df8a9a5ae68c \
--hash=sha256:c38a4796abf7380f83b1653c2711cb2449dd0b2e5aca1caa75447d6fa5179c69 \
--hash=sha256:c76659ae29a84f2c14f56aad305dd00eb685bd88f8c0a3281a9a4bc6bd7d2aa7 \
--hash=sha256:c84a0174109f329eeda169004c7b7ca2e884a6305acab4a39600be67f915ed38 \
--hash=sha256:cd2a9f7f0d4dacc5b9ce7f0e767ae6cc64153264151f50698898c42cabffec0c \
--hash=sha256:d322ba72cde4ca2eefc2196dad9ad7e52451acd2f04e3688d590290625d0c970 \
--hash=sha256:d4422af5232699f14b7266a754da49dc9bcd45eba244cf3812307934cd5d6679 \
--hash=sha256:d46ae44d66bf6058a812467f6ae84e4e157dee281bfb1cfaeca07dee07452e85 \
--hash=sha256:da917f6df8c6b2002043193cb0d74cc173b3af7eb5800ad69c4e1fbac2a71c30 \
--hash=sha256:dea4a59da7850192fdead9da888e6b96166e90608cf39e17b503f45826b16f84 \
--hash=sha256:e05f6825f8db4428782135e6986fec79b139210398f3710ed4aa6ef41473c008 \
--hash=sha256:e1cf59e0bb12e031a48bb628aae32df3d0c98fd6c759cb89f464b1047f0ca9c8 \
--hash=sha256:e252d66276c992319ed6cd69a3ffa17538943954075051e992143ccbf6dc3d3e \
--hash=sha256:e262398e5d51563093edf30612cd1e20fedd932ad0994697d7781ca4880cdc3d \
--hash=sha256:e28ff8f3de7b56588c2a398dc135fd9f157d12c612bd3daa7e6ba9872337f6f5 \
--hash=sha256:eea5f14933177ffe5c40b200f04f814258cc14b14a71024ad109f308e8bad414 \
--hash=sha256:f876ebbf92db70125f6375f91ab4bc6b27648aa68f90d661b1fc5affb4c9731c \
--hash=sha256:f8ff3bc08b43f36fdc24fedb86d42749298a458c4724fb588c4d76823ac39f54
psycopg-binary==3.2.3 \
--hash=sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920 \
--hash=sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63 \
--hash=sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585 \
--hash=sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26 \
--hash=sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b \
--hash=sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e \
--hash=sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78 \
--hash=sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd \
--hash=sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b \
--hash=sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3 \
--hash=sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1 \
--hash=sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d \
--hash=sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f \
--hash=sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949 \
--hash=sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78 \
--hash=sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002 \
--hash=sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b \
--hash=sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6 \
--hash=sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502 \
--hash=sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c \
--hash=sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f \
--hash=sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40 \
--hash=sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f \
--hash=sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7 \
--hash=sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9 \
--hash=sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe \
--hash=sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818 \
--hash=sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0 \
--hash=sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056 \
--hash=sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505 \
--hash=sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6 \
--hash=sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308 \
--hash=sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c \
--hash=sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae \
--hash=sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2 \
--hash=sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350 \
--hash=sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c \
--hash=sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1 \
--hash=sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410 \
--hash=sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96 \
--hash=sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6 \
--hash=sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4 \
--hash=sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045 \
--hash=sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea \
--hash=sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942 \
--hash=sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c \
--hash=sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d \
--hash=sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d \
--hash=sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e \
--hash=sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b \
--hash=sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e \
--hash=sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339 \
--hash=sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955 \
--hash=sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749 \
--hash=sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e \
--hash=sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0 \
--hash=sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170 \
--hash=sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426 \
--hash=sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774 \
--hash=sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf \
--hash=sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6 \
--hash=sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f \
--hash=sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393 \
--hash=sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea
# via psycopg
psycopg-pool==3.2.1 \
--hash=sha256:060b551d1b97a8d358c668be58b637780b884de14d861f4f5ecc48b7563aafb7 \
--hash=sha256:6509a75c073590952915eddbba7ce8b8332a440a31e77bba69561483492829ad
psycopg-pool==3.2.3 \
--hash=sha256:53bd8e640625e01b2927b2ad96df8ed8e8f91caea4597d45e7673fc7bbb85eb1 \
--hash=sha256:bb942f123bef4b7fbe4d55421bd3fb01829903c95c0f33fd42b7e94e5ac9b52a
# via psycopg
pyasn1==0.6.0 \
--hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \
--hash=sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473
pyasn1==0.6.1 \
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
--hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
# via
# pyasn1-modules
# python-ldap
pyasn1-modules==0.4.0 \
--hash=sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6 \
--hash=sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b
pyasn1-modules==0.4.1 \
--hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \
--hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c
# via python-ldap
python-ldap==3.4.4 \
--hash=sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828
# via
# -r contrib/container/requirements.in
# django-auth-ldap
pyyaml==6.0.1 \
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
--hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
--hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
--hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
--hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
--hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
--hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
--hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
--hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
--hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
--hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
--hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
--hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
--hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
--hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
--hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
--hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
--hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
--hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
--hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
--hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
--hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
--hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
--hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
--hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
--hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
--hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
--hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
--hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
--hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
--hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
--hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
--hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
--hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
--hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
--hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
--hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
--hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
--hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
--hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
--hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via -r contrib/container/requirements.in
setuptools==70.3.0 \
--hash=sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5 \
--hash=sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc
setuptools==75.2.0 \
--hash=sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec \
--hash=sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8
# via -r contrib/container/requirements.in
sqlparse==0.5.0 \
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
sqlparse==0.5.1 \
--hash=sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4 \
--hash=sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e
# via django
typing-extensions==4.11.0 \
--hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
--hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# psycopg
# psycopg-pool
uv==0.1.38 \
--hash=sha256:03242a734a572733f2b9a5dbb94517e918fe26fc01114b7c51d12296dfbb8f8b \
--hash=sha256:067af2d986329db4fa3c7373017d49f0e16ddff23e483b7e5bc3a5a18ce08ea6 \
--hash=sha256:0937ad16ae0e0b6bb6dd3c386f8fb33141ad08d1762eaacffb4d2b27fb466a17 \
--hash=sha256:0e1d64ac437b0a14fbcec55b1c3f162fa24860711e0d855fcd9c672b149a122a \
--hash=sha256:1be7aa46936c0351ccb1400ea95e5381b3f05fef772fa3b9f23af728cc175dea \
--hash=sha256:309e73a3ec3a5a536a3efaf434270fc94b483069f1425765165c1c9d786c27fd \
--hash=sha256:4251f9771d392d7badc1e5fb934b397b12ca00fef9d955207ade169cc1f7e872 \
--hash=sha256:43772e7589f70e954b1ae29230e575ef9e4d8d769138a94dfa5ae7eaf1e26ac5 \
--hash=sha256:4a6024256d38b77151e32876be9fcb99cf75df7a86b26e0161cc202bed558adf \
--hash=sha256:5a98d6aacd4b57b7e00daf154919e7c9206fefdf40bd28cfb13efe0e0324d491 \
--hash=sha256:8de6dbd8f348ee90af044f4cc7b6650521d25ba2d20a813c1e157a3f90069dd9 \
--hash=sha256:9133e24db9bdd4f412eab69586d03294419825432a9a27ee1b510a4c01eb7b0b \
--hash=sha256:92f65b6e4e5c8126501785af3629dc537d7c82caa56ac9336a86929c73d0e138 \
--hash=sha256:afd85029923e712b6b2c45ddc1680c785392220876c766521e45778db3f71f8e \
--hash=sha256:b0b15e51a0f8240969bc412ed0dd60cfe3f664b30173139ef263d71c596d631f \
--hash=sha256:ea44c07605d1359a7d82bf42706dd86d341f15f4ca2e1f36e51626a7111c2ad5 \
--hash=sha256:f87c9711493c53d32012a96b49c4d53aabdf7ed666cbf2c3fb55dd402a6b31a8
uv==0.4.25 \
--hash=sha256:18100f0f36419a154306ed6211e3490bf18384cdf3f1a0950848bf64b62fa251 \
--hash=sha256:2d29a78f011ecc2f31c13605acb6574c2894c06d258b0f8d0dbb899986800450 \
--hash=sha256:2fc35b5273f1e018aecd66b70e0fd7d2eb6698853dde3e2fc644e7ebf9f825b1 \
--hash=sha256:3d7680795ea78cdbabbcce73d039b2651cf1fa635ddc1aa3082660f6d6255c50 \
--hash=sha256:4c55040e67470f2b73e95e432aba06f103a0b348ea0b9c6689b1029c8d9e89fd \
--hash=sha256:50c7d0d9e7f392f81b13bf3b7e37768d1486f2fc9d533a54982aa0ed11e4db23 \
--hash=sha256:578ae385fad6bd6f3868828e33d54994c716b315b1bc49106ec1f54c640837e4 \
--hash=sha256:6e981b1465e30102e41946adede9cb08051a5d70c6daf09f91a7ea84f0b75c08 \
--hash=sha256:7d266e02fefef930609328c31c075084295c3cb472bab3f69549fad4fd9d82b3 \
--hash=sha256:94fb2b454afa6bdfeeea4b4581c878944ca9cf3a13712e6762f245f5fbaaf952 \
--hash=sha256:a7022a71ff63a3838796f40e954b76bf7820fc27e96fe002c537e75ff8e34f1d \
--hash=sha256:a7c3a18c20ddb527d296d1222bddf42b78031c50b5b4609d426569b5fb61f5b0 \
--hash=sha256:aae9dcafd20d5ba978c8a4939ab942e8e2e155c109e9945207fbbd81d2892c9e \
--hash=sha256:bdbfd0c476b9e80a3f89af96aed6dd7d2782646311317a9c72614ccce99bb2ad \
--hash=sha256:be2a4fc4fcade9ea5e67e51738c95644360d6e59b6394b74fc579fb617f902f7 \
--hash=sha256:d39077cdfe3246885fcdf32e7066ae731a166101d063629f9cea08738f79e6a3 \
--hash=sha256:e02afb0f6d4b58718347f7d7cfa5a801e985ce42181ba971ed85ef149f6658ca \
--hash=sha256:ec181be2bda10651a3558156409ac481549983e0276d0e3645e3b1464e7f8715
# via -r contrib/container/requirements.in
wheel==0.43.0 \
--hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \
--hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
# via -r contrib/container/requirements.in

View File

@ -104,9 +104,9 @@ jc==1.25.3 \
--hash=sha256:ea17a8578497f2da92f73924d9d403f4563ba59422fbceff7bb4a16cdf84a54f \
--hash=sha256:fa3140ceda6cba1210d1362f363cd79a0514741e8a1dd6167db2b2e2d5f24f7b
# via -r contrib/dev_reqs/requirements.in
pygments==2.17.2 \
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
pygments==2.18.0 \
--hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \
--hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a
# via jc
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \

View File

@ -13,6 +13,7 @@ function detect_docker() {
else
DOCKER="no"
fi
echo "# POI04| Running in docker: ${DOCKER}"
}
function detect_initcmd() {
@ -30,6 +31,7 @@ function detect_initcmd() {
if [ "${DOCKER}" == "yes" ]; then
INIT_CMD="initctl"
fi
echo "# POI05| Using init command: ${INIT_CMD}"
}
function detect_ip() {
@ -37,36 +39,36 @@ function detect_ip() {
if [ "${SETUP_NO_CALLS}" == "true" ]; then
# Use local IP address
echo "# Getting the IP address of the first local IP address"
echo "# POI06| Getting the IP address of the first local IP address"
export INVENTREE_IP=$(hostname -I | awk '{print $1}')
else
# Use web service to get the IP address
echo "# Getting the IP address of the server via web service"
echo "# POI06| Getting the IP address of the server via web service"
export INVENTREE_IP=$(curl -s https://checkip.amazonaws.com)
fi
echo "IP address is ${INVENTREE_IP}"
echo "# POI06| IP address is ${INVENTREE_IP}"
}
function detect_python() {
# Detect if there is already a python version installed in /opt/inventree/env/lib
if test -f "${APP_HOME}/env/bin/python"; then
echo "# Python environment already present"
echo "# POI07| Python environment already present"
# Extract earliest python version initialised from /opt/inventree/env/lib
SETUP_PYTHON=$(ls -1 ${APP_HOME}/env/bin/python* | sort | head -n 1)
echo "# Found earlier used version: ${SETUP_PYTHON}"
echo "# POI07| Found earlier used version: ${SETUP_PYTHON}"
else
echo "# No python environment found - using environment variable: ${SETUP_PYTHON}"
echo "# POI07| No python environment found - using environment variable: ${SETUP_PYTHON}"
fi
# Try to detect a python between 3.9 and 3.12 in reverse order
if [ -z "$(which ${SETUP_PYTHON})" ]; then
echo "# Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
echo "# POI07| Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
for i in $(seq $PYTHON_TO -1 $PYTHON_FROM); do
echo "# Checking for python3.${i}"
echo "# POI07| Checking for python3.${i}"
if [ -n "$(which python3.${i})" ]; then
SETUP_PYTHON="python3.${i}"
echo "# Found python3.${i} installed - using for setup ${SETUP_PYTHON}"
echo "# POI07| Found python3.${i} installed - using for setup ${SETUP_PYTHON}"
break
fi
done
@ -75,12 +77,14 @@ function detect_python() {
# Ensure python can be executed - abort if not
if [ -z "$(which ${SETUP_PYTHON})" ]; then
echo "${On_Red}"
echo "# Python ${SETUP_PYTHON} not found - aborting!"
echo "# Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
echo "# If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.10'."
echo "# POI07| Python ${SETUP_PYTHON} not found - aborting!"
echo "# POI07| Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.10'."
echo "${Color_Off}"
exit 1
fi
echo "# POI07| Using python command: ${SETUP_PYTHON}"
}
function get_env() {
@ -95,7 +99,7 @@ function get_env() {
done
if [ -n "${SETUP_DEBUG}" ]; then
echo "Done getting env $envname: ${!envname}"
echo "# POI02| Done getting env $envname: ${!envname}"
fi
}
@ -103,7 +107,7 @@ function detect_local_env() {
# Get all possible envs for the install
if [ -n "${SETUP_DEBUG}" ]; then
echo "# Printing local envs - before #++#"
echo "# POI02| Printing local envs - before #++#"
printenv
fi
@ -113,7 +117,7 @@ function detect_local_env() {
done
if [ -n "${SETUP_DEBUG}" ]; then
echo "# Printing local envs - after #++#"
echo "# POI02| Printing local envs - after #++#"
printenv
fi
}
@ -121,15 +125,17 @@ function detect_local_env() {
function detect_envs() {
# Detect all envs that should be passed to setup commands
echo "# Setting base environment variables"
echo "# POI03| Setting base environment variables"
export INVENTREE_CONFIG_FILE=${INVENTREE_CONFIG_FILE:-${CONF_DIR}/config.yaml}
if test -f "${INVENTREE_CONFIG_FILE}"; then
echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}"
echo "# POI03| Using existing config file: ${INVENTREE_CONFIG_FILE}"
# Install parser
echo "# POI03| Installing requirements"
pip install --require-hashes -r ${APP_HOME}/contrib/dev_reqs/requirements.txt -q
echo "# POI03| Installed requirements"
# Load config
export INVENTREE_CONF_DATA=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)
@ -149,10 +155,10 @@ function detect_envs() {
export INVENTREE_DB_HOST=$(jq -r '.[].database.HOST' <<< ${INVENTREE_CONF_DATA})
export INVENTREE_DB_PORT=$(jq -r '.[].database.PORT' <<< ${INVENTREE_CONF_DATA})
else
echo "# No config file found: ${INVENTREE_CONFIG_FILE}, using envs or defaults"
echo "# POI03| No config file found: ${INVENTREE_CONFIG_FILE}, using envs or defaults"
if [ -n "${SETUP_DEBUG}" ]; then
echo "# Print current envs"
echo "# POI03| Print current envs"
printenv | grep INVENTREE_
printenv | grep SETUP_
fi
@ -175,43 +181,43 @@ function detect_envs() {
fi
# For debugging pass out the envs
echo "# Collected environment variables:"
echo "# INVENTREE_MEDIA_ROOT=${INVENTREE_MEDIA_ROOT}"
echo "# INVENTREE_STATIC_ROOT=${INVENTREE_STATIC_ROOT}"
echo "# INVENTREE_BACKUP_DIR=${INVENTREE_BACKUP_DIR}"
echo "# INVENTREE_PLUGINS_ENABLED=${INVENTREE_PLUGINS_ENABLED}"
echo "# INVENTREE_PLUGIN_FILE=${INVENTREE_PLUGIN_FILE}"
echo "# INVENTREE_SECRET_KEY_FILE=${INVENTREE_SECRET_KEY_FILE}"
echo "# INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE}"
echo "# INVENTREE_DB_NAME=${INVENTREE_DB_NAME}"
echo "# INVENTREE_DB_USER=${INVENTREE_DB_USER}"
echo "# POI03| Collected environment variables:"
echo "# POI03| INVENTREE_MEDIA_ROOT=${INVENTREE_MEDIA_ROOT}"
echo "# POI03| INVENTREE_STATIC_ROOT=${INVENTREE_STATIC_ROOT}"
echo "# POI03| INVENTREE_BACKUP_DIR=${INVENTREE_BACKUP_DIR}"
echo "# POI03| INVENTREE_PLUGINS_ENABLED=${INVENTREE_PLUGINS_ENABLED}"
echo "# POI03| INVENTREE_PLUGIN_FILE=${INVENTREE_PLUGIN_FILE}"
echo "# POI03| INVENTREE_SECRET_KEY_FILE=${INVENTREE_SECRET_KEY_FILE}"
echo "# POI03| INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE}"
echo "# POI03| INVENTREE_DB_NAME=${INVENTREE_DB_NAME}"
echo "# POI03| INVENTREE_DB_USER=${INVENTREE_DB_USER}"
if [ -n "${SETUP_DEBUG}" ]; then
echo "# INVENTREE_DB_PASSWORD=${INVENTREE_DB_PASSWORD}"
echo "# POI03| INVENTREE_DB_PASSWORD=${INVENTREE_DB_PASSWORD}"
fi
echo "# INVENTREE_DB_HOST=${INVENTREE_DB_HOST}"
echo "# INVENTREE_DB_PORT=${INVENTREE_DB_PORT}"
echo "# POI03| INVENTREE_DB_HOST=${INVENTREE_DB_HOST}"
echo "# POI03| INVENTREE_DB_PORT=${INVENTREE_DB_PORT}"
}
function create_initscripts() {
# Make sure python env exists
if test -f "${APP_HOME}/env"; then
echo "# python environment already present - skipping"
echo "# POI09| python environment already present - skipping"
else
echo "# Setting up python environment"
echo "# POI09| Setting up python environment"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && ${SETUP_PYTHON} -m venv env"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && env/bin/pip install invoke wheel"
# Check INSTALLER_EXTRA exists and load it
if test -f "${APP_HOME}/INSTALLER_EXTRA"; then
echo "# Loading extra packages from INSTALLER_EXTRA"
echo "# POI09| Loading extra packages from INSTALLER_EXTRA"
source ${APP_HOME}/INSTALLER_EXTRA
fi
if [ -n "${SETUP_EXTRA_PIP}" ]; then
echo "# Installing extra pip packages"
echo "# POI09| Installing extra pip packages"
if [ -n "${SETUP_DEBUG}" ]; then
echo "# Extra pip packages: ${SETUP_EXTRA_PIP}"
echo "# POI09| Extra pip packages: ${SETUP_EXTRA_PIP}"
fi
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && env/bin/pip install ${SETUP_EXTRA_PIP}"
# Write extra packages to INSTALLER_EXTRA
@ -221,41 +227,45 @@ function create_initscripts() {
# Unlink default config if it exists
if test -f "/etc/nginx/sites-enabled/default"; then
echo "# Unlinking default nginx config\n# Old file still in /etc/nginx/sites-available/default"
echo "# POI09| Unlinking default nginx config\n# POI09| Old file still in /etc/nginx/sites-available/default"
sudo unlink /etc/nginx/sites-enabled/default
echo "# POI09| Unlinked default nginx config"
fi
# Create InvenTree specific nginx config
echo "# Stopping nginx"
echo "# POI09| Stopping nginx"
${INIT_CMD} stop nginx
echo "# Setting up nginx to ${SETUP_NGINX_FILE}"
echo "# POI09| Stopped nginx"
echo "# POI09| Setting up nginx to ${SETUP_NGINX_FILE}"
# Always use the latest nginx config; important if new headers are added / needed for security
cp ${APP_HOME}/contrib/packager.io/nginx.prod.conf ${SETUP_NGINX_FILE}
sed -i s/inventree-server:8000/localhost:6000/g ${SETUP_NGINX_FILE}
sed -i s=var/www=opt/inventree/data=g ${SETUP_NGINX_FILE}
# Start nginx
echo "# Starting nginx"
echo "# POI09| Starting nginx"
${INIT_CMD} start nginx
echo "# POI09| Started nginx"
echo "# (Re)creating init scripts"
echo "# POI09| (Re)creating init scripts"
# This resets scale parameters to a known state
inventree scale web="1" worker="1"
echo "# Enabling InvenTree on boot"
echo "# POI09| Enabling InvenTree on boot"
${INIT_CMD} enable inventree
echo "# POI09| Enabled InvenTree on boot"
}
function create_admin() {
# Create data for admin users - stop with setting SETUP_ADMIN_NOCREATION to true
if [ "${SETUP_ADMIN_NOCREATION}" == "true" ]; then
echo "# Admin creation is disabled - skipping"
echo "# POI10| Admin creation is disabled - skipping"
return
fi
if test -f "${SETUP_ADMIN_PASSWORD_FILE}"; then
echo "# Admin data already exists - skipping"
echo "# POI10| Admin data already exists - skipping"
else
echo "# Creating admin user data"
echo "# POI10| Creating admin user data"
# Static admin data
export INVENTREE_ADMIN_USER=${INVENTREE_ADMIN_USER:-admin}
@ -270,13 +280,15 @@ function create_admin() {
}
function start_inventree() {
echo "# Starting InvenTree"
echo "# POI15| Starting InvenTree"
${INIT_CMD} start inventree
echo "# POI15| Started InvenTree"
}
function stop_inventree() {
echo "# Stopping InvenTree"
echo "# POI11| Stopping InvenTree"
${INIT_CMD} stop inventree
echo "# POI11| Stopped InvenTree"
}
function update_or_install() {
@ -285,23 +297,23 @@ function update_or_install() {
chown ${APP_USER}:${APP_GROUP} ${APP_HOME} -R
# Run update as app user
echo "# Updating InvenTree"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && pip install wheel"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke update | sed -e 's/^/# inv update| /;'"
echo "# POI12| Updating InvenTree"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && pip install uv wheel"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke update --uv | sed -e 's/^/# POI12| u | /;'"
# Make sure permissions are correct again
echo "# Set permissions for data dir and media: ${DATA_DIR}"
echo "# POI12| Set permissions for data dir and media: ${DATA_DIR}"
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} -R
chown ${APP_USER}:${APP_GROUP} ${CONF_DIR} -R
}
function set_env() {
echo "# Setting up InvenTree config values"
echo "# POI13| Setting up InvenTree config values"
inventree config:set INVENTREE_CONFIG_FILE=${INVENTREE_CONFIG_FILE}
# Changing the config file
echo "# Writing the settings to the config file ${INVENTREE_CONFIG_FILE}"
echo "# POI13| Writing the settings to the config file ${INVENTREE_CONFIG_FILE}"
# Media Root
sed -i s=#media_root:\ \'/home/inventree/data/media\'=media_root:\ \'${INVENTREE_MEDIA_ROOT}\'=g ${INVENTREE_CONFIG_FILE}
# Static Root
@ -332,23 +344,28 @@ function set_env() {
# Fixing the permissions
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE}
echo "# POI13| Done setting up InvenTree config values"
}
function set_site() {
# Ensure IP is known
if [ -z "${INVENTREE_IP}" ]; then
echo "# No IP address found - skipping"
echo "# POI14| No IP address found - skipping"
return
fi
# Check if INVENTREE_SITE_URL in inventree config
if [ -z "$(inventree config:get INVENTREE_SITE_URL)" ]; then
echo "# Setting up InvenTree site URL"
echo "# POI14| Setting up InvenTree site URL"
inventree config:set INVENTREE_SITE_URL=http://${INVENTREE_IP}
else
echo "# POI14| Site URL already set - skipping"
fi
}
function final_message() {
echo "# POI16| Printing Final message"
echo -e "####################################################################################"
echo -e "This InvenTree install uses nginx, the settings for the webserver can be found in"
echo -e "${SETUP_NGINX_FILE}"
@ -362,10 +379,12 @@ function final_message() {
function update_checks() {
echo "# Running upgrade"
echo "# POI08| Running upgrade"
local old_version=$1
local old_version_rev=$(echo ${old_version} | cut -d'-' -f1 | cut -d'.' -f2)
echo "# Old version is: ${old_version} - release: ${old_version_rev}"
local new_version=$(dpkg-query --show --showformat='${Version}' inventree)
local new_version_rev=$(echo ${new_version} | cut -d'-' -f1 | cut -d'.' -f2)
echo "# POI08| Old version is: ${old_version} | ${old_version_rev} - updating to ${new_version} | ${old_version_rev}"
local ABORT=false
function check_config_value() {
@ -378,25 +397,25 @@ function update_checks() {
value=$(jq -r ".[].${config_key}" <<< ${INVENTREE_CONF_DATA})
fi
if [ -z "${value}" ] || [ "$value" == "null" ]; then
echo "# No setting for ${name} found - please set it manually either in ${INVENTREE_CONFIG_FILE} under '${config_key}' or with 'inventree config:set ${env_key}=value'"
echo "# POI08| No setting for ${name} found - please set it manually either in ${INVENTREE_CONFIG_FILE} under '${config_key}' or with 'inventree config:set ${env_key}=value'"
ABORT=true
else
echo "# Found setting for ${name} - ${value}"
echo "# POI08| Found setting for ${name} - ${value}"
fi
}
# Custom checks if old version is below 0.8.0
if [ "${old_version_rev}" -lt "9" ]; then
echo "# Old version is below 0.9.0 - You might be missing some configs"
echo "# POI08| Old version is below 0.9.0 - You might be missing some configs"
# Check for BACKUP_DIR and SITE_URL in INVENTREE_CONF_DATA and config
check_config_value "INVENTREE_SITE_URL" "site_url" "site URL"
check_config_value "INVENTREE_BACKUP_DIR" "backup_dir" "backup dir"
if [ "${ABORT}" = true ]; then
echo "# Aborting - please set the missing values and run the update again"
echo "# POI08| Aborting - please set the missing values and run the update again"
exit 1
fi
echo "# All checks passed - continuing with the update"
echo "# POI08| All checks passed - continuing with the update"
fi
}

View File

@ -3,15 +3,18 @@
# packager.io postinstall script
#
echo "# POI01| Running postinstall script - start - $(date)"
exec > >(tee ${APP_HOME}/log/setup_$(date +"%F_%H_%M_%S").log) 2>&1
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
# import functions
echo "# POI01| Importing functions"
. ${APP_HOME}/contrib/packager.io/functions.sh
echo "# POI01| Functions imported"
# Envs that should be passed to setup commands
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON,SETUP_ADMIN_NOCREATION
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_SITE_URL,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON,SETUP_ADMIN_NOCREATION
# Get the envs
detect_local_env
@ -37,9 +40,9 @@ detect_ip
detect_python
# Check if we are updating and need to alert
echo "# Checking if update checks are needed"
echo "# POI08| Checking if update checks are needed"
if [ -z "$2" ]; then
echo "# Normal install - no need for checks"
echo "# POI08| Normal install - no need for checks"
else
update_checks $2
fi
@ -60,3 +63,4 @@ start_inventree
# show info
final_message
echo "# POI17| Running postinstall script - done - $(date)"

View File

@ -2,14 +2,22 @@
#
# packager.io preinstall/preremove script
#
echo "# PRI01| Running preinstall script - start - $(date)"
PATH=${APP_HOME}/env/bin:${APP_HOME}/:/sbin:/bin:/usr/sbin:/usr/bin:
# Envs that should be passed to setup commands
export SETUP_ENVS=PATH,APP_HOME,INVENTREE_MEDIA_ROOT,INVENTREE_STATIC_ROOT,INVENTREE_BACKUP_DIR,INVENTREE_PLUGINS_ENABLED,INVENTREE_PLUGIN_FILE,INVENTREE_CONFIG_FILE,INVENTREE_SECRET_KEY_FILE,INVENTREE_DB_ENGINE,INVENTREE_DB_NAME,INVENTREE_DB_USER,INVENTREE_DB_PASSWORD,INVENTREE_DB_HOST,INVENTREE_DB_PORT,INVENTREE_ADMIN_USER,INVENTREE_ADMIN_EMAIL,INVENTREE_ADMIN_PASSWORD,SETUP_NGINX_FILE,SETUP_ADMIN_PASSWORD_FILE,SETUP_NO_CALLS,SETUP_DEBUG,SETUP_EXTRA_PIP,SETUP_PYTHON
if test -f "${APP_HOME}/env/bin/pip"; then
echo "# Clearing precompiled files"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke clear-generated"
# Check if clear-generated is available
if sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke int.clear-generated --help" > /dev/null 2>&1; then
echo "# PRI02| Clearing precompiled files"
sudo -u ${APP_USER} --preserve-env=$SETUP_ENVS bash -c "cd ${APP_HOME} && invoke int.clear-generated"
else
echo "# PRI02| Clearing precompiled files - skipping"
fi
else
echo "# No python environment found - skipping"
echo "# PRI02| No python environment found - skipping"
fi
echo "# PRI03| Running preinstall script - done - $(date)"

View File

@ -1,8 +1,14 @@
# Configuration file for Crowdin project integration
# See: https://crowdin.com/project/inventree
"commit_message": "Fix: New translations %original_file_name% from Crowdin"
"append_commit_message": false
"preserve_hierarchy": true
files:
- source: /src/backend/InvenTree/locale/en/LC_MESSAGES/django.po
dest: /%original_path%/%original_file_name%
translation: /src/backend/InvenTree/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%
- source: /src/frontend/src/locales/en/messages.po
dest: /%original_path%/%original_file_name%
translation: /src/frontend/src/locales/%two_letters_code%/%original_file_name%

View File

@ -10,7 +10,7 @@ tld = os.path.abspath(os.path.join(here, '..'))
config_file = os.path.join(tld, 'mkdocs.yml')
with open(config_file, 'r') as f:
with open(config_file, encoding='utf-8') as f:
data = yaml.load(f, yaml.BaseLoader)
assert data['strict'] == 'true'

View File

@ -22,10 +22,10 @@ The API is self-documenting, and the documentation is provided alongside any Inv
### Generating Schema File
If you want to generate the API schema file yourself (for example to use with an external client, use the `invoke schema` command. Run with the `-help` command to see available options.
If you want to generate the API schema file yourself (for example to use with an external client, use the `invoke dev.schema` command. Run with the `-help` command to see available options.
```
invoke schema -help
invoke dev.schema -help
```
## Authentication

View File

@ -56,3 +56,11 @@ If no match is found for the scanned barcode, the following error message is dis
## App Integration
Barcode scanning is a key feature of the [companion mobile app](../app/barcode.md).
## Barcode History
If enabled, InvenTree can retain logs of the most recent barcode scans. This can be very useful for debugging or auditing purpopes.
Refer to the [barcode settings](../settings/global.md#barcodes) to enable barcode history logging.
The barcode history can be viewed via the admin panel in the web interface.

View File

@ -16,6 +16,7 @@ The demo instance has a number of user accounts which you can use to explore the
| Username | Password | Staff Access | Enabled | Description |
| -------- | -------- | ------------ | ------- | ----------- |
| noaccess | youshallnotpass | No | Yes | Can login, but has no permissions |
| allaccess | nolimits | No | Yes | View / create / edit all pages and items |
| reader | readonly | No | Yes | Can view all pages but cannot create, edit or delete database records |
| engineer | partsonly | No | Yes | Can manage parts, view stock, but no access to purchase orders or sales orders |
@ -23,3 +24,35 @@ The demo instance has a number of user accounts which you can use to explore the
| ian | inactive | No | No | Inactive account, cannot log in |
| susan | inactive | No | No | Inactive account, cannot log in |
| admin | inventree | Yes | Yes | Superuser account, can access all parts of the system |
### Dataset
The demo instance is populated with a sample dataset, which is reset every 24 hours.
The source data used in the demo instance can be found on our [GitHub page](https://github.com/inventree/demo-dataset).
### Local Setup
If you wish to install the demo dataset locally (for initial testing), you can run the following command (via [invoke](./start/invoke.md)):
```bash
invoke dev.setup-test -i
```
*(Note: The command above may be slightly different if you are running in docker.)*
This will install the demo dataset into your local InvenTree instance.
!!! warning "Warning"
This command will **delete all existing data** in your InvenTree instance! It is not intended to be used on a production system, or loaded into an existing dataset.
### Clear Data
To clear demo data from your instance, and start afresh with a clean database, you can run the following command (via [invoke](./start/invoke.md)):
```bash
invoke dev.delete-data
```
!!! warning "Warning"
This command will **delete all existing data** in your InvenTree instance, including any data that you have added yourself.

View File

@ -23,7 +23,7 @@ To setup a development environment using [docker](../start/docker.md), run the f
```bash
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke install
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke setup-test --dev
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml run --rm inventree-dev-server invoke dev.setup-test --dev
docker compose --project-directory . -f contrib/container/dev-docker-compose.yml up -d
```
@ -34,25 +34,28 @@ A "bare metal" development setup can be installed as follows:
```bash
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
python3 -m venv env && source env/bin/activate
pip install invoke && invoke
pip install invoke && invoke setup-dev --tests
pip install django invoke && invoke
invoke dev.setup-dev --tests
```
Read the [InvenTree setup documentation](../start/intro.md) for a complete installation reference guide.
!!! note "Required Packages"
Depending on your system, you may need to install additional software packages as required.
### Setup Devtools
Run the following command to set up all toolsets for development.
```bash
invoke setup-dev
invoke dev.setup-dev
```
*We recommend you run this command before starting to contribute. This will install and set up `pre-commit` to run some checks before each commit and help reduce errors.*
## Branches and Versioning
InvenTree roughly follow the [GitLab flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
InvenTree roughly follow the [GitLab flow](https://about.gitlab.com/topics/version-control/what-are-gitlab-flow-best-practices/) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
There are nominally 5 active branches:
- `master` - The main development branch
@ -169,19 +172,20 @@ The various github actions can be found in the `./github/workflows` directory
### Run tests locally
To run test locally, use:
```
invoke test
invoke dev.test
```
To run only partial tests, for example for a module use:
```
invoke test --runtest order
invoke dev.test --runtest order
```
To see all the available options:
```
invoke test --help
invoke dev.test --help
```
## Code Style

View File

@ -52,7 +52,7 @@ Tasks can help you executing scripts. You can run them by open the command panel
#### Setup demo dataset
If you need some demo test-data, run the `setup-test` task. This will import an `admin` user with the password `inventree`. For more info on what this dataset contains see [inventree/demo-dataset](https://github.com/inventree/demo-dataset).
If you need some demo test-data, run the `setup-test` task. This will import an `admin` user with the password `inventree`. For more info on what this dataset contains see [inventree/demo-dataset](../demo.md).
#### Setup a superuser

View File

@ -1,37 +1,48 @@
---
title: Platform UI / React
title: React Frontend Development
---
## Setup
The new React-based UI will not be available by default. In order to set your development environment up to view the frontend, follow this guide.
The new UI requires a separate frontend server to run to serve data for the new Frontend.
The following documentation details how to setup and run a development installation of the InvenTree frontend user interface.
### Prerequisites
To run the frontend development server, you will need to have the following installed:
- Node.js
- Yarn
!!! note "Devcontainer"
The [devcontainer](./devcontainer.md) setup already includes all prerequisite packages, and is ready to run the frontend server.
### Install
The React frontend requires its own packages that aren't installed via the usual invoke tasks.
The React frontend requires its own packages that aren't installed via the usual [invoke](../start/invoke.md) tasks.
#### Docker
Run the following command:
`docker compose run inventree-dev-server invoke frontend-compile`
`docker compose run inventree-dev-server invoke int.frontend-compile`
This will install the required packages for running the React frontend on your InvenTree dev server.
#### Devcontainer
!!! warning "This guide assumes you already have a running devcontainer"
!!! info "All these steps are performed within Visual Studio Code"
Open a new terminal from the top menu by clicking `Terminal > New Terminal`
Make sure this terminal is running within the virtual env. The start of the last line should display `(venv)`
Run the command `invoke frontend-compile`. Wait for this to finish
Run the command `invoke int.frontend-compile`. Wait for this to finish
### Running
After finishing the install, you need to launch a frontend server to be able to view the new UI.
Using the previously described ways of running commands, execute the following:
`invoke frontend-dev` in your environment
`invoke dev.frontend-server` in your environment
This command does not run as a background daemon, and will occupy the window it's ran in.
### Accessing
@ -40,7 +51,7 @@ When the frontend server is running, it will be available on port 5173.
i.e: https://localhost:5173/
!!! note "Backend Server"
The InvenTree backend server must also be running, for the frontend interface to have something to connect to! To launch a backend server, use the `invoke server` command.
The InvenTree backend server must also be running, for the frontend interface to have something to connect to! To launch a backend server, use the `invoke dev.server` command.
### Debugging
@ -77,3 +88,27 @@ When running the frontend development server, some features may not work entirel
#### SSO Login
When logging into the frontend dev server via SSO, the redirect URL may not redirect correctly.
## Testing
The frontend codebase it tested using [Playwright](https://playwright.dev/). There are a large number of tests that cover the frontend codebase, which are run automatically as part of the CI pipeline.
### Install Playwright
To install the required packages to run the tests, you can use the following command:
```bash
cd src/frontend
npx playwright install
```
### Running Tests
To run the tests locally, in an interactive editor, you can use the following command:
```bash
cd src/frontend
npx playwright test --ui
```
This will first launch the backend server (at `http://localhost:8000`), and then run the tests against the frontend server (at `http://localhost:5173`). An interactive browser window will open, and you can run the tests individually or as a group.

View File

@ -4,6 +4,12 @@ title: Panel Mixin
## PanelMixin
!!! warning "Legacy User Interface"
This plugin mixin class is designed specifically for the the *legacy* user interface (which is rendered on the server using django templates). The new user interface (which is rendered on the client using React) does not support this mixin class. Instead, refer to the new [User Interface Mixin](./ui.md) class.
!!! warning "Deprecated Class"
This mixin class is considered deprecated, and will be removed in the 1.0.0 release.
The `PanelMixin` enables plugins to render custom content to "panels" on individual pages in the web interface.
Most pages in the web interface support multiple panels, which are selected via the sidebar menu on the left side of the screen:

View File

@ -0,0 +1,186 @@
---
title: User Interface Mixin
---
## User Interface Mixin
The `UserInterfaceMixin` class provides a set of methods to implement custom functionality for the InvenTree web interface.
### Enable User Interface Mixin
To enable user interface plugins, the global setting `ENABLE_PLUGINS_INTERFACE` must be enabled, in the [plugin settings](../../settings/global.md#plugin-settings).
## Custom UI Features
The InvenTree user interface functionality can be extended in various ways using plugins. Multiple types of user interface *features* can be added to the InvenTree user interface.
The entrypoint for user interface plugins is the `UserInterfaceMixin` class, which provides a number of methods which can be overridden to provide custom functionality. The `get_ui_features` method is used to extract available user interface features from the plugin:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_features
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
Note here that the `get_ui_features` calls other methods to extract the available features from the plugin, based on the requested feature type. These methods can be overridden to provide custom functionality.
!!! info "Implementation"
Your custom plugin does not need to override the `get_ui_features` method. Instead, override one of the other methods to provide custom functionality.
### UIFeature Return Type
The `get_ui_features` method should return a list of `UIFeature` objects, which define the available user interface features for the plugin. The `UIFeature` class is defined as follows:
::: plugin.base.ui.mixins.UIFeature
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
Note that the *options* field contains fields which may be specific to a particular feature type - read the documentation below on each feature type for more information.
### Dynamic Feature Loading
Each of the provided feature types can be loaded dynamically by the plugin, based on the information provided in the API request. For example, the plugin can choose to show or hide a particular feature based on the user permissions, or the current state of the system.
For examples of this dynamic feature loading, refer to the [sample plugin](#sample-plugin) implementation which demonstrates how to dynamically load custom panels based on the provided context.
### Javascript Source Files
The rendering function for the custom user interface features expect that the plugin provides a Javascript source file which contains the necessary code to render the custom content. The path to this file should be provided in the `source` field of the `UIFeature` object.
Note that the `source` field can include the name of the function to be called (if this differs from the expected default function name).
For example:
```
"source": "/static/plugins/my_plugin/my_plugin.js:my_custom_function"
```
## Available UI Feature Types
The following user interface feature types are available:
### Dashboard Items
The InvenTree dashboard is a collection of "items" which are displayed on the main dashboard page. Custom dashboard items can be added to the dashboard by implementing the `get_ui_dashboard_items` method:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_dashboard_items
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Dashboard Item Options
The *options* field in the returned `UIFeature` object can contain the following properties:
::: plugin.base.ui.mixins.CustomDashboardItemOptions
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Source Function
The frontend code expects a path to a javascript file containing a function named `renderDashboardItem` which will be called to render the custom dashboard item. Note that this function name can be overridden by appending the function name in the `source` field of the `UIFeature` object.
#### Example
Refer to the [sample plugin](#sample-plugin) for an example of how to implement server side rendering for custom panels.
### Panels
Many of the pages in the InvenTree web interface are built using a series of "panels" which are displayed on the page. Custom panels can be added to these pages, by implementing the `get_ui_panels` method:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_panels
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Panel Options
The *options* field in the returned `UIFeature` object can contain the following properties:
::: plugin.base.ui.mixins.CustomPanelOptions
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
#### Source Function
The frontend code expects a path to a javascript file containing a function named `renderPanel` which will be called to render the custom panel. Note that this function name can be overridden by appending the function name in the `source` field of the `UIFeature` object.
#### Example
Refer to the [sample plugin](#sample-plugin) for an example of how to implement server side rendering for custom panels.
### Template Editors
The `get_ui_template_editors` feature type can be used to provide custom template editors.
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_template_editors
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
### Template previews
The `get_ui_template_previews` feature type can be used to provide custom template previews:
::: plugin.base.ui.mixins.UserInterfaceMixin.get_ui_template_previews
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_sources: True
summary: False
members: []
## Plugin Context
When rendering certain content in the user interface, the rendering functions are passed a `context` object which contains information about the current page being rendered. The type of the `context` object is defined in the `PluginContext` file:
{{ includefile("src/frontend/src/components/plugins/PluginContext.tsx", title="Plugin Context", fmt="javascript") }}
This context data can be used to provide additional information to the rendering functions, and can be used to dynamically render content based on the current state of the system.
### Additional Context
Note that additional context can be passed to the rendering functions by adding additional key-value pairs to the `context` field in the `UIFeature` return type (provided by the backend via the API). This field is optional, and can be used at the discretion of the plugin developer.
## Sample Plugin
A sample plugin which implements custom user interface functionality is provided in the InvenTree source code, which provides a full working example of how to implement custom user interface functionality.
::: plugin.samples.integration.user_interface_sample.SampleUserInterfacePlugin
options:
show_bases: False
show_root_heading: False
show_root_toc_entry: False
show_source: True
members: []

View File

@ -14,37 +14,17 @@ InvenTree installation is not officially supported natively on Windows. However
### Command 'invoke' not found
If the `invoke` command does not work, it means that the [invoke](https://pypi.org/project/invoke/) python library has not been correctly installed.
If the `invoke` command does not work, it means that the invoke tool has not been correctly installed.
Update the installed python packages with PIP:
Refer to the [invoke installation guide](./start/invoke.md#installation) for more information.
```
pip3 install -U --require-hashes -r requirements.txt
```
### Can't find any collection named tasks
Refer to the [invoke guide](./start/invoke.md#cant-find-any-collection-named-tasks) for more information.
### Invoke Version
If the installed version of invoke is too old, users may see error messages during the installation procedure, such as:
- *'update' did not receive all required positional arguments!*
- *Function has keyword-only arguments or annotations*
As per the [invoke guide](./start/intro.md#invoke), the minimum required version of Invoke is `{{ config.extra.min_invoke_version }}`.
To determine the version of invoke you have installed, run either:
```
invoke --version
```
```
python -m invoke --version
```
If you are running an older version of invoke, ensure it is updated to the latest version:
```
pip install -U invoke
```
If the installed version of invoke is too old, users may see error messages during the installation procedure. Refer to the [invoke guide](./start/invoke.md#minimum-version) for more information.
### No module named 'django'

View File

@ -57,7 +57,7 @@ def fetch_rtd_versions():
versions = sorted(versions, key=lambda x: StrictVersion(x['version']), reverse=True)
# Add "latest" version first
if not any((x['title'] == 'latest' for x in versions)):
if not any(x['title'] == 'latest' for x in versions):
versions.insert(
0,
{
@ -70,7 +70,7 @@ def fetch_rtd_versions():
# Ensure we have the 'latest' version
current_version = os.environ.get('READTHEDOCS_VERSION', None)
if current_version and not any((x['title'] == current_version for x in versions)):
if current_version and not any(x['title'] == current_version for x in versions):
versions.append({
'version': current_version,
'title': current_version,
@ -82,7 +82,7 @@ def fetch_rtd_versions():
print('Discovered the following versions:')
print(versions)
with open(output_filename, 'w') as file:
with open(output_filename, 'w', encoding='utf-8') as file:
json.dump(versions, file, indent=2)
@ -100,7 +100,7 @@ def get_release_data():
# Release information has been cached to file
print("Loading release information from 'releases.json'")
with open(json_file) as f:
with open(json_file, encoding='utf-8') as f:
return json.loads(f.read())
# Download release information via the GitHub API
@ -127,7 +127,7 @@ def get_release_data():
page += 1
# Cache these results to file
with open(json_file, 'w') as f:
with open(json_file, 'w', encoding='utf-8') as f:
print("Saving release information to 'releases.json'")
f.write(json.dumps(releases))
@ -173,7 +173,7 @@ def on_config(config, *args, **kwargs):
# Add *all* readthedocs related keys
readthedocs = {}
for key in os.environ.keys():
for key in os.environ:
if key.startswith('READTHEDOCS_'):
k = key.replace('READTHEDOCS_', '').lower()
readthedocs[k] = os.environ[key]

View File

@ -93,6 +93,14 @@ There are two options to mark items as "received":
!!! note "Permissions"
Marking line items as received requires the "Purchase order" ADD permission.
### Item Location
When receiving items from a purchase order, the location of the items must be specified. There are multiple ways to specify the location:
* **Order Destination**: The *destination* field of the purchase order can be set to a specific location. When receiving items, the location will default to the destination location.
* **Line Item Location**: Each line item can have a specific location set. When receiving items, the location will default to the line item location. *Note: A destination specified at the line item level will override the destination specified at the order level.*
### Received Items
Each item marked as "received" is automatically converted into a stock item.

View File

@ -130,8 +130,8 @@ The following keyword arguments are available to the `render_currency` function:
| --- | --- |
| currency | Specify the currency code to render in (will attempt conversion if different to provided currency) |
| decimal_places | Specify the number of decimal places to render |
| min_decimal_places | Specify the minimum number of decimal places to render (ignored if *decimal_places* is specified) |
| max_decimal_places | Specify the maximum number of decimal places to render (ignored if *decimal_places* is specified) |
| min_decimal_places | Specify the minimum number of decimal places to render |
| max_decimal_places | Specify the maximum number of decimal places to render |
| include_symbol | Include currency symbol in rendered value (default = True) |
## Maths Operations

View File

@ -19,6 +19,7 @@ The following report templates are provided "out of the box" and can be used as
| [Purchase Order](#purchase-order) | [PurchaseOrder](../order/purchase_order.md) | Purchase Order report |
| [Return Order](#return-order) | [ReturnOrder](../order/return_order.md) | Return Order report |
| [Sales Order](#sales-order) | [SalesOrder](../order/sales_order.md) | Sales Order report |
| [Sales Order Shipment](#sales-order-shipment) | [SalesOrderShipment](../order/sales_order.md) | Sales Order Shipment report |
| [Stock Location](#stock-location) | [StockLocation](../stock/stock.md#stock-location) | Stock Location report |
| [Test Report](#test-report) | [StockItem](../stock/stock.md#stock-item) | Test Report |
@ -42,6 +43,10 @@ The following report templates are provided "out of the box" and can be used as
{{ templatefile("report/inventree_sales_order_report.html") }}
### Sales Order Shipment
{{ templatefile("report/inventree_sales_order_shipment_report.html") }}
### Stock Location
{{ templatefile("report/inventree_stock_location_report.html") }}

View File

@ -90,6 +90,8 @@ Configuration of barcode functionality:
{{ globalsetting("BARCODE_WEBCAM_SUPPORT") }}
{{ globalsetting("BARCODE_SHOW_TEXT") }}
{{ globalsetting("BARCODE_GENERATION_PLUGIN") }}
{{ globalsetting("BARCODE_STORE_RESULTS") }}
{{ globalsetting("BARCODE_RESULTS_MAX_NUM") }}
### Pricing and Currency
@ -121,8 +123,6 @@ Configuration of report generation:
{{ globalsetting("REPORT_DEFAULT_PAGE_SIZE") }}
{{ globalsetting("REPORT_DEBUG_MODE") }}
{{ globalsetting("REPORT_LOG_ERRORS") }}
{{ globalsetting("REPORT_ENABLE_TEST_REPORT") }}
{{ globalsetting("REPORT_ATTACH_TEST_REPORT") }}
### Parts
@ -188,6 +188,7 @@ Configuration of stock item options
{{ globalsetting("STOCK_ENFORCE_BOM_INSTALLATION") }}
{{ globalsetting("STOCK_ALLOW_OUT_OF_STOCK_TRANSFER") }}
{{ globalsetting("TEST_STATION_DATA") }}
{{ globalsetting("TEST_UPLOAD_CREATE_TEMPLATE") }}
### Build Orders
@ -207,7 +208,6 @@ Refer to the [return order settings](../order/return_order.md#return-order-setti
### Plugin Settings
| Name | Description | Default | Units |
| ---- | ----------- | ------- | ----- |
{{ globalsetting("PLUGIN_ON_STARTUP") }}
@ -217,3 +217,4 @@ Refer to the [return order settings](../order/return_order.md#return-order-setti
{{ globalsetting("ENABLE_PLUGINS_APP") }}
{{ globalsetting("ENABLE_PLUGINS_SCHEDULE") }}
{{ globalsetting("ENABLE_PLUGINS_EVENTS") }}
{{ globalsetting("ENABLE_PLUGINS_INTERFACE") }}

View File

@ -0,0 +1,49 @@
---
title: Account Management
---
## User Accounts
By default, InvenTree does not ship with any user accounts. Configuring user accounts is the first step to login to the InvenTree server.
### Administrator Account
You can configure InvenTree to create an administrator account on the first run. This account will have full *superuser* access to the InvenTree server.
This account is created when you first run the InvenTree server instance. The username / password for this account can be configured in the configuration file, or environment variables.
!!! info "More Information"
For more information on configuring the administrator account, refer to the [configuration documentation](./config.md#administrator-account).
### Create Superuser
Another way to create an administrator account is to use the `superuser` command (via [invoke](./invoke.md)). This will create a new superuser account with the specified username and password.
```bash
invoke superuser
```
Or, if you are running InvenTree in a Docker container:
```bash
docker exec -rm -it inventree-server invoke superuser
```
### User Management
Once you have created an administrator account, you can create and manage additional user accounts from the InvenTree web interface.
## Password Management
### Reset Password via Command Line
If a password has been lost, and other backup options (such as email recovery) are unavailable, the system administrator can reset the password for a user account from the command line.
Log into the machine running the InvenTree server, and run the following command (from the top-level source directory):
```bash
cd src/backend/InvenTree
python ./manage.py changepassword <username>
```
The system will prompt you to enter a new password for the specified user account.

View File

@ -17,7 +17,7 @@ InvenTree includes a simple server application, suitable for use in a developmen
To run the development server on a local machine, run the command:
```
(env) invoke server
(env) invoke dev.server
```
This will launch the InvenTree web interface at `http://127.0.0.1:8000`.
@ -25,7 +25,7 @@ This will launch the InvenTree web interface at `http://127.0.0.1:8000`.
A different port can be specified using the `-a` flag:
```
(env) invoke server -a 127.0.0.1:8123
(env) invoke dev.server -a 127.0.0.1:8123
```
Serving on the address `127.0.0.1` means that InvenTree will only be available *on that computer*. The server will be accessible from a web browser on the same computer, but not from any other computers on the local network.
@ -35,12 +35,12 @@ Serving on the address `127.0.0.1` means that InvenTree will only be available *
To enable access to the InvenTree server from other computers on a local network, you need to know the IP of the computer running the server. For example, if the server IP address is `192.168.120.1`:
```
(env) invoke server -a 192.168.120.1:8000
(env) invoke dev.server -a 192.168.120.1:8000
```
## Background Worker
The background task manager must also be started. The InvenTree server is already running in the foreground, so open a *new shell window* to start the server.
The [background task manager](./processes.md#background-worker) must also be started. The InvenTree server is already running in the foreground, so open a *new shell window* to start the server.
### Activate Virtual Environment
@ -55,4 +55,4 @@ source ./env/bin/activate
(env) invoke worker
```
This will start the background process manager in the current shell.
This will start an instance of the background worker in the current shell.

View File

@ -19,7 +19,7 @@ The InvenTree web server is hosted using [Gunicorn](https://gunicorn.org/). Guni
### Supervisor
[Supervisor](http://supervisord.org/) is a process control system which monitors and controls multiple background processes. It is used in the InvenTree production setup to ensure that the server and background worker processes are always running.
[Supervisor](http://supervisord.org/) is a process control system which monitors and controls multiple background processes. It is used in the InvenTree production setup to ensure that the [web server](./processes.md#web-server) and [background worker](./processes.md#background-worker) processes are always running.
## Gunicorn
@ -98,11 +98,12 @@ The InvenTree server (and background task manager) should now be running!
In addition to the InvenTree server, you will need a method of delivering static and media files (this is *not* handled by the InvenTree server in a production environment).
!!! info "Read More"
Refer to the [Serving Files](./serving_files.md) section for more details
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details
### Next Steps
You (or your system administrator) may wish to perform further steps such as placing the InvenTree server behind a reverse-proxy such as [caddy](https://caddyserver.com/), or [nginx](https://www.nginx.com/).
You (or your system administrator) may wish to perform further steps such as placing the InvenTree server behind a [reverse proxy](./processes.md#proxy-server) such as [caddy](https://caddyserver.com/), or [nginx](https://www.nginx.com/).
As production environment options are many and varied, such tasks are outside the scope of this documentation.
There are many great online tutorials about running django applications in production!

View File

@ -26,7 +26,7 @@ The InvenTree server tries to locate the `config.yaml` configuration file on sta
The configuration file *template* can be found on [GitHub]({{ sourcefile("src/backend/InvenTree/config_template.yaml") }}), and is shown below:
{{ includefile("src/backend/InvenTree/config_template.yaml", "Configuration File Template", format="yaml") }}
{{ includefile("src/backend/InvenTree/config_template.yaml", "Configuration File Template", fmt="yaml") }}
!!! info "Template File"
The default configuration file (as defined by the template linked above) will be copied to the specified configuration file location on first run, if a configuration file is not found in that location.
@ -107,7 +107,22 @@ Depending on how your InvenTree installation is configured, you will need to pay
| INVENTREE_USE_X_FORWARDED_HOST | use_x_forwarded_host | Use forwarded host header | `False` |
| INVENTREE_USE_X_FORWARDED_PORT | use_x_forwarded_port | Use forwarded port header | `False` |
| INVENTREE_SESSION_COOKIE_SECURE | cookie.secure | Enforce secure session cookies | `False` |
| INVENTREE_COOKIE_SAMESITE | cookie.samesite | Session cookie mode. Must be one of `Strict | Lax | None`. Refer to the [mozilla developer docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) for more information. | `None` |
| INVENTREE_COOKIE_SAMESITE | cookie.samesite | Session cookie mode. Must be one of `Strict | Lax | None | False`. Refer to the [mozilla developer docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) and the [django documentation]({% include "django.html" %}/ref/settings/#std-setting-SESSION_COOKIE_SAMESITE) for more information. | False |
### Debug Mode
Note that in [debug mode](./intro.md#debug-mode), some of the above settings are automatically adjusted to allow for easier development:
| Setting | Value in Debug Mode | Description |
| --- | --- | --- |
| `INVENTREE_ALLOWED_HOSTS` | `*` | Allow all host in debug mode |
| `CSRF_TRUSTED_ORIGINS` | Value is appended to allow `http://*.localhost:*` | Allow all connections from localhost, for development purposes |
| `INVENTREE_COOKIE_SAMESITE` | `False` | Disable all same-site cookie checks in debug mode |
| `INVENTREE_SESSION_COOKIE_SECURE` | `False` | Disable secure session cookies in debug mode (allow non-https cookies) |
### INVENTREE_COOKIE_SAMESITE vs INVENTREE_SESSION_COOKIE_SECURE
Note that if you set the `INVENTREE_COOKIE_SAMESITE` to `None`, then `INVENTREE_SESSION_COOKIE_SECURE` is automatically set to `True` to ensure that the session cookie is secure! This means that the session cookie will only be sent over secure (https) connections.
### Proxy Settings
@ -264,12 +279,12 @@ InvenTree requires some external directories for storing files:
| Environment Variable | Configuration File | Description | Default |
| --- | --- | --- | --- |
| INVENTREE_STATIC_ROOT | static_root | [Static files](./serving_files.md#static-files) directory | *Not specified* |
| INVENTREE_MEDIA_ROOT | media_root | [Media files](./serving_files.md#media-files) directory | *Not specified* |
| INVENTREE_STATIC_ROOT | static_root | [Static files](./processes.md#static-files) directory | *Not specified* |
| INVENTREE_MEDIA_ROOT | media_root | [Media files](./processes.md#media-files) directory | *Not specified* |
| INVENTREE_BACKUP_DIR | backup_dir | Backup files directory | *Not specified* |
!!! tip "Serving Files"
Read the [Serving Files](./serving_files.md) section for more information on hosting *static* and *media* files
Read the [proxy server documentation](./processes.md#proxy-server) for more information on hosting *static* and *media* files
### Static File Storage
@ -354,6 +369,15 @@ The logo and custom messages can be changed/set:
| INVENTREE_CUSTOMIZE | customize.navbar_message | Custom message for navbar | *Not specified* |
| INVENTREE_CUSTOMIZE | customize.hide_pui_banner | Disable PUI banner | False |
The INVENTREE_CUSTOMIZE environment variable must contain a json object with the keys from the table above and
the wanted values. Example:
```
INVENTREE_CUSTOMIZE={"login_message":"Hallo Michi","hide_pui_banner":"True"}
```
This example removes the PUI banner and sets a login message. Take care of the double quotes.
If you want to remove the InvenTree branding as far as possible from your end-user also check the [global server settings](../settings/global.md#server-settings).
!!! info "Custom Splash Screen Path"

View File

@ -87,7 +87,7 @@ Plugins are supported natively when running under docker. There are two ways to
The production docker compose configuration outlined on this page uses [Caddy](https://caddyserver.com/) to serve static files and media files. If you change this configuration, you will need to ensure that static and media files are served correctly.
!!! info "Read More"
Refer to the [Serving Files](./serving_files.md) section for more details
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details
### SSL Certificates
@ -99,45 +99,11 @@ The example docker compose file launches the following containers:
| Container | Description |
| --- | --- |
| inventree-db | PostgreSQL database |
| inventree-server | Gunicorn web server |
| inventree-worker | django-q background worker |
| inventree-proxy | Caddy file server and reverse proxy |
| *inventree-cache* | *redis cache (optional)* |
#### PostgreSQL Database
A PostgreSQL database container which requires a username:password combination (which can be changed). This uses the official [PostgreSQL image](https://hub.docker.com/_/postgres).
#### Web Server
Runs an InvenTree web server instance, powered by a Gunicorn web server.
#### Background Worker
Runs the InvenTree background worker process. This spins up a second instance of the *inventree* container, with a different entrypoint command.
#### Proxy Server
Caddy working as a reverse proxy, separating requests for static and media files, and directing everything else to Gunicorn.
This container uses the official [caddy image](https://hub.docker.com/_/caddy).
!!! info "Nginx Proxy"
An alternative is to run nginx as the reverse proxy. A sample configuration file is provided in the `./contrib/container/` source directory.
#### Redis Cache
Redis is used as cache storage for the InvenTree server. This provides a more performant caching system which can useful in larger installations.
This container uses the official [redis image](https://hub.docker.com/_/redis).
!!! 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).
!!! 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)
| inventree-db | [PostgreSQL database](./processes.md#database) |
| inventree-server | [InvenTree web server](./processes.md#web-server) |
| inventree-worker | [django-q background worker](./processes.md#background-worker) |
| inventree-proxy | [Caddy file server and reverse proxy](./processes.md#proxy-server) |
| *inventree-cache* | [*redis cache (optional)*](./processes.md#cache-server) |
### Data Volume

View File

@ -37,8 +37,11 @@ The following files required for this setup are provided with the InvenTree sour
Download these files to a directory on your local machine.
!!! warning "File Extensions"
If your computer adds *.txt* extensions to any of the downloaded files, rename the file and remove the added extension before continuing!
!!! success "Working Directory"
This tutorial assumes you are working from a direction where all of these files are located.
This tutorial assumes you are working from a directory where all of these files are located.
!!! tip "No Source Required"
For a production setup you do not need the InvenTree source code. Simply download the three required files from the links above!
@ -101,10 +104,11 @@ docker compose up -d
This command launches the following containers:
- `inventree-db` - PostgreSQL database
- `inventree-server` - InvenTree web server
- `inventree-worker` - Background worker
- `inventree-proxy` - Caddy reverse proxy
- `inventree-db` - [PostgreSQL database](./processes.md#database)
- `inventree-server` - [InvenTree web server](./processes.md#web-server)
- `inventree-worker` - [Background worker](./processes.md#background-worker)
- `inventree-proxy` - [Caddy reverse proxy](./processes.md#proxy-server)
- `inventree-cache` - [Redis cache](./processes.md#cache-server)
!!! success "Up and Running!"
You should now be able to view the InvenTree login screen at [http://inventree.localhost](http://inventree.localhost) (or whatever custom domain you have configured in the `.env` file).
@ -202,7 +206,7 @@ Any persistent files generated by the Caddy container (such as certificates, etc
To quickly get started with a [demo dataset](../demo.md), you can run the following command:
```
docker compose run --rm inventree-server invoke setup-test -i
docker compose run --rm inventree-server invoke dev.setup-test -i
```
This will install the InvenTree demo dataset into your instance.
@ -210,7 +214,7 @@ This will install the InvenTree demo dataset into your instance.
To start afresh (and completely remove the existing database), run the following command:
```
docker compose run --rm inventree-server invoke delete-data
docker compose run --rm inventree-server invoke dev.delete-data
```
## Install custom packages

View File

@ -66,14 +66,14 @@ In addition to the location where the InvenTree source code is located, you will
InvenTree requires a directory for storage of [static files](./config.md#static-file-storage).
!!! info "Read More"
Refer to the [Serving Files](./serving_files.md) section for more details
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details
#### Media Files
InvenTree requires a directory for storage of [user uploaded files](./config.md#uploaded-file-storage)
!!! info "Read More"
Refer to the [Serving Files](./serving_files.md) section for more details
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details
#### Backup Directory

View File

@ -132,11 +132,14 @@ To update InvenTree run `apt install --only-upgrade inventree` - this might need
## Controlling InvenTree
### Services
InvenTree installs multiple services that can be controlled with your local system runner (`service` or `systemctl`).
The service `inventree` controls everything, `inventree-web` the (internal) webserver and `inventree-worker` the background worker(s).
The service `inventree` controls everything, `inventree-web` (the [InvenTree web server](./processes.md#web-server)) and `inventree-worker` the [background worker(s)](./processes.md#background-worker).
More instances of the worker can be instantiated from the command line. This is only meant for advanced users.
This sample script launches 3 services. By default, 1 is launched.
```bash
inventree scale worker=3
```

View File

@ -29,30 +29,7 @@ Independent of the preferred installation method, InvenTree provides a number of
## System Components
The InvenTree server ecosystem consists of the following components:
### Database
A persistent database is required for data storage. By default, InvenTree is configured to use [PostgreSQL](https://www.postgresql.org/) - and this is the recommended database backend to use. However, InvenTree can also be configured to connect to any database backend [supported by Django]({% include "django.html" %}/ref/databases/)
### Web Server
The bulk of the InvenTree code base supports the custom web server application. The web server application services user requests and facilitates database access. The webserver provides access to the [API](../api/api.md) for performing database query actions.
InvenTree uses [Gunicorn](https://gunicorn.org/) as the web server - a Python WSGI HTTP server.
### Background Tasks
A separate application handles management of [background tasks](../settings/tasks.md), separate to user-facing web requests. The background task manager is required to perform asynchronous tasks, such as sending emails, generating reports, and other long-running tasks.
InvenTree uses [django-q2](https://django-q2.readthedocs.io/en/master/) as the background task manager.
### File Storage
Uploaded *media* files (images, attachments, reports, etc) and *static* files (javascript, html) are stored to a persistent storage volume. A *file server* is required to serve these files to the user.
InvenTree uses [Caddy](https://caddyserver.com/) as a file server, which is configured to serve both *static* and *media* files. Additionally, Caddy provides SSL termination and reverse proxy services.
The InvenTree software stack is composed of multiple components, each of which is required for a fully functional server environment. Your can read more about the [InvenTree processes here](./processes.md).
## OS Requirements
@ -70,25 +47,7 @@ InvenTree requires a minimum Python version of {{ config.extra.min_python_versio
### Invoke
InvenTree makes use of the [invoke](https://www.pyinvoke.org/) python toolkit for performing various administrative actions.
!!! warning "Invoke Version"
InvenTree requires invoke version {{ config.extra.min_invoke_version }} or newer. Some platforms may be shipped with older versions of invoke!
!!! tip "Updating Invoke"
To update your invoke version, run `pip install -U invoke`
To display a list of the available InvenTree administration actions, run the following commands from the top level source directory:
```
invoke --list
```
This provides a list of the available invoke commands - also displayed below:
```
{{ invoke_commands() }}
```
InvenTree makes use of the [invoke](https://www.pyinvoke.org/) python toolkit for performing various administrative actions. You can read [more about out use of the invoke tool here](./invoke.md)
### Virtual Environment
@ -150,4 +109,4 @@ So, for a production setup, you should set `INVENTREE_DEBUG=false` in the [confi
Turning off DEBUG mode creates further work for the system administrator. In particular, when running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that the InvenTree server can run "monolithically" without the need for a separate web server.
!!! info "Read More"
Refer to the [Serving Files](./serving_files.md) section for more details
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details

129
docs/docs/start/invoke.md Normal file
View File

@ -0,0 +1,129 @@
---
title: Invoke Tool
---
## Invoke Tool
InvenTree uses the [invoke](https://www.pyinvoke.org/) tool to manage various system administration tasks. Invoke is a powerful python-based task execution tool, which allows for the creation of custom tasks and command-line utilities.
### Installation
InvenTree setup and administration requires that the invoke tool is installed. This is usually installed automatically as part of the InvenTree installation process - however (if you are configuring InvenTree from source) you may need to install it manually.
To install the invoke tool, run the following command:
```
pip install -U invoke
```
### Minimum Version
The minimum required version of the invoke tool is `{{ config.extra.min_invoke_version }}`.
To determine the version of invoke you have installed, run either:
```
invoke --version
```
```
python -m invoke --version
```
If you are running an older version of invoke, ensure it is updated to the latest version:
```
pip install -U invoke
```
### Running from Command Line
To run the `invoke` tool from the command line, you must be in the top-level InvenTree source directory. This is the directory that contains the [tasks.py]({{ sourcefile("tasks.py") }}) file.
### Running in Docker Mode
If you have installed InvenTree via [docker](./docker_install.md), then you need to ensure that the `invoke` commands are called from within the docker container context.
For example, to run the `update` task, you might use the following command to run the `invoke` command - using the `docker compose` tool.
```
docker compose run --rm inventree-server invoke update
```
!!! note "Docker Compose Directory"
The `docker compose` command should be run from the directory where the `docker-compose.yml` file is located.
Alternatively, to manually run the command within the environment of the running docker container:
```
docker exec -it inventree-server invoke update
```
!!! note "Container Name"
The container name may be different depending on how you have configured the docker environment.
### Running in Installer Mode
If you have installed InvenTree using the [package installer](./installer.md), then you need to prefix all `invoke` commands with `inventree run`.
For example, to run the `update` task, use:
```
inventree run invoke update
```
## Available Tasks
To display a list of the available InvenTree administration actions, run the following commands from the top level source directory:
```
invoke --list
```
This provides a list of the available invoke commands - also displayed below:
```
{{ invoke_commands() }}
```
### Task Information
Each task has a brief description of its purpose, which is displayed when running the `invoke --list` command. To find more detailed information about a specific task, run the command with the `--help` flag.
For example, to find more information about the `update` task, run:
```
invoke update --help
```
### Internal Tasks
Tasks with the `int.` prefix are internal tasks, and are not intended for general use. These are called by other tasks, and should generally not be called directly.
### Developer Tasks
Tasks with the `dev.` prefix are tasks intended for InvenTree developers, and are also not intended for general use.
## Common Issues
Below are some common issues that users may encounter when using the `invoke` tool, and how to resolve them.
### Command 'invoke' not found
If the `invoke` command does not work, it means that the invoke tool has not been [installed correctly](#installation).
### Invoke Version
If the installed version of invoke is too old, users may see error messages during the installation procedure, such as:
- *'update' did not receive all required positional arguments!*
- *Function has keyword-only arguments or annotations*
Ensure that the installed version of invoke is [up to date](#minimum-version).
### Can't find any collection named 'tasks'
It means that the `invoke` tool is not able to locate the InvenTree task collection.
- If running in docker, ensure that you are running the `invoke` command from within the [docker container](#running-in-docker-mode)
- If running in installer mode, ensure that you are running the `invoke` command with the [correct prefix](#running-in-installer-mode)
- If running via command line, ensure that you are running the `invoke` command from the [correct directory](#running-from-command-line)

View File

@ -0,0 +1,103 @@
---
title: InvenTree Processes
---
## InvenTree Processes
InvenTree is a complex application, and there are a number of processes which must be running in order for the system to operate correctly. Typically, these processes are started automatically when the InvenTree application stack is launched. However, in some cases, it may be necessary to start these processes manually.
System administrators should be aware of the following processes when configuring an InvenTree installation:
### Database
At the core of the InvenTree application is the SQL database. The database is responsible for storing all of the persistent data which is used by the InvenTree application.
InvenTree supports a [number of database backends]({% include "django.html" %}/ref/databases) - which typically require their own process to be running.
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).
### Web Server
The InvenTree web server is responsible for serving the InvenTree web interface to users. The web server is a [Django](https://www.djangoproject.com/) application, which is run using the [Gunicorn](https://gunicorn.org/) WSGI server.
The web server interfaces with the backend database and provides a [REST API](../api/api.md) (via the [Django REST framework](https://www.django-rest-framework.org/)) which is used by the frontend web interface.
In running InvenTree via [docker compose](./docker_install.md), the web server process is managed by the `inventree-server` service, which runs from a custom docker image.
### Proxy Server
In a production installation, the InvenTree web server application *does not* provide hosting of static files, or user-uploaded (media) files. Instead, these files should be served by a separate web server, such as [Caddy](https://caddyserver.com/), [Nginx](https://www.nginx.com/), or [Apache](https://httpd.apache.org/).
!!! info "Debug Mode"
When running in [production mode](./bare_prod.md) (i.e. the `INVENTREE_DEBUG` flag is disabled), a separate web server is required for serving *static* and *media* files. In `DEBUG` mode, the django webserver facilitates delivery of *static* and *media* files, but this is explicitly not suitable for a production environment.
!!! tip "Read More"
You can find further information in the [django documentation]({% include "django.html" %}/howto/static-files/deployment/).
A proxy server is required to store and serve static files (such as images, documents, etc) which are used by the InvenTree application. As django is not designed to serve static files in a production environment, a separate file server is required. For our docker compose setup, we use the `inventree-proxy` service, which runs a [Caddy](https://caddyserver.com/) proxy server to serve static files.
In addition to serving static files, the proxy server also provides a reverse proxy to the InvenTree web server, allowing the InvenTree web interface to be accessed via a standard HTTP/HTTPS port.
Further, it provides an authentication endpoint for accessing files in the `/static/` and `/media/` directories.
Finally, it provides a [Let's Encrypt](https://letsencrypt.org/) endpoint for automatic SSL certificate generation and renewal.
#### Static Files
Static files can be served without any need for authentication. In fact, they must be accessible *without* authentication, otherwise the unauthenticated views (such as the login screen) will not function correctly.
#### Media Files
It is highly recommended that the *media* files are served behind an authentication layer. This is because the media files are user-uploaded, and may contain sensitive information. Most modern web servers provide a way to serve files behind an authentication layer.
#### Example Configuration
The [docker production example](./docker.md) provides an example using [Caddy](https://caddyserver.com) to serve *static* and *media* files, and redirecting other requests to the InvenTree web server itself.
Caddy is a modern web server which is easy to configure and provides a number of useful features, including automatic SSL certificate generation.
#### Alternatives to Caddy
An alternative is to run nginx as the reverse proxy. A sample configuration file is provided in the `./contrib/container/` source directory.
#### Integrating with Existing Proxy
You may wish to integrate the InvenTree web server with an existing reverse proxy server. This is possible, but requires careful configuration to ensure that the static and media files are served correctly.
*Note: A custom configuration of the proxy server is outside the scope of this documentation!*
### Background Worker
The InvenTree background worker is responsible for handling [asynchronous tasks](../settings/tasks.md) which are not suitable for the main web server process. This includes tasks such as sending emails, generating reports, and other long-running tasks.
InvenTree uses the [django-q2](https://django-q2.readthedocs.io/en/master/) package to manage background tasks.
The background worker process is managed by the `inventree-worker` service in the [docker compose](./docker_install.md) setup. Note that this services runs a duplicate copy of the `inventree-server` container, but with a different entrypoint command which starts the background worker process.
#### Important Information
If the background worker process is not running, InvenTree will not be able to perform background tasks. This can lead to issues such as emails not being sent, or reports not being generated. Additionally, certain data may not be updated correctly if the background worker is not running.
!!! warning "Background Worker"
The background worker process is a critical part of the InvenTree application stack. It is important that this process is running correctly in order for the InvenTree application to operate correctly.
#### Limitations
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.
### 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.
Using a cache server can significantly improve the performance of the InvenTree application, as it reduces the need to query the database for frequently accessed data.
InvenTree uses the [Redis](https://redis.io/) cache server to manage cache data. When running in docker, we use the official [redis image](https://hub.docker.com/_/redis).
!!! 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).
!!! 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)

View File

@ -1,29 +0,0 @@
---
title: Serving Static and Media Files
---
## Serving Files
In a production installation, the InvenTree web server application *does not* provide hosting of static files, or user-uploaded (media) files. Instead, these files should be served by a separate web server, such as [Caddy](https://caddyserver.com/), [Nginx](https://www.nginx.com/), or [Apache](https://httpd.apache.org/).
!!! info "Debug Mode"
When running in [production mode](./bare_prod.md) (i.e. the `INVENTREE_DEBUG` flag is disabled), a separate web server is required for serving *static* and *media* files. In `DEBUG` mode, the django webserver facilitates delivery of *static* and *media* files, but this is explicitly not suitable for a production environment.
!!! tip "Read More"
You can find further information in the [django documentation]({% include "django.html" %}/howto/static-files/deployment/).
There are *many* different ways that a sysadmin might wish to handle this - and it depends on your particular installation requirements.
### Static Files
Static files can be served without any need for authentication. In fact, they must be accessible *without* authentication, otherwise the unauthenticated views (such as the login screen) will not function correctly.
### Media Files
It is highly recommended that the *media* files are served behind an authentication layer. This is because the media files are user-uploaded, and may contain sensitive information. Most modern web servers provide a way to serve files behind an authentication layer.
### Example Configuration
The [docker production example](./docker.md) provides an example using [Caddy](https://caddyserver.com) to serve *static* and *media* files, and redirecting other requests to the InvenTree web server itself.
Caddy is a modern web server which is easy to configure and provides a number of useful features, including automatic SSL certificate generation.

View File

@ -46,7 +46,7 @@ def top_level_path(path: str) -> str:
key = path.split('/')[1]
if key in SPECIAL_PATHS.keys():
if key in SPECIAL_PATHS:
return key
return GENERAL_PATH
@ -54,9 +54,7 @@ def top_level_path(path: str) -> str:
def generate_schema_file(key: str) -> None:
"""Generate a schema file for the provided key."""
description = (
SPECIAL_PATHS[key] if key in SPECIAL_PATHS else 'General API Endpoints'
)
description = SPECIAL_PATHS.get(key, 'General API Endpoints')
output = f"""
---
@ -75,7 +73,7 @@ def generate_schema_file(key: str) -> None:
print('Writing schema file to:', output_file)
with open(output_file, 'w') as f:
with open(output_file, 'w', encoding='utf-8') as f:
f.write(output)
@ -121,7 +119,7 @@ def generate_index_file(version: str):
print('Writing index file to:', output_file)
with open(output_file, 'w') as f:
with open(output_file, 'w', encoding='utf-8') as f:
f.write(output)
@ -173,7 +171,7 @@ def parse_api_file(filename: str):
The intent is to make the API schema easier to peruse on the documentation.
"""
with open(filename, 'r') as f:
with open(filename, encoding='utf-8') as f:
data = yaml.safe_load(f)
paths = data['paths']
@ -213,7 +211,7 @@ def parse_api_file(filename: str):
output_file = os.path.abspath(output_file)
with open(output_file, 'w') as f:
with open(output_file, 'w', encoding='utf-8') as f:
yaml.dump(output, f)
# Generate a markdown file for the schema

View File

@ -16,7 +16,7 @@ global USER_SETTINGS
here = os.path.dirname(__file__)
settings_file = os.path.join(here, 'inventree_settings.json')
with open(settings_file, 'r') as sf:
with open(settings_file, encoding='utf-8') as sf:
settings = json.load(sf)
GLOBAL_SETTINGS = settings['global']
@ -27,7 +27,7 @@ def get_repo_url(raw=False):
"""Return the repository URL for the current project."""
mkdocs_yml = os.path.join(os.path.dirname(__file__), 'mkdocs.yml')
with open(mkdocs_yml, 'r') as f:
with open(mkdocs_yml, encoding='utf-8') as f:
mkdocs_config = yaml.safe_load(f)
repo_name = mkdocs_config['repo_name']
@ -47,7 +47,7 @@ def check_link(url) -> bool:
# Keep a local cache file of URLs we have already checked
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'r') as f:
with open(CACHE_FILE, encoding='utf-8') as f:
cache = f.read().splitlines()
if url in cache:
@ -59,7 +59,7 @@ def check_link(url) -> bool:
response = requests.head(url, timeout=5000)
if response.status_code == 200:
# Update the cache file
with open(CACHE_FILE, 'a') as f:
with open(CACHE_FILE, 'a', encoding='utf-8') as f:
f.write(f'{url}\n')
return True
@ -177,7 +177,7 @@ def define_env(env):
assert subprocess.call(command, shell=True) == 0
with open(output, 'r') as f:
with open(output, encoding='utf-8') as f:
content = f.read()
return content
@ -200,12 +200,13 @@ def define_env(env):
return assets
@env.macro
def includefile(filename: str, title: str, format: str = ''):
def includefile(filename: str, title: str, fmt: str = ''):
"""Include a file in the documentation, in a 'collapse' block.
Arguments:
- filename: The name of the file to include (relative to the top-level directory)
- title:
- fmt:
"""
here = os.path.dirname(__file__)
path = os.path.join(here, '..', filename)
@ -214,11 +215,11 @@ def define_env(env):
if not os.path.exists(path):
raise FileNotFoundError(f'Required file {path} does not exist.')
with open(path, 'r') as f:
with open(path, encoding='utf-8') as f:
content = f.read()
data = f'??? abstract "{title}"\n\n'
data += f' ```{format}\n'
data += f' ```{fmt}\n'
data += textwrap.indent(content, ' ')
data += '\n\n'
data += ' ```\n\n'
@ -233,15 +234,15 @@ def define_env(env):
'src', 'backend', 'InvenTree', 'report', 'templates', filename
)
return includefile(fn, f'Template: {base}', format='html')
return includefile(fn, f'Template: {base}', fmt='html')
@env.macro
def rendersetting(setting: dict):
"""Render a provided setting object into a table row."""
name = setting['name']
description = setting['description']
default = setting.get('default', None)
units = setting.get('units', None)
default = setting.get('default')
units = setting.get('units')
return f'| {name} | {description} | {default if default is not None else ""} | {units if units is not None else ""} |'

View File

@ -88,6 +88,7 @@ nav:
- Security: security.md
- Install:
- Introduction: start/intro.md
- Processes: start/processes.md
- Configuration: start/config.md
- Docker:
- Introduction: start/docker.md
@ -97,8 +98,9 @@ nav:
- Installer: start/installer.md
- Production: start/bare_prod.md
- Development: start/bare_dev.md
- Serving Files: start/serving_files.md
- User Accounts: start/accounts.md
- Data Backup: start/backup.md
- Invoke: start/invoke.md
- Migrating Data: start/migrate.md
- Advanced Topics: start/advanced.md
- Parts:
@ -213,6 +215,7 @@ nav:
- Schedule Mixin: extend/plugins/schedule.md
- Settings Mixin: extend/plugins/settings.md
- URL Mixin: extend/plugins/urls.md
- User Interface Mixin: extend/plugins/ui.md
- Validation Mixin: extend/plugins/validation.md
- Machines:
- Overview: extend/machines/overview.md

View File

@ -6,6 +6,9 @@
{
"pattern": "http://localhost"
},
{
"pattern": "https://localhost:5173/"
},
{
"pattern": "http://127.0.0.1"
},
@ -14,6 +17,9 @@
},
{
"pattern": "https://www.reddit.com/r/InvenTree/"
},
{
"pattern": "https://opensource.org/license/MIT"
}
]
}

View File

@ -1,4 +1,4 @@
mkdocs==1.6.0
mkdocs==1.6.1
mkdocs-macros-plugin>=0.7,<2.0
mkdocs-material>=9.0,<10.0
mkdocs-git-revision-date-localized-plugin>=1.1,<2.0

View File

@ -158,6 +158,12 @@ h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
# via httpcore
hjson==3.1.0 \
--hash=sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75 \
--hash=sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89
# via
# mkdocs-macros-plugin
# super-collections
httpcore==1.0.5 \
--hash=sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61 \
--hash=sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5
@ -173,9 +179,9 @@ idna==3.7 \
# anyio
# httpx
# requests
importlib-metadata==8.2.0 \
--hash=sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369 \
--hash=sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d
importlib-metadata==8.5.0 \
--hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \
--hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7
# via
# markdown
# mkdocs
@ -280,9 +286,9 @@ mergedeep==1.3.4 \
# via
# mkdocs
# mkdocs-get-deps
mkdocs==1.6.0 \
--hash=sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7 \
--hash=sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512
mkdocs==1.6.1 \
--hash=sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2 \
--hash=sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e
# via
# -r docs/requirements.in
# mkdocs-autorefs
@ -293,29 +299,29 @@ mkdocs==1.6.0 \
# mkdocs-simple-hooks
# mkdocstrings
# neoteroi-mkdocs
mkdocs-autorefs==1.0.1 \
--hash=sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570 \
--hash=sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971
mkdocs-autorefs==1.2.0 \
--hash=sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f \
--hash=sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f
# via mkdocstrings
mkdocs-get-deps==0.2.0 \
--hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \
--hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134
# via mkdocs
mkdocs-git-revision-date-localized-plugin==1.2.7 \
--hash=sha256:2f83b52b4dad642751a79465f80394672cbad022129286f40d36b03aebee490f \
--hash=sha256:d2b30ccb74ec8e118298758d75ae4b4f02c620daf776a6c92fcbb58f2b78f19f
mkdocs-git-revision-date-localized-plugin==1.3.0 \
--hash=sha256:439e2f14582204050a664c258861c325064d97cdc848c541e48bb034a6c4d0cb \
--hash=sha256:c99377ee119372d57a9e47cff4e68f04cce634a74831c06bc89b33e456e840a1
# via -r docs/requirements.in
mkdocs-include-markdown-plugin==6.2.2 \
--hash=sha256:d293950f6499d2944291ca7b9bc4a60e652bbfd3e3a42b564f6cceee268694e7 \
--hash=sha256:f2bd5026650492a581d2fd44be6c22f90391910d76582b96a34c264f2d17875d
mkdocs-include-markdown-plugin==7.0.0 \
--hash=sha256:a8eac8f2e6aa391d82d1d5e473b819b52393d91464060c02db5741834fe9008b \
--hash=sha256:bf8d19245ae3fb2eea395888e80c60bc91806a0d879279707d707896c24319c3
# via -r docs/requirements.in
mkdocs-macros-plugin==1.0.5 \
--hash=sha256:f60e26f711f5a830ddf1e7980865bf5c0f1180db56109803cdd280073c1a050a \
--hash=sha256:fe348d75f01c911f362b6d998c57b3d85b505876dde69db924f2c512c395c328
mkdocs-macros-plugin==1.3.7 \
--hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \
--hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523
# via -r docs/requirements.in
mkdocs-material==9.5.32 \
--hash=sha256:38ed66e6d6768dde4edde022554553e48b2db0d26d1320b19e2e2b9da0be1120 \
--hash=sha256:f3704f46b63d31b3cd35c0055a72280bed825786eccaf19c655b44e0cd2c6b3f
mkdocs-material==9.5.43 \
--hash=sha256:4aae0664c456fd12837a3192e0225c17960ba8bf55d7f0a7daef7e4b0b914a34 \
--hash=sha256:83be7ff30b65a1e4930dfa4ab911e75780a3afc9583d162692e434581cb46979
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
@ -325,9 +331,9 @@ mkdocs-simple-hooks==0.1.5 \
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
# via -r docs/requirements.in
mkdocstrings[python]==0.25.2 \
--hash=sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc \
--hash=sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc
mkdocstrings[python]==0.26.2 \
--hash=sha256:1248f3228464f3b8d1a15bd91249ce1701fe3104ac517a5f167a0e01ca850ba5 \
--hash=sha256:34a8b50f1e6cfd29546c6c09fbe02154adfb0b361bb758834bf56aa284ba876e
# via
# -r docs/requirements.in
# mkdocstrings-python
@ -342,14 +348,18 @@ neoteroi-mkdocs==1.1.0 \
packaging==24.0 \
--hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
--hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
# via mkdocs
# via
# mkdocs
# mkdocs-macros-plugin
paginate==0.5.6 \
--hash=sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d
# via mkdocs-material
pathspec==0.12.1 \
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
# via mkdocs
# via
# mkdocs
# mkdocs-macros-plugin
platformdirs==4.2.2 \
--hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \
--hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3
@ -443,86 +453,101 @@ pyyaml-env-tag==0.1 \
--hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \
--hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069
# via mkdocs
regex==2024.7.24 \
--hash=sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c \
--hash=sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535 \
--hash=sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24 \
--hash=sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce \
--hash=sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc \
--hash=sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5 \
--hash=sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce \
--hash=sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53 \
--hash=sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d \
--hash=sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c \
--hash=sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908 \
--hash=sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8 \
--hash=sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024 \
--hash=sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281 \
--hash=sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a \
--hash=sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169 \
--hash=sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364 \
--hash=sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa \
--hash=sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be \
--hash=sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53 \
--hash=sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759 \
--hash=sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e \
--hash=sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b \
--hash=sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52 \
--hash=sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610 \
--hash=sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05 \
--hash=sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2 \
--hash=sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca \
--hash=sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0 \
--hash=sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293 \
--hash=sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289 \
--hash=sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e \
--hash=sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f \
--hash=sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c \
--hash=sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94 \
--hash=sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad \
--hash=sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46 \
--hash=sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9 \
--hash=sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9 \
--hash=sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee \
--hash=sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9 \
--hash=sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1 \
--hash=sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9 \
--hash=sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799 \
--hash=sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1 \
--hash=sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b \
--hash=sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf \
--hash=sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5 \
--hash=sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2 \
--hash=sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e \
--hash=sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51 \
--hash=sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506 \
--hash=sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73 \
--hash=sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7 \
--hash=sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5 \
--hash=sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57 \
--hash=sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4 \
--hash=sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd \
--hash=sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b \
--hash=sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41 \
--hash=sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe \
--hash=sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59 \
--hash=sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8 \
--hash=sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f \
--hash=sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e \
--hash=sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750 \
--hash=sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1 \
--hash=sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96 \
--hash=sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc \
--hash=sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440 \
--hash=sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe \
--hash=sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38 \
--hash=sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950 \
--hash=sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2 \
--hash=sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd \
--hash=sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce \
--hash=sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66 \
--hash=sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3 \
--hash=sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86
regex==2024.9.11 \
--hash=sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623 \
--hash=sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199 \
--hash=sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664 \
--hash=sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f \
--hash=sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca \
--hash=sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066 \
--hash=sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca \
--hash=sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39 \
--hash=sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d \
--hash=sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6 \
--hash=sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35 \
--hash=sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408 \
--hash=sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5 \
--hash=sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a \
--hash=sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9 \
--hash=sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92 \
--hash=sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766 \
--hash=sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168 \
--hash=sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca \
--hash=sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508 \
--hash=sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df \
--hash=sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf \
--hash=sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b \
--hash=sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4 \
--hash=sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268 \
--hash=sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6 \
--hash=sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c \
--hash=sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62 \
--hash=sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231 \
--hash=sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36 \
--hash=sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba \
--hash=sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4 \
--hash=sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e \
--hash=sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822 \
--hash=sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4 \
--hash=sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d \
--hash=sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71 \
--hash=sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50 \
--hash=sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d \
--hash=sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad \
--hash=sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8 \
--hash=sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8 \
--hash=sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8 \
--hash=sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd \
--hash=sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16 \
--hash=sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664 \
--hash=sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a \
--hash=sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f \
--hash=sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd \
--hash=sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a \
--hash=sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9 \
--hash=sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199 \
--hash=sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d \
--hash=sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963 \
--hash=sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009 \
--hash=sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a \
--hash=sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679 \
--hash=sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96 \
--hash=sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42 \
--hash=sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8 \
--hash=sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e \
--hash=sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7 \
--hash=sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8 \
--hash=sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802 \
--hash=sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366 \
--hash=sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137 \
--hash=sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784 \
--hash=sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29 \
--hash=sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3 \
--hash=sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771 \
--hash=sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60 \
--hash=sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a \
--hash=sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4 \
--hash=sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0 \
--hash=sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84 \
--hash=sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd \
--hash=sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1 \
--hash=sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776 \
--hash=sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142 \
--hash=sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89 \
--hash=sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c \
--hash=sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8 \
--hash=sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35 \
--hash=sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a \
--hash=sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86 \
--hash=sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9 \
--hash=sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64 \
--hash=sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554 \
--hash=sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85 \
--hash=sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb \
--hash=sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0 \
--hash=sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8 \
--hash=sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb \
--hash=sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919
# via mkdocs-material
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
@ -546,6 +571,10 @@ sniffio==1.3.1 \
# via
# anyio
# httpx
super-collections==0.5.3 \
--hash=sha256:907d35b25dc4070910e8254bf2f5c928348af1cf8a1f1e8259e06c666e902cff \
--hash=sha256:94c1ec96c0a0d5e8e7d389ed8cde6882ac246940507c5e6b86e91945c2968d46
# via mkdocs-macros-plugin
termcolor==2.4.0 \
--hash=sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63 \
--hash=sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a
@ -595,7 +624,7 @@ wcmatch==8.5.2 \
--hash=sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478 \
--hash=sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2
# via mkdocs-include-markdown-plugin
zipp==3.20.0 \
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
zipp==3.20.2 \
--hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \
--hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29
# via importlib-metadata

View File

@ -20,13 +20,30 @@ src = ["src/backend/InvenTree"]
"__init__.py" = ["D104"]
[tool.ruff.lint]
select = ["A", "B", "C4", "D", "I", "N", "F"]
select = ["A", "B", "C", "C4", "D", "F", "I", "N", "SIM", "PIE", "PLE", "PLW", "RUF", "UP", "W"]
# Things that should be enabled in the future:
# - LOG
# - DJ # for Django stuff
# - S # for security stuff (bandit)
ignore = [
"PLE1205",
# - PLE1205 - Too many arguments for logging format string
"PLW2901",
# - PLW2901 - Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
"PLW0602","PLW0603","PLW0604", # global variable things
"RUF015",
# - RUF015 - Prefer next({iterable}) over single element slice
"RUF012",
# - RUF012 - Mutable class attributes should be annotated with typing.ClassVar
"SIM117",
# - SIM117 - Use a single with statement with multiple contexts instead of nested with statements
"SIM102",
# - SIM102 - Use a single if statement instead of nested if statements
"SIM105",
# - SIM105 - Use contextlib.suppress({exception}) instead of try-except-pass
"C901",
# - C901 - function is too complex
"N999",
# - N802 - function name should be lowercase
"N802",
@ -36,13 +53,15 @@ ignore = [
"N812",
# - D417 Missing argument descriptions in the docstring
"D417",
# - RUF032 - decimal-from-float-literal
"RUF032",
# TODO These should be followed up and fixed
# - B904 Within an `except` clause, raise exceptions
"B904",
# Remove fast
"A001", "A002","A003","B018"
"A002", "B018"
]
[tool.ruff.lint.pydocstyle]

View File

@ -17,6 +17,6 @@ build:
- echo "Generating API schema file"
- pip install -U invoke
- invoke migrate
- invoke export-settings-definitions --filename docs/inventree_settings.json --overwrite
- invoke schema --filename docs/schema.yml --ignore-warnings
- invoke int.export-settings-definitions --filename docs/inventree_settings.json --overwrite
- invoke dev.schema --filename docs/schema.yml --ignore-warnings
- python docs/extract_schema.py docs/schema.yml

View File

@ -104,14 +104,16 @@ class InvenTreeResource(ModelResource):
attribute = getattr(field, 'attribute', field_name)
# Check if the associated database field is a non-nullable string
if db_field := db_fields.get(attribute):
if (
if (
(db_field := db_fields.get(attribute))
and (
isinstance(db_field, CharField)
and db_field.blank
and not db_field.null
):
if column not in self.CONVERT_NULL_FIELDS:
self.CONVERT_NULL_FIELDS.append(column)
)
and column not in self.CONVERT_NULL_FIELDS
):
self.CONVERT_NULL_FIELDS.append(column)
return super().before_import(dataset, using_transactions, dry_run, **kwargs)

View File

@ -26,7 +26,7 @@ from part.models import Part
from plugin.serializers import MetadataSerializer
from users.models import ApiToken
from .email import is_email_configured
from .helpers_email import is_email_configured
from .mixins import ListAPI, RetrieveUpdateAPI
from .status import check_system_health, is_worker_running
from .version import inventreeApiText
@ -77,7 +77,7 @@ class LicenseView(APIView):
# Ensure we do not have any duplicate 'name' values in the list
for entry in data:
name = None
for key in entry.keys():
for key in entry:
if key.lower() == 'name':
name = entry[key]
break
@ -221,6 +221,7 @@ class InfoView(AjaxView):
'instance': InvenTree.version.inventreeInstanceName(),
'apiVersion': InvenTree.version.inventreeApiVersion(),
'worker_running': is_worker_running(),
'worker_count': settings.BACKGROUND_WORKER_COUNT,
'worker_pending_tasks': self.worker_pending_tasks(),
'plugins_enabled': settings.PLUGINS_ENABLED,
'plugins_install_disabled': settings.PLUGINS_INSTALL_DISABLED,
@ -321,7 +322,6 @@ class BulkDeleteMixin:
Raises:
ValidationError: If the deletion should not proceed
"""
pass
def filter_delete_queryset(self, queryset, request):
"""Provide custom filtering for the queryset *before* it is deleted.
@ -380,11 +380,26 @@ class BulkDeleteMixin:
# Filter by provided item ID values
if items:
queryset = queryset.filter(id__in=items)
try:
queryset = queryset.filter(id__in=items)
except Exception:
raise ValidationError({
'non_field_errors': _('Invalid items list provided')
})
# Filter by provided filters
if filters:
queryset = queryset.filter(**filters)
try:
queryset = queryset.filter(**filters)
except Exception:
raise ValidationError({
'non_field_errors': _('Invalid filters provided')
})
if queryset.count() == 0:
raise ValidationError({
'non_field_errors': _('No items found to delete')
})
# Run a final validation step (should raise an error if the deletion should not proceed)
self.validate_delete(queryset, request)
@ -398,8 +413,6 @@ class BulkDeleteMixin:
class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
"""Custom API endpoint which provides BulkDelete functionality in addition to List and Create."""
...
class APISearchViewSerializer(serializers.Serializer):
"""Serializer for the APISearchView."""

View File

@ -1,12 +1,106 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 248
INVENTREE_API_VERSION = 277
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v277 - 2024-11-01 : https://github.com/inventree/InvenTree/pull/8278
- Allow build order list to be filtered by "outstanding" (alias for "active")
v276 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403
- Adds 'destination' field to the PurchaseOrder model and API endpoints
v275 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
- Adds SKU and MPN fields to the StockItem serializer
- Additional export options for the StockItem serializer
v274 - 2024-10-29 : https://github.com/inventree/InvenTree/pull/8392
- Add more detailed information to NotificationEntry API serializer
v273 - 2024-10-28 : https://github.com/inventree/InvenTree/pull/8376
- Fixes for the BuildLine API endpoint
v272 - 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343
- Adjustments to BuildLine API serializers
v271 - 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
- Fixes for SalesOrderLineItem endpoints
v270 - 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307
- Adds missing date fields from order API endpoint(s)
v269 - 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295
- Adds "include_variants" filter to the BuildOrder API endpoint
- Adds "include_variants" filter to the SalesOrder API endpoint
- Adds "include_variants" filter to the PurchaseOrderLineItem API endpoint
- Adds "include_variants" filter to the ReturnOrder API endpoint
268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
- Adds "in_stock" attribute to the StockItem serializer
267 - 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
- Remove "allocations" field from the SalesOrderShipment API endpoint(s)
- Add "allocated_items" field to the SalesOrderShipment API endpoint(s)
266 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
- Tweak SalesOrderShipment API for more efficient data retrieval
265 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228
- Adds API endpoint for providing custom admin integration details for plugins
264 - 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231
- Adds Sales Order Shipment attachment model type
263 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194
- Adds Sales Order Shipment report
262 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
- Tweak permission requirements for uninstalling plugins via API
261 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
- Fixes for BuildOrder API serializers
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
- Adds facility for server-side context data to be passed to client-side plugins
v259 - 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
- Implements new API endpoint for enabling custom UI features via plugins
v258 - 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
- Enhances the existing PartScheduling API endpoint
- Adds a formal DRF serializer to the endpoint
v257 - 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
- Adds API endpoint for reporting barcode scan history
v256 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
- Adjustments for "stocktake" (stock history) API endpoints
v255 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
- Enables copying line items when duplicating an order
v254 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470
- Implements new API endpoints for enabling custom UI functionality via plugins
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
- Adjustments for user API endpoints
v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
- Add endpoint for listing all known units
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
- Adds "attach_to_model" field to the ReporTemplate model
v250 - 2024-09-04 : https://github.com/inventree/InvenTree/pull/8069
- Fixes 'revision' field definition in Part serializer
v249 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7978
- Sort status enums
v248 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965
- Small adjustments to labels for new custom status fields

View File

@ -40,9 +40,14 @@ class InvenTreeConfig(AppConfig):
- Adding users set in the current environment
"""
# skip loading if plugin registry is not loaded or we run in a background thread
if not InvenTree.ready.isPluginRegistryLoaded():
return
# Skip if not in worker or main thread
if (
not InvenTree.ready.isPluginRegistryLoaded()
or not InvenTree.ready.isInMainThread()
not InvenTree.ready.isInMainThread()
and not InvenTree.ready.isInWorkerThread()
):
return
@ -52,7 +57,6 @@ class InvenTreeConfig(AppConfig):
if InvenTree.ready.canAppAccessDatabase() or settings.TESTING_ENV:
self.remove_obsolete_tasks()
self.collect_tasks()
self.start_background_tasks()
@ -125,7 +129,7 @@ class InvenTreeConfig(AppConfig):
for task in tasks:
ref_name = f'{task.func.__module__}.{task.func.__name__}'
if ref_name in existing_tasks.keys():
if ref_name in existing_tasks:
# This task already exists - update the details if required
existing_task = existing_tasks[ref_name]

View File

@ -131,7 +131,7 @@ def load_config_data(set_cache: bool = False) -> map:
cfg_file = get_config_file()
with open(cfg_file, 'r') as cfg:
with open(cfg_file, encoding='utf-8') as cfg:
data = yaml.safe_load(cfg)
# Set the cache if requested

View File

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
"""Provides extra global data to all templates."""
import InvenTree.email
import InvenTree.helpers_email
import InvenTree.ready
import InvenTree.status
from generic.states.custom import get_custom_classes
@ -27,7 +25,7 @@ def health_status(request):
status = {
'django_q_running': InvenTree.status.is_worker_running(),
'email_configured': InvenTree.email.is_email_configured(),
'email_configured': InvenTree.helpers_email.is_email_configured(),
}
# The following keys are required to denote system health
@ -75,7 +73,7 @@ def user_roles(request):
roles = {}
for role in RuleSet.get_ruleset_models().keys():
for role in RuleSet.get_ruleset_models():
permissions = {}
for perm in ['view', 'add', 'change', 'delete']:

View File

@ -2,6 +2,7 @@
import logging
import re
from typing import Optional
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@ -95,7 +96,7 @@ def from_engineering_notation(value):
"""
value = str(value).strip()
pattern = '(\d+)([a-zA-Z]+)(\d+)(.*)'
pattern = r'(\d+)([a-zA-Z]+)(\d+)(.*)'
if match := re.match(pattern, value):
left, prefix, right, suffix = match.groups()
@ -133,7 +134,7 @@ def convert_value(value, unit):
return value
def convert_physical_value(value: str, unit: str = None, strip_units=True):
def convert_physical_value(value: str, unit: Optional[str] = None, strip_units=True):
"""Validate that the provided value is a valid physical quantity.
Arguments:
@ -203,7 +204,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
if unit:
raise ValidationError(_(f'Could not convert {original} to {unit}'))
else:
raise ValidationError(_('Invalid quantity supplied'))
raise ValidationError(_('Invalid quantity provided'))
# Calculate the "magnitude" of the value, as a float
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues
@ -217,7 +218,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
magnitude = float(ureg.Quantity(magnitude).to_base_units().magnitude)
except Exception as exc:
raise ValidationError(_(f'Invalid quantity supplied ({exc})'))
raise ValidationError(_('Invalid quantity provided') + f': ({exc})')
if strip_units:
return magnitude
@ -245,8 +246,4 @@ def is_dimensionless(value):
if value.units == ureg.dimensionless:
return True
if value.to_base_units().units == ureg.dimensionless:
return True
# At this point, the value is not dimensionless
return False
return value.to_base_units().units == ureg.dimensionless

View File

@ -1,7 +1,6 @@
"""Custom exception handling for the DRF API."""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import sys
@ -41,10 +40,7 @@ def log_error(path, error_name=None, error_info=None, error_data=None):
if kind in settings.IGNORED_ERRORS:
return
if error_name:
kind = error_name
else:
kind = getattr(kind, '__name__', 'Unknown Error')
kind = error_name or getattr(kind, '__name__', 'Unknown Error')
if error_info:
info = error_info

View File

@ -31,10 +31,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
# Find the selected exchange rate plugin
slug = get_global_setting('CURRENCY_UPDATE_PLUGIN', create=False)
if slug:
plugin = registry.get_plugin(slug)
else:
plugin = None
plugin = registry.get_plugin(slug) if slug else None
if not plugin:
# Find the first active currency exchange plugin

View File

@ -93,9 +93,8 @@ class InvenTreeModelMoneyField(ModelMoneyField):
allow_negative = kwargs.pop('allow_negative', False)
# If no validators are provided, add some "standard" ones
if len(validators) == 0:
if not allow_negative:
validators.append(MinMoneyValidator(0))
if len(validators) == 0 and not allow_negative:
validators.append(MinMoneyValidator(0))
kwargs['validators'] = validators
@ -134,9 +133,9 @@ class DatePickerFormField(forms.DateField):
def __init__(self, **kwargs):
"""Set up custom values."""
help_text = kwargs.get('help_text', _('Enter date'))
label = kwargs.get('label', None)
label = kwargs.get('label')
required = kwargs.get('required', False)
initial = kwargs.get('initial', None)
initial = kwargs.get('initial')
widget = forms.DateInput(attrs={'type': 'date'})

View File

@ -118,10 +118,7 @@ class InvenTreeOrderingFilter(filters.OrderingFilter):
field = field[1:]
# Are aliases defined for this field?
if field in aliases:
alias = aliases[field]
else:
alias = field
alias = aliases.get(field, field)
"""
Potentially, a single field could be "aliased" to multiple field,

View File

@ -2,6 +2,7 @@
import re
import string
from typing import Optional
from django.conf import settings
from django.utils import translation
@ -106,10 +107,7 @@ def construct_format_regex(fmt_string: str) -> str:
# Add a named capture group for the format entry
if name:
# Check if integer values are required
if _fmt.endswith('d'):
c = '\d'
else:
c = '.'
c = '\\d' if _fmt.endswith('d') else '.'
# Specify width
# TODO: Introspect required width
@ -160,7 +158,7 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
"""
info = parse_format_string(fmt_string)
if name not in info.keys():
if name not in info:
raise NameError(_(f"Value '{name}' does not appear in pattern format"))
# Construct a regular expression for matching against the provided format string
@ -182,8 +180,8 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
def format_money(
money: Money,
decimal_places: int = None,
format: str = None,
decimal_places: Optional[int] = None,
fmt: Optional[str] = None,
include_symbol: bool = True,
) -> str:
"""Format money object according to the currently set local.
@ -191,7 +189,7 @@ def format_money(
Args:
money (Money): The money object to format
decimal_places (int): Number of decimal places to use
format (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
fmt (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
Returns:
str: The formatted string
@ -199,10 +197,10 @@ def format_money(
Raises:
ValueError: format string is incorrectly specified
"""
language = None and translation.get_language() or settings.LANGUAGE_CODE
language = (None) or settings.LANGUAGE_CODE
locale = Locale.parse(translation.to_locale(language))
if format:
pattern = parse_pattern(format)
if fmt:
pattern = parse_pattern(fmt)
else:
pattern = locale.currency_formats['standard']
if decimal_places is not None:

View File

@ -266,9 +266,8 @@ class RegistratonMixin:
raise forms.ValidationError(
_('The provided primary email address is not valid.')
)
else:
if split_email[1] == option[1:]:
return super().clean_email(email)
elif split_email[1] == option[1:]:
return super().clean_email(email)
logger.info('The provided email domain for %s is not approved', email)
raise forms.ValidationError(_('The provided email domain is not approved.'))

View File

@ -2,6 +2,7 @@
import datetime
import hashlib
import inspect
import io
import logging
import os
@ -9,17 +10,18 @@ import os.path
import re
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import TypeVar, Union
from typing import Optional, TypeVar, Union
from wsgiref.util import FileWrapper
import django.utils.timezone as timezone
from django.conf import settings
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import Storage, default_storage
from django.http import StreamingHttpResponse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
import bleach
import pytz
import regex
from bleach import clean
@ -97,10 +99,7 @@ def generateTestKey(test_name: str) -> str:
if char.isidentifier():
return True
if char.isalnum():
return True
return False
return bool(char.isalnum())
# Remove any characters that cannot be used to represent a variable
key = ''.join([c for c in key if valid_char(c)])
@ -435,7 +434,7 @@ def DownloadFile(
return response
def increment_serial_number(serial):
def increment_serial_number(serial, part=None):
"""Given a serial number, (attempt to) generate the *next* serial number.
Note: This method is exposed to custom plugins.
@ -446,6 +445,7 @@ def increment_serial_number(serial):
Returns:
incremented value, or None if incrementing could not be performed.
"""
from InvenTree.exceptions import log_error
from plugin.registry import registry
# Ensure we start with a string value
@ -454,16 +454,30 @@ def increment_serial_number(serial):
# First, let any plugins attempt to increment the serial number
for plugin in registry.with_mixin('validation'):
result = plugin.increment_serial_number(serial)
if result is not None:
return str(result)
try:
if not hasattr(plugin, 'increment_serial_number'):
continue
signature = inspect.signature(plugin.increment_serial_number)
# Note: 2024-08-21 - The 'part' parameter has been added to the signature
if 'part' in signature.parameters:
result = plugin.increment_serial_number(serial, part=part)
else:
result = plugin.increment_serial_number(serial)
if result is not None:
return str(result)
except Exception:
log_error(f'{plugin.slug}.increment_serial_number')
# If we get to here, no plugins were able to "increment" the provided serial value
# Attempt to perform increment according to some basic rules
return increment(serial)
def extract_serial_numbers(input_string, expected_quantity: int, starting_value=None):
def extract_serial_numbers(
input_string, expected_quantity: int, starting_value=None, part=None
):
"""Extract a list of serial numbers from a provided input string.
The input string can be specified using the following concepts:
@ -483,27 +497,29 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
starting_value: Provide a starting value for the sequence (or None)
"""
if starting_value is None:
starting_value = increment_serial_number(None)
starting_value = increment_serial_number(None, part=part)
try:
expected_quantity = int(expected_quantity)
except ValueError:
raise ValidationError([_('Invalid quantity provided')])
if input_string:
input_string = str(input_string).strip()
else:
input_string = ''
if expected_quantity > 1000:
raise ValidationError({
'quantity': [_('Cannot serialize more than 1000 items at once')]
})
input_string = str(input_string).strip() if input_string else ''
if len(input_string) == 0:
raise ValidationError([_('Empty serial number string')])
next_value = increment_serial_number(starting_value)
next_value = increment_serial_number(starting_value, part=part)
# Substitute ~ character with latest value
while '~' in input_string and next_value:
input_string = input_string.replace('~', str(next_value), 1)
next_value = increment_serial_number(next_value)
next_value = increment_serial_number(next_value, part=part)
# Split input string by whitespace or comma (,) characters
groups = re.split(r'[\s,]+', input_string)
@ -557,7 +573,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
if a == b:
# Invalid group
add_error(_(f'Invalid group range: {group}'))
add_error(_(f'Invalid group: {group}'))
continue
group_items = []
@ -600,7 +616,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
for item in group_items:
add_serial(item)
else:
add_error(_(f'Invalid group range: {group}'))
add_error(_(f'Invalid group: {group}'))
else:
# In the case of a different number of hyphens, simply add the entire group
@ -618,14 +634,14 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
sequence_count = max(0, expected_quantity - len(serials))
if len(items) > 2 or len(items) == 0:
add_error(_(f'Invalid group sequence: {group}'))
add_error(_(f'Invalid group: {group}'))
continue
elif len(items) == 2:
try:
if items[1]:
sequence_count = int(items[1]) + 1
except ValueError:
add_error(_(f'Invalid group sequence: {group}'))
add_error(_(f'Invalid group: {group}'))
continue
value = items[0]
@ -644,7 +660,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
for item in sequence_items:
add_serial(item)
else:
add_error(_(f'Invalid group sequence: {group}'))
add_error(_(f'Invalid group: {group}'))
else:
# At this point, we assume that the 'group' is just a single serial value
@ -800,14 +816,73 @@ def remove_non_printable_characters(
if remove_unicode:
# Remove Unicode control characters
if remove_newline:
cleaned = regex.sub('[^\P{C}]+', '', cleaned)
cleaned = regex.sub(r'[^\P{C}]+', '', cleaned)
else:
# Use 'negative-lookahead' to exclude newline character
cleaned = regex.sub('(?![\x0a])[^\P{C}]+', '', cleaned)
cleaned = regex.sub('(?![\x0a])[^\\P{C}]+', '', cleaned)
return cleaned
def clean_markdown(value: str):
"""Clean a markdown string.
This function will remove javascript and other potentially harmful content from the markdown string.
"""
import markdown
try:
markdownify_settings = settings.MARKDOWNIFY['default']
except (AttributeError, KeyError):
markdownify_settings = {}
extensions = markdownify_settings.get('MARKDOWN_EXTENSIONS', [])
extension_configs = markdownify_settings.get('MARKDOWN_EXTENSION_CONFIGS', {})
# Generate raw HTML from provided markdown (without sanitizing)
# Note: The 'html' output_format is required to generate self closing tags, e.g. <tag> instead of <tag />
html = markdown.markdown(
value or '',
extensions=extensions,
extension_configs=extension_configs,
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
)
whitelist_protocols = markdownify_settings.get(
'WHITELIST_PROTOCOLS', bleach.sanitizer.ALLOWED_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,
)
# Clean the HTML content (for comparison). This must be the same as the original content
clean_html = cleaner.clean(html)
if html != clean_html:
raise ValidationError(_('Data contains prohibited markdown content'))
return value
def hash_barcode(barcode_data):
"""Calculate a 'unique' hash for a barcode string.
@ -827,7 +902,7 @@ def hash_barcode(barcode_data):
def hash_file(filename: Union[str, Path], storage: Union[Storage, None] = None):
"""Return the MD5 hash of a file."""
content = (
open(filename, 'rb').read()
open(filename, 'rb').read() # noqa: SIM115
if storage is None
else storage.open(str(filename), 'rb').read()
)
@ -865,7 +940,7 @@ def server_timezone() -> str:
return settings.TIME_ZONE
def to_local_time(time, target_tz: str = None):
def to_local_time(time, target_tz: Optional[str] = None):
"""Convert the provided time object to the local timezone.
Arguments:
@ -947,7 +1022,14 @@ def get_objectreference(
ret = {}
if url_fnc:
ret['link'] = url_fnc()
return {'name': str(item), 'model': str(model_cls._meta.verbose_name), **ret}
return {
'name': str(item),
'model_name': str(model_cls._meta.verbose_name),
'model_type': str(model_cls._meta.model_name),
'model_id': getattr(item, 'pk', None),
**ret,
}
Inheritors_T = TypeVar('Inheritors_T')

View File

@ -62,7 +62,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
# If we are importing data, don't send emails
return
if not InvenTree.email.is_email_configured() and not settings.TESTING:
if not is_email_configured() and not settings.TESTING:
# Email is not configured / enabled
return
@ -86,4 +86,5 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
recipients,
fail_silently=False,
html_message=html_message,
group='notification',
)

View File

@ -114,10 +114,7 @@ def download_image_from_url(remote_url, timeout=2.5):
# Add user specified user-agent to request (if specified)
user_agent = get_global_setting('INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT')
if user_agent:
headers = {'User-Agent': user_agent}
else:
headers = None
headers = {'User-Agent': user_agent} if user_agent else None
try:
response = requests.get(
@ -130,7 +127,7 @@ def download_image_from_url(remote_url, timeout=2.5):
# Throw an error if anything goes wrong
response.raise_for_status()
except requests.exceptions.ConnectionError as exc:
raise Exception(_('Connection error') + f': {str(exc)}')
raise Exception(_('Connection error') + f': {exc!s}')
except requests.exceptions.Timeout as exc:
raise exc
except requests.exceptions.HTTPError:
@ -138,7 +135,7 @@ def download_image_from_url(remote_url, timeout=2.5):
_('Server responded with invalid status code') + f': {response.status_code}'
)
except Exception as exc:
raise Exception(_('Exception occurred') + f': {str(exc)}')
raise Exception(_('Exception occurred') + f': {exc!s}')
if response.status_code != 200:
raise Exception(
@ -213,9 +210,6 @@ def render_currency(
except Exception:
pass
if decimal_places is None:
decimal_places = get_global_setting('PRICING_DECIMAL_PLACES', 6)
if min_decimal_places is None:
min_decimal_places = get_global_setting('PRICING_DECIMAL_PLACES_MIN', 0)
@ -225,17 +219,19 @@ def render_currency(
value = Decimal(str(money.amount)).normalize()
value = str(value)
if '.' in value:
decimals = len(value.split('.')[-1])
decimals = max(decimals, min_decimal_places)
decimals = min(decimals, decimal_places)
decimal_places = decimals
if decimal_places is not None:
# Decimal place count is provided, use it
pass
elif '.' in value:
# If the value has a decimal point, use the number of decimal places in the value
decimal_places = len(value.split('.')[-1])
else:
decimal_places = max(decimal_places, 2)
# No decimal point, use 2 as a default
decimal_places = 2
decimal_places = max(decimal_places, max_decimal_places)
# Clip the decimal places to the specified range
decimal_places = max(decimal_places, min_decimal_places)
decimal_places = min(decimal_places, max_decimal_places)
return format_money(
money, decimal_places=decimal_places, include_symbol=include_symbol

View File

@ -10,6 +10,8 @@ Additionally, update the following files with the new locale code:
- /src/frontend/.linguirc file
- /src/frontend/src/contexts/LanguageContext.tsx
(and then run "invoke int.frontend-trans")
"""
from django.utils.translation import gettext_lazy as _
@ -34,6 +36,7 @@ LOCALES = [
('it', _('Italian')),
('ja', _('Japanese')),
('ko', _('Korean')),
('lt', _('Lithuanian')),
('lv', _('Latvian')),
('nl', _('Dutch')),
('no', _('Norwegian')),

View File

@ -25,7 +25,7 @@ def send_simple_login_email(user, link):
)
send_mail(
_(f'[{site_name}] Log in to the app'),
f'[{site_name}] ' + _('Log in to the app'),
email_plaintext_message,
settings.DEFAULT_FROM_EMAIL,
[user.email],

View File

@ -8,6 +8,7 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
"""Run the management command."""
from plugin.staticfiles import collect_plugins_static_files
import plugin.staticfiles
collect_plugins_static_files()
plugin.staticfiles.collect_plugins_static_files()
plugin.staticfiles.clear_plugins_static_files()

View File

@ -47,7 +47,7 @@ class Command(BaseCommand):
filename = kwargs.get('filename', 'inventree_settings.json')
with open(filename, 'w') as f:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=4)
print(f"Exported InvenTree settings definitions to '{filename}'")

View File

@ -103,14 +103,14 @@ class Command(BaseCommand):
})
self.stdout.write(f'Writing icon map for {len(icons.keys())} icons')
with open(kwargs['output_file'], 'w') as f:
with open(kwargs['output_file'], 'w', encoding='utf-8') as f:
json.dump(icons, f, indent=2)
self.stdout.write(f'Icon map written to {kwargs["output_file"]}')
# Import icon map file
if kwargs['input_file']:
with open(kwargs['input_file'], 'r') as f:
with open(kwargs['input_file'], encoding='utf-8') as f:
icons = json.load(f)
self.stdout.write(f'Loaded icon map for {len(icons.keys())} icons')

View File

@ -19,10 +19,11 @@ def render_file(file_name, source, target, locales, ctx):
target_file = os.path.join(target, locale + '.' + file_name)
with open(target_file, 'w') as localised_file:
with lang_over(locale):
rendered = render_to_string(os.path.join(source, file_name), ctx)
localised_file.write(rendered)
with open(target_file, 'w', encoding='utf-8') as localised_file, lang_over(
locale
):
rendered = render_to_string(os.path.join(source, file_name), ctx)
localised_file.write(rendered)
class Command(BaseCommand):

View File

@ -35,7 +35,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(os.path.exists(path) for path in img_paths):
# All images exist - skip further work
return

View File

@ -35,4 +35,4 @@ class Command(BaseCommand):
mfa_user[0].staticdevice_set.all().delete()
# TOTP tokens
mfa_user[0].totpdevice_set.all().delete()
print(f'Removed all MFA methods for user {str(mfa_user[0])}')
print(f'Removed all MFA methods for user {mfa_user[0]!s}')

View File

@ -2,9 +2,13 @@
import logging
from rest_framework import serializers
from django.core.exceptions import PermissionDenied
from django.http import Http404
from rest_framework import exceptions, serializers
from rest_framework.fields import empty
from rest_framework.metadata import SimpleMetadata
from rest_framework.request import clone_request
from rest_framework.utils import model_meta
import common.models
@ -29,6 +33,40 @@ class InvenTreeMetadata(SimpleMetadata):
so we can perform lookup for ForeignKey related fields.
"""
def determine_actions(self, request, view):
"""Determine the 'actions' available to the user for the given view.
Note that this differs from the standard DRF implementation,
in that we also allow annotation for the 'GET' method.
This allows the client to determine what fields are available,
even if they are only for a read (GET) operation.
See SimpleMetadata.determine_actions for more information.
"""
actions = {}
for method in {'PUT', 'POST', 'GET'} & set(view.allowed_methods):
view.request = clone_request(request, method)
try:
# Test global permissions
if hasattr(view, 'check_permissions'):
view.check_permissions(view.request)
# Test object permissions
if method == 'PUT' and hasattr(view, 'get_object'):
view.get_object()
except (exceptions.APIException, PermissionDenied, Http404):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = view.get_serializer()
actions[method] = self.get_serializer_info(serializer)
finally:
view.request = request
return actions
def determine_metadata(self, request, view):
"""Overwrite the metadata to adapt to the request user."""
self.request = request
@ -81,6 +119,7 @@ class InvenTreeMetadata(SimpleMetadata):
# Map the request method to a permission type
rolemap = {
'GET': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
@ -102,10 +141,6 @@ class InvenTreeMetadata(SimpleMetadata):
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
actions['DELETE'] = {}
# Add a 'VIEW' action if we are allowed to view
if 'GET' in view.allowed_methods and check(user, table, 'view'):
actions['GET'] = {}
metadata['actions'] = actions
except AttributeError:
@ -204,7 +239,7 @@ class InvenTreeMetadata(SimpleMetadata):
# Iterate through simple fields
for name, field in model_fields.fields.items():
if name in serializer_info.keys():
if name in serializer_info:
if name in read_only_fields:
serializer_info[name]['read_only'] = True
@ -236,7 +271,7 @@ class InvenTreeMetadata(SimpleMetadata):
# Iterate through relations
for name, relation in model_fields.relations.items():
if name not in serializer_info.keys():
if name not in serializer_info:
# Skip relation not defined in serializer
continue
@ -307,12 +342,12 @@ class InvenTreeMetadata(SimpleMetadata):
instance_filters = instance.api_instance_filters()
for field_name, field_filters in instance_filters.items():
if field_name not in serializer_info.keys():
if field_name not in serializer_info:
# The field might be missing, but is added later on
# This function seems to get called multiple times?
continue
if 'instance_filters' not in serializer_info[field_name].keys():
if 'instance_filters' not in serializer_info[field_name]:
serializer_info[field_name]['instance_filters'] = {}
for key, value in field_filters.items():

View File

@ -36,7 +36,7 @@ def get_token_from_request(request):
return None
class AuthRequiredMiddleware(object):
class AuthRequiredMiddleware:
"""Check for user to be authenticated."""
def __init__(self, get_response):
@ -92,23 +92,18 @@ class AuthRequiredMiddleware(object):
# Allow static files to be accessed without auth
# Important for e.g. login page
if request.path_info.startswith('/static/'):
authorized = True
# Unauthorized users can access the login page
elif request.path_info.startswith('/accounts/'):
authorized = True
elif (
request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/')
or request.path_info.startswith('/assets/')
or request.path_info == f'/{settings.FRONTEND_URL_BASE}'
if (
request.path_info.startswith('/static/')
or request.path_info.startswith('/accounts/')
or (
request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/')
or request.path_info.startswith('/assets/')
or request.path_info == f'/{settings.FRONTEND_URL_BASE}'
)
or self.check_token(request)
):
authorized = True
elif self.check_token(request):
authorized = True
# No authorization was found for the request
if not authorized:
path = request.path_info

View File

@ -6,7 +6,11 @@ from rest_framework import generics, mixins, status
from rest_framework.response import Response
from InvenTree.fields import InvenTreeNotesField
from InvenTree.helpers import remove_non_printable_characters, strip_html_tags
from InvenTree.helpers import (
clean_markdown,
remove_non_printable_characters,
strip_html_tags,
)
class CleanMixin:
@ -57,6 +61,7 @@ class CleanMixin:
# By default, newline characters are removed
remove_newline = True
is_markdown = False
try:
if hasattr(self, 'serializer_class'):
@ -64,11 +69,12 @@ class CleanMixin:
field = model._meta.get_field(field)
# The following field types allow newline characters
allow_newline = [InvenTreeNotesField]
allow_newline = [(InvenTreeNotesField, True)]
for field_type in allow_newline:
if issubclass(type(field), field_type):
if issubclass(type(field), field_type[0]):
remove_newline = False
is_markdown = field_type[1]
break
except AttributeError:
@ -80,6 +86,9 @@ class CleanMixin:
cleaned, remove_newline=remove_newline
)
if is_markdown:
cleaned = clean_markdown(cleaned)
return cleaned
def clean_data(self, data: dict) -> dict:
@ -128,14 +137,10 @@ class CreateAPI(CleanMixin, generics.CreateAPIView):
class RetrieveAPI(generics.RetrieveAPIView):
"""View for retrieve API."""
pass
class RetrieveUpdateAPI(CleanMixin, generics.RetrieveUpdateAPIView):
"""View for retrieve and update API."""
pass
class CustomDestroyModelMixin:
"""This mixin was created pass the kwargs from the API to the models."""
@ -184,5 +189,9 @@ class RetrieveUpdateDestroyAPI(CleanMixin, generics.RetrieveUpdateDestroyAPIView
"""View for retrieve, update and destroy API."""
class RetrieveDestroyAPI(generics.RetrieveDestroyAPIView):
"""View for retrieve and destroy API."""
class UpdateAPI(CleanMixin, generics.UpdateAPIView):
"""View for update API."""

View File

@ -10,8 +10,10 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _
from django_q.models import Task
from error_report.models import Error
from mptt.exceptions import InvalidMove
from mptt.models import MPTTModel, TreeForeignKey
@ -390,10 +392,7 @@ class ReferenceIndexingMixin(models.Model):
except Exception:
# If anything goes wrong, return the most recent reference
recent = cls.get_most_recent_item()
if recent:
reference = recent.reference
else:
reference = ''
reference = recent.reference if recent else ''
return reference
@ -410,14 +409,14 @@ class ReferenceIndexingMixin(models.Model):
})
# Check that only 'allowed' keys are provided
for key in info.keys():
if key not in ctx.keys():
for key in info:
if key not in ctx:
raise ValidationError({
'value': _('Unknown format key specified') + f": '{key}'"
})
# Check that the 'ref' variable is specified
if 'ref' not in info.keys():
if 'ref' not in info:
raise ValidationError({
'value': _('Missing required format key') + ": 'ref'"
})
@ -442,7 +441,7 @@ class ReferenceIndexingMixin(models.Model):
)
# Check that the reference field can be rebuild
cls.rebuild_reference_field(value, validate=True)
return cls.rebuild_reference_field(value, validate=True)
@classmethod
def rebuild_reference_field(cls, reference, validate=False):
@ -859,7 +858,7 @@ class InvenTreeTree(MetadataMixin, PluginValidationMixin, MPTTModel):
Returns:
List of category names from the top level to this category
"""
return self.parentpath + [self]
return [*self.parentpath, self]
def get_path(self):
"""Return a list of element in the item tree.
@ -1054,6 +1053,71 @@ class InvenTreeBarcodeMixin(models.Model):
self.save()
def notify_staff_users_of_error(instance, label: str, context: dict):
"""Helper function to notify staff users of an error."""
import common.models
import common.notifications
try:
# Get all staff users
staff_users = get_user_model().objects.filter(is_staff=True)
target_users = []
# Send a notification to each staff user (unless they have disabled error notifications)
for user in staff_users:
if common.models.InvenTreeUserSetting.get_setting(
'NOTIFICATION_ERROR_REPORT', True, user=user
):
target_users.append(user)
if len(target_users) > 0:
common.notifications.trigger_notification(
instance,
label,
context=context,
targets=target_users,
delivery_methods={common.notifications.UIMessageNotification},
)
except Exception as exc:
# We do not want to throw an exception while reporting an exception!
logger.error(exc)
@receiver(post_save, sender=Task, dispatch_uid='failure_post_save_notification')
def after_failed_task(sender, instance: Task, created: bool, **kwargs):
"""Callback when a new task failure log is generated."""
from django.conf import settings
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 = ''
notify_staff_users_of_error(
instance,
'inventree.task_failure',
{
'failure': instance,
'name': _('Task Failure'),
'message': _(
f"Background worker task '{instance.func}' failed after {n} attempts"
),
'link': url,
},
)
@receiver(post_save, sender=Error, dispatch_uid='error_post_save_notification')
def after_error_logged(sender, instance: Error, created: bool, **kwargs):
"""Callback when a server error is logged.
@ -1062,41 +1126,21 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
"""
if created:
try:
import common.models
import common.notifications
users = get_user_model().objects.filter(is_staff=True)
link = InvenTree.helpers_model.construct_absolute_url(
url = InvenTree.helpers_model.construct_absolute_url(
reverse(
'admin:error_report_error_change', kwargs={'object_id': instance.pk}
)
)
except NoReverseMatch:
url = ''
context = {
notify_staff_users_of_error(
instance,
'inventree.error_log',
{
'error': instance,
'name': _('Server Error'),
'message': _('An error has been logged by the server.'),
'link': link,
}
target_users = []
for user in users:
if common.models.InvenTreeUserSetting.get_setting(
'NOTIFICATION_ERROR_REPORT', True, user=user
):
target_users.append(user)
if len(target_users) > 0:
common.notifications.trigger_notification(
instance,
'inventree.error_log',
context=context,
targets=target_users,
delivery_methods={common.notifications.UIMessageNotification},
)
except Exception as exc:
"""We do not want to throw an exception while reporting an exception"""
logger.error(exc) # noqa: LOG005
'link': url,
},
)

View File

@ -79,6 +79,9 @@ class RolePermission(permissions.BasePermission):
# Extract the model name associated with this request
model = get_model_for_view(view)
if model is None:
return True
app_label = model._meta.app_label
model_name = model._meta.model_name
@ -99,14 +102,24 @@ class IsSuperuser(permissions.IsAdminUser):
return bool(request.user and request.user.is_superuser)
class IsSuperuserOrReadOnly(permissions.IsAdminUser):
"""Allow read-only access to any user, but write access is restricted to superuser users."""
def has_permission(self, request, view):
"""Check if the user is a superuser."""
return bool(
(request.user and request.user.is_superuser)
or request.method in permissions.SAFE_METHODS
)
class IsStaffOrReadOnly(permissions.IsAdminUser):
"""Allows read-only access to any user, but write access is restricted to staff users."""
def has_permission(self, request, view):
"""Check if the user is a superuser."""
return bool(
request.user
and request.user.is_staff
(request.user and request.user.is_staff)
or request.method in permissions.SAFE_METHODS
)

View File

@ -11,43 +11,37 @@ def isInTestMode():
def isImportingData():
"""Returns True if the database is currently importing (or exporting) data, e.g. 'loaddata' command is performed."""
return any((x in sys.argv for x in ['flush', 'loaddata', 'dumpdata']))
return any(x in sys.argv for x in ['flush', 'loaddata', 'dumpdata'])
def isRunningMigrations():
"""Return True if the database is currently running migrations."""
return any(
(
x in sys.argv
for x in ['migrate', 'makemigrations', 'showmigrations', 'runmigrations']
)
x in sys.argv
for x in ['migrate', 'makemigrations', 'showmigrations', 'runmigrations']
)
def isRebuildingData():
"""Return true if any of the rebuilding commands are being executed."""
return any(
(
x in sys.argv
for x in ['prerender', 'rebuild_models', 'rebuild_thumbnails', 'rebuild']
)
x in sys.argv
for x in ['prerender', 'rebuild_models', 'rebuild_thumbnails', 'rebuild']
)
def isRunningBackup():
"""Return true if any of the backup commands are being executed."""
return any(
(
x in sys.argv
for x in [
'backup',
'restore',
'dbbackup',
'dbresotore',
'mediabackup',
'mediarestore',
]
)
x in sys.argv
for x in [
'backup',
'restore',
'dbbackup',
'dbresotore',
'mediabackup',
'mediarestore',
]
)
@ -64,10 +58,7 @@ def isInServerThread():
if 'runserver' in sys.argv:
return True
if 'gunicorn' in sys.argv[0]:
return True
return False
return 'gunicorn' in sys.argv[0]
def isInMainThread():
@ -128,11 +119,7 @@ def canAppAccessDatabase(
if not allow_plugins:
excluded_commands.extend(['collectplugins'])
for cmd in excluded_commands:
if cmd in sys.argv:
return False
return True
return all(cmd not in sys.argv for cmd in excluded_commands)
def isPluginRegistryLoaded():

View File

@ -8,6 +8,7 @@ from django.http import Http404
import rest_framework.exceptions
import sentry_sdk
from djmoney.contrib.exchange.exceptions import MissingRate
from sentry_sdk.integrations.django import DjangoIntegration
import InvenTree.version
@ -27,6 +28,7 @@ def sentry_ignore_errors():
"""
return [
Http404,
MissingRate,
ValidationError,
rest_framework.exceptions.AuthenticationFailed,
rest_framework.exceptions.NotAuthenticated,

View File

@ -89,7 +89,7 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
)
if allow_blank:
choices = [('', '---------')] + choices
choices = [('', '---------'), *choices]
kwargs['choices'] = choices
@ -379,7 +379,7 @@ class InvenTreeTaggitSerializer(TaggitSerializer):
tag_object = super().update(instance, validated_data)
for key in to_be_tagged.keys():
for key in to_be_tagged:
# re-add the tagmanager
new_tagobject = tag_object.__class__.objects.get(id=tag_object.id)
setattr(tag_object, key, getattr(new_tagobject, key))
@ -390,8 +390,6 @@ class InvenTreeTaggitSerializer(TaggitSerializer):
class InvenTreeTagModelSerializer(InvenTreeTaggitSerializer, InvenTreeModelSerializer):
"""Combination of InvenTreeTaggitSerializer and InvenTreeModelSerializer."""
pass
class UserSerializer(InvenTreeModelSerializer):
"""Serializer for a User."""
@ -405,18 +403,21 @@ class UserSerializer(InvenTreeModelSerializer):
read_only_fields = ['username', 'email']
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
first_name = serializers.CharField(
label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
)
last_name = serializers.CharField(
label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
)
email = serializers.EmailField(
label=_('Email'), help_text=_('Email address of the user'), allow_blank=True
)
class ExendedUserSerializer(UserSerializer):
class ExtendedUserSerializer(UserSerializer):
"""Serializer for a User with a bit more info."""
from users.serializers import GroupSerializer
@ -426,21 +427,24 @@ class ExendedUserSerializer(UserSerializer):
class Meta(UserSerializer.Meta):
"""Metaclass defines serializer fields."""
fields = UserSerializer.Meta.fields + [
fields = [
*UserSerializer.Meta.fields,
'groups',
'is_staff',
'is_superuser',
'is_active',
]
read_only_fields = UserSerializer.Meta.read_only_fields + ['groups']
read_only_fields = [*UserSerializer.Meta.read_only_fields, 'groups']
is_staff = serializers.BooleanField(
label=_('Staff'), help_text=_('Does this user have staff permissions')
)
is_superuser = serializers.BooleanField(
label=_('Superuser'), help_text=_('Is this user a superuser')
)
is_active = serializers.BooleanField(
label=_('Active'), help_text=_('Is this user account active')
)
@ -465,9 +469,33 @@ class ExendedUserSerializer(UserSerializer):
return super().validate(attrs)
class UserCreateSerializer(ExendedUserSerializer):
class MeUserSerializer(ExtendedUserSerializer):
"""API serializer specifically for the 'me' endpoint."""
class Meta(ExtendedUserSerializer.Meta):
"""Metaclass options.
Extends the ExtendedUserSerializer.Meta options,
but ensures that certain fields are read-only.
"""
read_only_fields = [
*ExtendedUserSerializer.Meta.read_only_fields,
'is_active',
'is_staff',
'is_superuser',
]
class UserCreateSerializer(ExtendedUserSerializer):
"""Serializer for creating a new User."""
class Meta(ExtendedUserSerializer.Meta):
"""Metaclass options for the UserCreateSerializer."""
# Prevent creation of users with superuser or staff permissions
read_only_fields = ['groups', 'is_staff', 'is_superuser']
def validate(self, attrs):
"""Expanded valiadation for auth."""
# Check that the user trying to create a new user is a superuser
@ -482,6 +510,7 @@ class UserCreateSerializer(ExendedUserSerializer):
def create(self, validated_data):
"""Send an e email to the user after creation."""
from InvenTree.helpers_model import get_base_url
from InvenTree.tasks import email_user, offload_task
base_url = get_base_url()
@ -499,8 +528,12 @@ class UserCreateSerializer(ExendedUserSerializer):
if base_url:
message += f'\n\nURL: {base_url}'
subject = _('Welcome to InvenTree')
# Send the user an onboarding email (from current site)
instance.email_user(subject=_('Welcome to InvenTree'), message=message)
offload_task(
email_user, instance.pk, str(subject), str(message), force_async=True
)
return instance
@ -596,7 +629,7 @@ class DataFileUploadSerializer(serializers.Serializer):
accepted_file_types = ['xls', 'xlsx', 'csv', 'tsv', 'xml']
if ext not in accepted_file_types:
raise serializers.ValidationError(_('Unsupported file type'))
raise serializers.ValidationError(_('Unsupported file format'))
# Impose a 50MB limit on uploaded BOM files
max_upload_file_size = 50 * 1024 * 1024
@ -704,7 +737,6 @@ class DataFileUploadSerializer(serializers.Serializer):
def save(self):
"""Empty overwrite for save."""
...
class DataFileExtractSerializer(serializers.Serializer):
@ -806,11 +838,10 @@ class DataFileExtractSerializer(serializers.Serializer):
required = field.get('required', False)
# Check for missing required columns
if required:
if name not in self.columns:
raise serializers.ValidationError(
_(f"Missing required column: '{name}'")
)
if required and name not in self.columns:
raise serializers.ValidationError(
_(f"Missing required column: '{name}'")
)
for col in self.columns:
if not col:
@ -824,7 +855,6 @@ class DataFileExtractSerializer(serializers.Serializer):
def save(self):
"""No "save" action for this serializer."""
pass
class NotesFieldMixin:
@ -882,8 +912,8 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
try:
self.remote_image_file = download_image_from_url(url)
except Exception as exc:
except Exception:
self.remote_image_file = None
raise ValidationError(str(exc))
raise ValidationError(_('Failed to download image from remote URL'))
return url

View File

@ -32,7 +32,8 @@ from . import config, locales
checkMinPythonVersion()
INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom'
INVENTREE_BASE_URL = 'https://inventree.org'
INVENTREE_NEWS_URL = f'{INVENTREE_BASE_URL}/news/feed.atom'
# Determine if we are running in "test" mode e.g. "manage.py test"
TESTING = 'test' in sys.argv or 'TESTING' in os.environ
@ -133,6 +134,34 @@ STATIC_URL = '/static/'
# Web URL endpoint for served media files
MEDIA_URL = '/media/'
# Are plugins enabled?
PLUGINS_ENABLED = get_boolean_setting(
'INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False
)
PLUGINS_INSTALL_DISABLED = get_boolean_setting(
'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
)
PLUGIN_FILE = config.get_plugin_file()
# Plugin test settings
PLUGIN_TESTING = get_setting(
'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING
) # Are plugins being tested?
PLUGIN_TESTING_SETUP = get_setting(
'INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False
) # Load plugins from setup hooks in testing?
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
PLUGIN_RETRY = get_setting(
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
) # How often should plugin loading be tried?
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
STATICFILES_DIRS = []
# Translated Template settings
@ -153,6 +182,12 @@ if DEBUG and 'collectstatic' not in sys.argv:
if web_dir.exists():
STATICFILES_DIRS.append(web_dir)
# Append directory for sample plugin static content (if in debug mode)
if PLUGINS_ENABLED:
print('Adding plugin sample static content')
STATICFILES_DIRS.append(BASE_DIR.joinpath('plugin', 'samples', 'static'))
print('-', STATICFILES_DIRS[-1])
STATFILES_I18_PROCESSORS = ['InvenTree.context.status_codes']
# Color Themes Directory
@ -169,10 +204,11 @@ DBBACKUP_STORAGE = get_setting(
# Default backup configuration
DBBACKUP_STORAGE_OPTIONS = get_setting(
'INVENTREE_BACKUP_OPTIONS', 'backup_options', None
'INVENTREE_BACKUP_OPTIONS',
'backup_options',
default_value={'location': config.get_backup_dir()},
typecast=dict,
)
if DBBACKUP_STORAGE_OPTIONS is None:
DBBACKUP_STORAGE_OPTIONS = {'location': config.get_backup_dir()}
INVENTREE_ADMIN_ENABLED = get_boolean_setting(
'INVENTREE_ADMIN_ENABLED', config_key='admin_enabled', default_value=True
@ -281,7 +317,7 @@ QUERYCOUNT = {
'MIN_TIME_TO_LOG': 0.1,
'MIN_QUERY_COUNT_TO_LOG': 25,
},
'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'],
'IGNORE_REQUEST_PATTERNS': [r'^(?!\/(api)?(plugin)?\/).*'],
'IGNORE_SQL_PATTERNS': [],
'DISPLAY_DUPLICATES': 1,
'RESPONSE_HEADER': 'X-Django-Query-Count',
@ -298,7 +334,7 @@ if (
and INVENTREE_ADMIN_ENABLED
and not TESTING
and get_boolean_setting('INVENTREE_DEBUG_SHELL', 'debug_shell', False)
): # noqa
):
try:
import django_admin_shell # noqa: F401
@ -555,6 +591,9 @@ for key in db_keys:
# Check that required database configuration options are specified
required_keys = ['ENGINE', 'NAME']
# Ensure all database keys are upper case
db_config = {key.upper(): value for key, value in db_config.items()}
for key in required_keys:
if key not in db_config: # pragma: no cover
error_msg = f'Missing required database configuration value {key}'
@ -812,13 +851,20 @@ _q_worker_timeout = int(
get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90)
)
# Prevent running multiple background workers if global cache is disabled
# This is to prevent scheduling conflicts due to the lack of a shared cache
BACKGROUND_WORKER_COUNT = (
int(get_setting('INVENTREE_BACKGROUND_WORKERS', 'background.workers', 4))
if GLOBAL_CACHE_ENABLED
else 1
)
# django-q background worker configuration
Q_CLUSTER = {
'name': 'InvenTree',
'label': 'Background Tasks',
'workers': int(
get_setting('INVENTREE_BACKGROUND_WORKERS', 'background.workers', 4)
),
'workers': BACKGROUND_WORKER_COUNT,
'timeout': _q_worker_timeout,
'retry': max(120, _q_worker_timeout + 30),
'max_attempts': int(
@ -949,10 +995,7 @@ USE_I18N = True
# Do not use native timezone support in "test" mode
# It generates a *lot* of cruft in the logs
if not TESTING:
USE_TZ = True # pragma: no cover
else:
USE_TZ = False
USE_TZ = bool(not TESTING)
DATE_INPUT_FORMATS = ['%Y-%m-%d']
@ -1058,26 +1101,40 @@ if (
sys.exit(-1)
COOKIE_MODE = (
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'None'))
str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'False'))
.lower()
.strip()
)
valid_cookie_modes = {'lax': 'Lax', 'strict': 'Strict', 'none': None, 'null': None}
# Valid modes (as per the django settings documentation)
valid_cookie_modes = ['lax', 'strict', 'none']
if COOKIE_MODE not in valid_cookie_modes.keys():
logger.error('Invalid cookie samesite mode: %s', COOKIE_MODE)
sys.exit(-1)
COOKIE_MODE = valid_cookie_modes[COOKIE_MODE.lower()]
if not DEBUG and not TESTING and COOKIE_MODE in valid_cookie_modes:
# Set the cookie mode (in production mode only)
COOKIE_MODE = COOKIE_MODE.capitalize()
else:
# Default to False, as per the Django settings
COOKIE_MODE = False
# Additional CSRF settings
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_SAMESITE = COOKIE_MODE
SESSION_COOKIE_SAMESITE = COOKIE_MODE
SESSION_COOKIE_SECURE = get_boolean_setting(
'INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', False
"""Set the SESSION_COOKIE_SECURE value based on the following rules:
- False if the server is running in DEBUG mode
- True if samesite cookie setting is set to 'None'
- Otherwise, use the value specified in the configuration file (or env var)
"""
SESSION_COOKIE_SECURE = (
False
if DEBUG
else (
SESSION_COOKIE_SAMESITE == 'None'
or get_boolean_setting('INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', True)
)
)
USE_X_FORWARDED_HOST = get_boolean_setting(
@ -1229,23 +1286,29 @@ MARKDOWNIFY = {
'abbr',
'b',
'blockquote',
'code',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'hr',
'i',
'img',
'li',
'ol',
'p',
'pre',
's',
'strong',
'ul',
'table',
'thead',
'tbody',
'th',
'tr',
'td',
'ul',
],
}
}
@ -1257,32 +1320,12 @@ IGNORED_ERRORS = [Http404, django.core.exceptions.PermissionDenied]
MAINTENANCE_MODE_RETRY_AFTER = 10
MAINTENANCE_MODE_STATE_BACKEND = 'InvenTree.backends.InvenTreeMaintenanceModeBackend'
# Are plugins enabled?
PLUGINS_ENABLED = get_boolean_setting(
'INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False
)
PLUGINS_INSTALL_DISABLED = get_boolean_setting(
'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
)
PLUGIN_FILE = config.get_plugin_file()
# Plugin test settings
PLUGIN_TESTING = get_setting(
'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING
) # Are plugins being tested?
PLUGIN_TESTING_SETUP = get_setting(
'INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False
) # Load plugins from setup hooks in testing?
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
PLUGIN_RETRY = get_setting(
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
) # How often should plugin loading be tried?
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
# Flag to allow table events during testing
TESTING_TABLE_EVENTS = False
# Flag to allow pricing recalculations during testing
TESTING_PRICING = False
# User interface customization values
CUSTOM_LOGO = get_custom_file(
'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True

View File

@ -94,20 +94,19 @@ for name, provider in providers.registry.provider_map.items():
urls = []
if len(adapters) == 1:
urls = handle_oauth2(adapter=adapters[0])
elif provider.id in legacy:
logger.warning(
'`%s` is not supported on platform UI. Use `%s` instead.',
provider.id,
legacy[provider.id],
)
continue
else:
if provider.id in legacy:
logger.warning(
'`%s` is not supported on platform UI. Use `%s` instead.',
provider.id,
legacy[provider.id],
)
continue
else:
logger.error(
'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.',
provider.id,
)
continue
logger.error(
'Found handler that is not yet ready for platform UI: `%s`. Open an feature request on GitHub if you need it implemented.',
provider.id,
)
continue
provider_urlpatterns += [path(f'{provider.id}/', include(urls))]

File diff suppressed because one or more lines are too long

View File

@ -1,16 +1,14 @@
"""Provides system status functionality checks."""
# -*- coding: utf-8 -*-
import logging
from datetime import timedelta
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_q.models import Success
from django_q.status import Stat
import InvenTree.email
import InvenTree.helpers_email
import InvenTree.ready
logger = logging.getLogger('inventree')
@ -63,13 +61,13 @@ def check_system_health(**kwargs):
if not is_worker_running(**kwargs): # pragma: no cover
result = False
logger.warning(_('Background worker check failed'))
logger.warning('Background worker check failed')
if not InvenTree.email.is_email_configured(): # pragma: no cover
if not InvenTree.helpers_email.is_email_configured(): # pragma: no cover
result = False
logger.warning(_('Email backend not configured'))
logger.warning('Email backend not configured')
if not result: # pragma: no cover
logger.warning(_('InvenTree system health checks failed'))
logger.warning('InvenTree system health checks failed')
return result

View File

@ -9,9 +9,10 @@ import time
import warnings
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Callable
from typing import Callable, Optional
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import AppRegistryNotReady
from django.core.management import call_command
from django.db import DEFAULT_DB_ALIAS, connections
@ -174,6 +175,9 @@ def offload_task(
"""
from InvenTree.exceptions import log_error
# Extract group information from kwargs
group = kwargs.pop('group', 'inventree')
try:
import importlib
@ -200,13 +204,13 @@ def offload_task(
if force_async or (is_worker_running() and not force_sync):
# Running as asynchronous task
try:
task = AsyncTask(taskname, *args, **kwargs)
task = AsyncTask(taskname, *args, group=group, **kwargs)
task.run()
except ImportError:
raise_warning(f"WARNING: '{taskname}' not offloaded - Function not found")
return False
except Exception as exc:
raise_warning(f"WARNING: '{taskname}' not offloaded due to {str(exc)}")
raise_warning(f"WARNING: '{taskname}' not offloaded due to {exc!s}")
log_error('InvenTree.offload_task')
return False
else:
@ -256,7 +260,7 @@ def offload_task(
_func(*args, **kwargs)
except Exception as exc:
log_error('InvenTree.offload_task')
raise_warning(f"WARNING: '{taskname}' failed due to {str(exc)}")
raise_warning(f"WARNING: '{taskname}' failed due to {exc!s}")
raise exc
# Finally, task either completed successfully or was offloaded
@ -291,7 +295,7 @@ class TaskRegister:
task_list: list[ScheduledTask] = []
def register(self, task, schedule, minutes: int = None):
def register(self, task, schedule, minutes: Optional[int] = None):
"""Register a task with the que."""
self.task_list.append(ScheduledTask(task, schedule, minutes))
@ -299,7 +303,9 @@ class TaskRegister:
tasks = TaskRegister()
def scheduled_task(interval: str, minutes: int = None, tasklist: TaskRegister = None):
def scheduled_task(
interval: str, minutes: Optional[int] = None, tasklist: TaskRegister = None
):
"""Register the given task as a scheduled task.
Example:
@ -688,3 +694,14 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True):
# We should be current now - triggering full reload to make sure all models
# are loaded fully in their new state.
registry.reload_plugins(full_reload=True, force_reload=True, collect=True)
def email_user(user_id: int, subject: str, message: str) -> None:
"""Send a message to a user."""
try:
user = get_user_model().objects.get(pk=user_id)
except Exception:
logger.warning('User <%s> not found - cannot send welcome message', user_id)
return
user.email_user(subject=subject, message=message)

View File

@ -80,7 +80,7 @@ def do_translate(parser, token):
"""
bits = token.split_contents()
if len(bits) < 2:
raise TemplateSyntaxError("'%s' takes at least one argument" % bits[0])
raise TemplateSyntaxError(f"'{bits[0]}' takes at least one argument")
message_string = parser.compile_filter(bits[1])
remaining = bits[2:]
@ -95,7 +95,7 @@ def do_translate(parser, token):
option = remaining.pop(0)
if option in seen:
raise TemplateSyntaxError(
"The '%s' option was specified more than once." % option
f"The '{option}' option was specified more than once."
)
elif option == 'noop':
noop = True
@ -104,13 +104,12 @@ def do_translate(parser, token):
value = remaining.pop(0)
except IndexError:
raise TemplateSyntaxError(
"No argument provided to the '%s' tag for the context option."
% bits[0]
f"No argument provided to the '{bits[0]}' tag for the context option."
)
if value in invalid_context:
raise TemplateSyntaxError(
"Invalid argument '%s' provided to the '%s' tag for the context "
'option' % (value, bits[0])
f"Invalid argument '{value}' provided to the '{bits[0]}' tag for the context "
'option'
)
message_context = parser.compile_filter(value)
elif option == 'as':
@ -118,16 +117,15 @@ def do_translate(parser, token):
value = remaining.pop(0)
except IndexError:
raise TemplateSyntaxError(
"No argument provided to the '%s' tag for the as option." % bits[0]
f"No argument provided to the '{bits[0]}' tag for the as option."
)
asvar = value
elif option == 'escape':
escape = True
else:
raise TemplateSyntaxError(
"Unknown argument for '%s' tag: '%s'. The only options "
f"Unknown argument for '{bits[0]}' tag: '{option}'. The only options "
"available are 'noop', 'context' \"xxx\", and 'as VAR'."
% (bits[0], option)
)
seen.add(option)

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