mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-17 20:35:01 -06:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d8eb2e0b3 | ||
|
|
56b16cb1ac | ||
|
|
e5a36f6936 | ||
|
|
61b5a7d393 | ||
|
|
23a9485e7e | ||
|
|
19924cac60 | ||
|
|
c1d9732e7c | ||
|
|
68e2f08fa5 | ||
|
|
cc4535748e | ||
|
|
2329179070 | ||
|
|
50fdefa473 | ||
|
|
1f522f47a5 | ||
|
|
b17c835218 | ||
|
|
91c5843425 | ||
|
|
b57f53c4cf | ||
|
|
fa1a9da23a | ||
|
|
fe09437214 | ||
|
|
f1dfced89b | ||
|
|
d7c76aab9d | ||
|
|
dfdaddbc7e | ||
|
|
1f6e52138a | ||
|
|
e8c9ec076c | ||
|
|
d4d9aa9d1b | ||
|
|
54f2072e97 | ||
|
|
d1042cde0e | ||
|
|
5f4275679d | ||
|
|
1ba0bee1ea | ||
|
|
ea7aa93a28 | ||
|
|
9eccf69456 | ||
|
|
9cebfa85df | ||
|
|
af3cf62b8e | ||
|
|
f20a1245e7 | ||
|
|
92a4989a8d | ||
|
|
be3b22ce36 | ||
|
|
258b8e4ecc | ||
|
|
7df92aad03 | ||
|
|
2dac705779 |
7
.github/actions/setup/action.yaml
vendored
7
.github/actions/setup/action.yaml
vendored
@@ -49,9 +49,10 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -m pip install -U pip
|
||||
pip3 install invoke wheel uv
|
||||
- name: Set the VIRTUAL_ENV variable for uv to work
|
||||
run: echo "VIRTUAL_ENV=${Python_ROOT_DIR}" >> $GITHUB_ENV
|
||||
pip3 install -U invoke wheel
|
||||
pip3 install 'uv<0.3.0'
|
||||
- name: Allow uv to use the system Python by default
|
||||
run: echo "UV_SYSTEM_PYTHON=1" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
- name: Install Specific Python Dependencies
|
||||
if: ${{ inputs.pip-dependency }}
|
||||
|
||||
7
.github/workflows/docker.yaml
vendored
7
.github/workflows/docker.yaml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Version Check
|
||||
run: |
|
||||
pip install --require-hashes -r .github/requirements.txt
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
|
||||
python3 .github/scripts/version_check.py
|
||||
echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
|
||||
@@ -85,12 +85,17 @@ jobs:
|
||||
docker run --rm inventree-test invoke --list
|
||||
docker run --rm inventree-test gunicorn --version
|
||||
docker run --rm inventree-test pg_dump --version
|
||||
docker run --rm inventree-test test -f /home/inventree/init.sh
|
||||
docker run --rm inventree-test test -f /home/inventree/tasks.py
|
||||
docker run --rm inventree-test test -f /home/inventree/gunicorn.conf.py
|
||||
docker run --rm inventree-test test -f /home/inventree/src/backend/requirements.txt
|
||||
docker run --rm inventree-test test -f /home/inventree/src/backend/InvenTree/manage.py
|
||||
- name: Build Docker Image
|
||||
# Build the development docker image (using docker-compose.yml)
|
||||
run: docker compose --project-directory . -f contrib/container/dev-docker-compose.yml build --no-cache
|
||||
- name: Update Docker Image
|
||||
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 up -d
|
||||
|
||||
10
.github/workflows/qc_checks.yaml
vendored
10
.github/workflows/qc_checks.yaml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # pin@v3.0.1
|
||||
- name: Check Version
|
||||
run: |
|
||||
pip install --require-hashes -r .github/requirements.txt
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
|
||||
python3 .github/scripts/version_check.py
|
||||
|
||||
mkdocs:
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
python-version: ${{ env.python_version }}
|
||||
- name: Check Config
|
||||
run: |
|
||||
pip install --require-hashes -r .github/requirements.txt
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
|
||||
pip install --require-hashes -r docs/requirements.txt
|
||||
python docs/ci/check_mkdocs_config.py
|
||||
- name: Check Links
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- name: Download public schema
|
||||
if: needs.paths-filter.outputs.api == 'false'
|
||||
run: |
|
||||
pip install --require-hashes -r .github/requirements.txt >/dev/null 2>&1
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
|
||||
version="$(python3 .github/scripts/version_check.py only_version 2>&1)"
|
||||
echo "Version: $version"
|
||||
url="https://raw.githubusercontent.com/inventree/schema/main/export/${version}/api.yaml"
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
id: version
|
||||
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
|
||||
run: |
|
||||
pip install --require-hashes -r .github/requirements.txt >/dev/null 2>&1
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt >/dev/null 2>&1
|
||||
version="$(python3 .github/scripts/version_check.py only_version 2>&1)"
|
||||
echo "Version: $version"
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
@@ -554,6 +554,8 @@ jobs:
|
||||
run: cd src/frontend && yarn install
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && yarn run compile && yarn run build
|
||||
- name: Write version file - SHA
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
|
||||
- name: Zip frontend
|
||||
run: |
|
||||
cd src/backend/InvenTree/web/static
|
||||
|
||||
15
.github/workflows/release.yaml
vendored
15
.github/workflows/release.yaml
vendored
@@ -5,12 +5,12 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
stable:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # pin@v4.1.5
|
||||
- name: Version Check
|
||||
run: |
|
||||
pip install --require-hashes -r .github/requirements.txt
|
||||
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
|
||||
python3 .github/scripts/version_check.py
|
||||
- name: Push to Stable Branch
|
||||
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0
|
||||
@@ -30,6 +30,9 @@ jobs:
|
||||
|
||||
publish-build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # pin@v4.1.5
|
||||
- name: Environment Setup
|
||||
@@ -40,6 +43,10 @@ jobs:
|
||||
run: cd src/frontend && yarn install
|
||||
- name: Build frontend
|
||||
run: cd src/frontend && npm run compile && npm run build
|
||||
- name: Write version file - SHA
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "$GITHUB_SHA" > sha.txt
|
||||
- name: Write version file - TAG
|
||||
run: cd src/backend/InvenTree/web/static/web/.vite && echo "${{ github.ref_name }}" > tag.txt
|
||||
- name: Zip frontend
|
||||
run: |
|
||||
cd src/backend/InvenTree/web/static/web
|
||||
|
||||
@@ -32,7 +32,7 @@ dependencies:
|
||||
- gettext
|
||||
- nginx
|
||||
- jq
|
||||
- libffi7
|
||||
- "libffi7 | libffi8"
|
||||
targets:
|
||||
ubuntu-20.04: true
|
||||
debian-11: true
|
||||
|
||||
@@ -39,8 +39,8 @@ repos:
|
||||
files: src/backend/requirements\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements.txt
|
||||
args: [.github/requirements.in, -o, .github/requirements.txt,--python-version=3.9, --no-strip-extras, --generate-hashes]
|
||||
files: .github/requirements\.(in|txt)$
|
||||
args: [contrib/dev_reqs/requirements.in, -o, contrib/dev_reqs/requirements.txt,--python-version=3.9, --no-strip-extras, --generate-hashes]
|
||||
files: contrib/dev_reqs/requirements\.(in|txt)$
|
||||
- id: pip-compile
|
||||
name: pip-compile requirements.txt
|
||||
args: [docs/requirements.in, -o, docs/requirements.txt,--python-version=3.9, --no-strip-extras, --generate-hashes]
|
||||
|
||||
@@ -123,7 +123,7 @@ Refer to the [getting started guide](https://docs.inventree.org/en/latest/start/
|
||||
<!-- Mobile App -->
|
||||
## :iphone: Mobile App
|
||||
|
||||
InvenTree is supported by a [companion mobile app](https://docs.inventree.org/en/latest/app/app/) which allows users access to stock control information and functionality.
|
||||
InvenTree is supported by a [companion mobile app](https://docs.inventree.org/app/) which allows users access to stock control information and functionality.
|
||||
|
||||
<div align="center"><h4>
|
||||
<a href="https://play.google.com/store/apps/details?id=inventree.inventree_app">Android Play Store</a>
|
||||
|
||||
@@ -128,6 +128,7 @@ COPY --from=prebuild /root/.local /root/.local
|
||||
|
||||
# Copy source code
|
||||
COPY src/backend/InvenTree ${INVENTREE_BACKEND_DIR}/InvenTree
|
||||
COPY src/backend/requirements.txt ${INVENTREE_BACKEND_DIR}/requirements.txt
|
||||
COPY --from=frontend ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web
|
||||
|
||||
# Launch the production server
|
||||
|
||||
@@ -114,7 +114,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro,z
|
||||
- ${INVENTREE_EXT_VOLUME}/static:/var/www/static:z
|
||||
- ${INVENTREE_EXT_VOLUME}/media:/var/www/media:z
|
||||
- ${INVENTREE_EXT_VOLUME}:/var/log:z
|
||||
|
||||
@@ -4,9 +4,9 @@ asgiref==3.8.1 \
|
||||
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
|
||||
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
|
||||
# via django
|
||||
django==4.2.11 \
|
||||
--hash=sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4 \
|
||||
--hash=sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3
|
||||
django==4.2.14 \
|
||||
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
|
||||
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
|
||||
# via django-auth-ldap
|
||||
django-auth-ldap==4.8.0 \
|
||||
--hash=sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements.in -o .github/requirements.txt --python-version=3.9 --no-strip-extras --generate-hashes
|
||||
# uv pip compile contrib/dev_reqs/requirements.in -o contrib/dev_reqs/requirements.txt --python-version=3.9 --no-strip-extras --generate-hashes
|
||||
certifi==2024.2.2 \
|
||||
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \
|
||||
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1
|
||||
@@ -75,6 +75,7 @@ root_command() {
|
||||
;;
|
||||
"Debian GNU/Linux" | "debian gnu/linux" | Raspbian)
|
||||
if [[ $VER == "12" ]]; then
|
||||
DIST_VER="11"
|
||||
SUPPORTED=true
|
||||
elif [[ $VER == "11" ]]; then
|
||||
SUPPORTED=true
|
||||
|
||||
@@ -5,33 +5,41 @@
|
||||
|
||||
set -eu
|
||||
|
||||
VERSION="$APP_PKG_VERSION-$APP_PKG_ITERATION"
|
||||
echo "Setting VERSION information to $VERSION"
|
||||
echo "$VERSION" > VERSION
|
||||
|
||||
# The sha is the second element in APP_PKG_ITERATION
|
||||
REPO="inventree/InvenTree"
|
||||
VERSION="$APP_PKG_VERSION-$APP_PKG_ITERATION"
|
||||
SHA=$(echo $APP_PKG_ITERATION | cut -d'.' -f2)
|
||||
|
||||
# Download info
|
||||
echo "Getting info from github for commit $SHA"
|
||||
curl -L \
|
||||
echo "INFO collection | Getting info from github for commit $SHA"
|
||||
curl -L -s -f \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/InvenTree/InvenTree/commits/$SHA > commit.json
|
||||
curl -L \
|
||||
https://api.github.com/repos/$REPO/commits/$SHA > commit.json
|
||||
echo "INFO collection | Got commit.json with size $(wc -c commit.json)"
|
||||
curl -L -s -f \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/InvenTree/InvenTree/commits/$SHA/branches-where-head > branches.json
|
||||
https://api.github.com/repos/$REPO/commits/$SHA/branches-where-head > branches.json
|
||||
echo "INFO collection | Got branches.json with size $(wc -c branches.json)"
|
||||
curl -L -s -f \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/$REPO/commits/$APP_PKG_VERSION > tag.json
|
||||
echo "INFO collection | Got tag.json with size $(wc -c tag.json)"
|
||||
|
||||
# Extract info
|
||||
echo "Extracting info from github"
|
||||
echo "INFO extract | Extracting info from github"
|
||||
DATE=$(jq -r '.commit.committer.date' commit.json)
|
||||
BRANCH=$(jq -r '.[].name' branches.json)
|
||||
NODE_ID=$(jq -r '.node_id' commit.json)
|
||||
SIGNATURE=$(jq -r '.commit.verification.signature' commit.json)
|
||||
FULL_SHA=$(jq -r '.sha' commit.json)
|
||||
|
||||
echo "Write VERSION information"
|
||||
echo "INFO write | Write VERSION information"
|
||||
echo "$VERSION" > VERSION
|
||||
echo "INVENTREE_COMMIT_HASH='$SHA'" >> VERSION
|
||||
echo "INVENTREE_COMMIT_SHA='$FULL_SHA'" >> VERSION
|
||||
echo "INVENTREE_COMMIT_DATE='$DATE'" >> VERSION
|
||||
echo "INVENTREE_PKG_INSTALLER='PKG'" >> VERSION
|
||||
echo "INVENTREE_PKG_BRANCH='$BRANCH'" >> VERSION
|
||||
@@ -39,5 +47,22 @@ echo "INVENTREE_PKG_TARGET='$TARGET'" >> VERSION
|
||||
echo "NODE_ID='$NODE_ID'" >> VERSION
|
||||
echo "SIGNATURE='$SIGNATURE'" >> VERSION
|
||||
|
||||
echo "Written VERSION information"
|
||||
echo "INFO write | Written VERSION information"
|
||||
echo "### VERSION ###"
|
||||
cat VERSION
|
||||
echo "### VERSION ###"
|
||||
|
||||
# Try to get frontend
|
||||
echo "INFO frontend | Trying to get frontend"
|
||||
# Check if tag sha is the same as the commit sha
|
||||
TAG_SHA=$(jq -r '.sha' tag.json)
|
||||
if [ "$TAG_SHA" != "$FULL_SHA" ]; then
|
||||
echo "INFO frontend | Tag sha '$TAG_SHA' is not the same as commit sha $FULL_SHA, can not download frontend"
|
||||
else
|
||||
echo "INFO frontend | Getting frontend from github via tag"
|
||||
curl https://github.com/$REPO/releases/download/$APP_PKG_VERSION/frontend-build.zip -L -O -f
|
||||
mkdir -p src/backend/InvenTree/web/static
|
||||
echo "INFO frontend | Unzipping frontend"
|
||||
unzip -qq frontend-build.zip -d src/backend/InvenTree/web/static/web
|
||||
echo "INFO frontend | Unzipped frontend"
|
||||
fi
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#
|
||||
# packager.io postinstall script functions
|
||||
#
|
||||
Color_Off='\033[0m'
|
||||
On_Red='\033[41m'
|
||||
|
||||
function detect_docker() {
|
||||
if [ -n "$(grep docker </proc/1/cgroup)" ]; then
|
||||
@@ -44,6 +46,28 @@ function detect_ip() {
|
||||
echo "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"
|
||||
# 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}"
|
||||
else
|
||||
echo "# No python environment found - using environment variable: ${SETUP_PYTHON}"
|
||||
fi
|
||||
|
||||
# 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 "${Color_Off}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function get_env() {
|
||||
envname=$1
|
||||
|
||||
@@ -90,7 +114,7 @@ function detect_envs() {
|
||||
echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}"
|
||||
|
||||
# Install parser
|
||||
pip install --require-hashes -r ${APP_HOME}/.github/requirements.txt -q
|
||||
pip install --require-hashes -r ${APP_HOME}/contrib/dev_reqs/requirements.txt -q
|
||||
|
||||
# Load config
|
||||
local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)
|
||||
@@ -163,12 +187,20 @@ function create_initscripts() {
|
||||
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"
|
||||
source ${APP_HOME}/INSTALLER_EXTRA
|
||||
fi
|
||||
|
||||
if [ -n "${SETUP_EXTRA_PIP}" ]; then
|
||||
echo "# Installing extra pip packages"
|
||||
if [ -n "${SETUP_DEBUG}" ]; then
|
||||
echo "# 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
|
||||
echo "SETUP_EXTRA_PIP='${SETUP_EXTRA_PIP}'" >>${APP_HOME}/INSTALLER_EXTRA
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -283,6 +315,20 @@ function set_env() {
|
||||
chown ${APP_USER}:${APP_GROUP} ${DATA_DIR} ${INVENTREE_CONFIG_FILE}
|
||||
}
|
||||
|
||||
function set_site() {
|
||||
# Ensure IP is known
|
||||
if [ -z "${INVENTREE_IP}" ]; then
|
||||
echo "# 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"
|
||||
inventree config:set INVENTREE_SITE_URL=http://${INVENTREE_IP}
|
||||
fi
|
||||
}
|
||||
|
||||
function final_message() {
|
||||
echo -e "####################################################################################"
|
||||
echo -e "This InvenTree install uses nginx, the settings for the webserver can be found in"
|
||||
|
||||
@@ -33,6 +33,7 @@ detect_envs
|
||||
detect_docker
|
||||
detect_initcmd
|
||||
detect_ip
|
||||
detect_python
|
||||
|
||||
# create processes
|
||||
create_initscripts
|
||||
@@ -45,6 +46,7 @@ update_or_install
|
||||
if [ "${SETUP_CONF_LOADED}" = "true" ]; then
|
||||
set_env
|
||||
fi
|
||||
set_site
|
||||
start_inventree
|
||||
|
||||
# show info
|
||||
|
||||
4
docs/.gitignore
vendored
4
docs/.gitignore
vendored
@@ -13,6 +13,10 @@ site/
|
||||
# Generated API schema files
|
||||
docs/api/schema/*.yml
|
||||
|
||||
# Temporary cache files
|
||||
url_cache.txt
|
||||
invoke-commands.txt
|
||||
|
||||
# Temp files
|
||||
releases.json
|
||||
versions.json
|
||||
|
||||
@@ -96,7 +96,7 @@ The HEAD of the "stable" branch represents the latest stable release code.
|
||||
|
||||
## API versioning
|
||||
|
||||
The [API version](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
|
||||
The [API version]({{ sourcefile("src/backend/InvenTree/InvenTree/api_version.py") }}) needs to be bumped every time when the API is changed.
|
||||
|
||||
## Environment
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ For further information, read more about [installing plugins](./plugins/install.
|
||||
|
||||
### Plugin Base Class
|
||||
|
||||
Custom plugins must inherit from the [InvenTreePlugin class](https://github.com/inventree/InvenTree/blob/2d1776a151721d65d0ae007049d358085b2fcfd5/InvenTree/plugin/plugin.py#L204). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches.
|
||||
Custom plugins must inherit from the [InvenTreePlugin class]({{ sourcefile("src/backend/InvenTree/plugin/plugin.py") }}). Any plugins installed via the methods outlined above will be "discovered" when the InvenTree server launches.
|
||||
|
||||
!!! warning "Namechange"
|
||||
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`. While the old name is still available till `0.8.0` we strongly suggest upgrading your plugins. Deprecation warnings are raised if the old name is used.
|
||||
@@ -28,7 +28,7 @@ Please read all release notes and watch out for warnings - we generally provide
|
||||
|
||||
#### Plugins
|
||||
|
||||
General classes and mechanisms are provided under the `plugin` [namespaces](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/__init__.py). These include:
|
||||
General classes and mechanisms are provided under the `plugin` [namespaces]({{ sourcefile("src/backend/InvenTree/plugin/__init__.py") }}). These include:
|
||||
|
||||
```python
|
||||
# Management objects
|
||||
@@ -44,7 +44,7 @@ MixinNotImplementedError # Is raised if a mixin was not implemented (core mec
|
||||
|
||||
#### Mixins
|
||||
|
||||
Mixins are split up internally to keep the source tree clean and enable better testing separation. All public APIs that should be used are exposed under `plugin.mixins`. These include all built-in mixins and notification methods. An up-to-date reference can be found in the source code (current master can be [found here](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/mixins/__init__.py)).
|
||||
Mixins are split up internally to keep the source tree clean and enable better testing separation. All public APIs that should be used are exposed under `plugin.mixins`. These include all built-in mixins and notification methods. An up-to-date reference can be found in the source code [can be found here]({{ sourcefile("src/backend/InvenTree/plugin/mixins/__init__.py") }}).
|
||||
|
||||
#### Models and other internal InvenTree APIs
|
||||
|
||||
@@ -72,7 +72,7 @@ MIN_VERSION = None # Lowest InvenTree version number that is supported by the p
|
||||
MAX_VERSION = None # Highest InvenTree version number that is supported by the plugin
|
||||
```
|
||||
|
||||
Refer to the [sample plugins](https://github.com/inventree/InvenTree/tree/master/src/backend/InvenTree/plugin/samples) for further examples.
|
||||
Refer to the [sample plugins]({{ sourcedir("src/backend/InvenTree/plugin/samples") }}) for further examples.
|
||||
|
||||
### Plugin Config
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ If a locate plugin is installed and activated, the [InvenTree mobile app](../../
|
||||
|
||||
### Implementation
|
||||
|
||||
Refer to the [InvenTree source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/locate/locate_sample.py) for a simple implementation example.
|
||||
Refer to the [InvenTree source code]({{ sourcefile("src/backend/InvenTree/plugin/samples/locate/locate_sample.py") }}) for a simple implementation example.
|
||||
|
||||
@@ -16,7 +16,7 @@ Additionally the `add_label_context` method, allowing custom context data to be
|
||||
|
||||
### Example
|
||||
|
||||
A sample plugin which provides additional context data to the report templates can be found [in the InvenTree source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/integration/report_plugin_sample.py):
|
||||
A sample plugin which provides additional context data to the report templates can be found [in the InvenTree source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/samples/integration/report_plugin_sample.py):
|
||||
|
||||
```python
|
||||
"""Sample plugin for extending reporting functionality"""
|
||||
|
||||
@@ -59,4 +59,4 @@ class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin):
|
||||
```
|
||||
|
||||
!!! info "More Info"
|
||||
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/integration/scheduled_task.py).
|
||||
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/plugin/samples/integration/scheduled_task.py).
|
||||
|
||||
@@ -65,7 +65,7 @@ Additionally, add the following imports after the extended line.
|
||||
#### Blocks
|
||||
The page_base file is split into multiple sections called blocks. This allows you to implement sections of the webpage while getting many items like navbars, sidebars, and general layout provided for you.
|
||||
|
||||
The current default page base can be found [here](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/templates/page_base.html). Look through this file to determine overridable blocks. The [stock app](https://github.com/inventree/InvenTree/tree/master/src/backend/InvenTree/stock) offers a great example of implementing these blocks.
|
||||
The current default page base can be found [here]({{ sourcefile("src/backend/InvenTree/templates/page_base.html") }}). Look through this file to determine overridable blocks. The [stock app]({{ sourcedir("src/backend/InvenTree/stock") }}) offers a great example of implementing these blocks.
|
||||
|
||||
!!! warning "Sidebar Block"
|
||||
You may notice that implementing the `sidebar` block doesn't initially work. Be sure to enable the sidebar using JavaScript. This can be achieved by appending the following code, replacing `label` with a label of your choosing, to the end of your template file.
|
||||
|
||||
@@ -9,7 +9,7 @@ The `ValidationMixin` class enables plugins to perform custom validation of obje
|
||||
Any of the methods described below can be implemented in a custom plugin to provide functionality as required.
|
||||
|
||||
!!! info "More Info"
|
||||
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/plugin/samples/integration/validation_sample.py).
|
||||
For more information on any of the methods described below, refer to the InvenTree source code. [A working example is available as a starting point]({{ sourcefile("src/backend/InvenTree/plugin/samples/integration/validation_sample.py") }}).
|
||||
|
||||
!!! info "Multi Plugin Support"
|
||||
It is possible to have multiple plugins loaded simultaneously which support validation methods. For example when validating a field, if one plugin returns a null value (`None`) then the *next* plugin (if available) will be queried.
|
||||
|
||||
@@ -183,4 +183,4 @@ Finally added a `{% raw %}|floatformat:0{% endraw %}` to the quantity that remov
|
||||
|
||||
A default *BOM Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_bill_of_materials_report.html) for the default test report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_bill_of_materials_report.html) for the default test report template.
|
||||
|
||||
@@ -321,4 +321,4 @@ This will result a report page like this:
|
||||
|
||||
A default *Build Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_build_order_base.html) for the default build report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_build_order_base.html) for the default build report template.
|
||||
|
||||
@@ -12,7 +12,7 @@ Some common functions are provided for use in custom report and label templates.
|
||||
```
|
||||
|
||||
!!! tip "Use the Source, Luke"
|
||||
To see the full range of available helper functions, refer to the source file [report.py](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templatetags/report.py) where these functions are defined!
|
||||
To see the full range of available helper functions, refer to the source file [report.py]({{ sourcefile("src/backend/InvenTree/report/templatetags/report.py") }}) where these functions are defined!
|
||||
|
||||
## Assigning Variables
|
||||
|
||||
|
||||
@@ -62,4 +62,4 @@ Price: {% render_currency line.total_line_price %}
|
||||
|
||||
A default *Purchase Order Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_po_report_base.html) for the default purchase order report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_po_report_base.html) for the default purchase order report template.
|
||||
|
||||
@@ -23,4 +23,4 @@ In addition to the default report context variables, the following context varia
|
||||
|
||||
A default report template is provided out of the box, which can be used as a starting point for developing custom return order report templates.
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_return_order_report_base.html) for the default return order report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_return_order_report_base.html) for the default return order report template.
|
||||
|
||||
@@ -28,4 +28,4 @@ In addition to the default report context variables, the following variables are
|
||||
|
||||
A default *Sales Order Report* template is provided out of the box, which is useful for generating simple test reports. Furthermore, it may be used as a starting point for developing custom BOM reports:
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_so_report_base.html) for the default sales order report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_so_report_base.html) for the default sales order report template.
|
||||
|
||||
@@ -13,4 +13,4 @@ You can use all content variables from the [StockLocation](./context_variables.m
|
||||
|
||||
A default report template is provided out of the box, which can be used as a starting point for developing custom return order report templates.
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_slr_report.html) for the default stock location report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_slr_report.html) for the default stock location report template.
|
||||
|
||||
@@ -84,4 +84,4 @@ A default *Test Report* template is provided out of the box, which is useful for
|
||||
{% include "img.html" %}
|
||||
{% endwith %}
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_test_report_base.html) for the default test report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/0.15.x/src/backend/InvenTree/report/templates/report/inventree_test_report_base.html) for the default test report template.
|
||||
|
||||
@@ -8,9 +8,9 @@ To that end, we have implemented a number of security measures over the years, w
|
||||
The InvenTree project is managed by a small team of developers, who are responsible for the ongoing development and maintenance of the software. Two geographically distributed users have administrative access to the InvenTree codebase. Merges are only done by one of these two users, the maintainer Oliver.
|
||||
InvenTree is open-source, and we welcome contributions from the community. However, all contributions are reviewed and scrutinised before being merged into the codebase.
|
||||
|
||||
We provide a written [Security Policy](https://github.com/inventree/InvenTree/blob/master/SECURITY.md) in our main repo to ensure that all security issues are handled in a timely manner.
|
||||
We provide a written [Security Policy]({{ sourcefile("SECURITY.md") }}) in our main repo to ensure that all security issues are handled in a timely manner.
|
||||
|
||||
If we become aware of a security issue, we will take immediate action to address the issue, and will provide a public disclosure of the issue once it has been resolved. We support assigning CVEs to security issues where appropriate. Our past security advisories can be found [here](https://github.com/inventree/InvenTree/security/advisories).
|
||||
If we become aware of a security issue, we will take immediate action to address the issue, and will provide a public disclosure of the issue once it has been resolved. We support assigning CVEs to security issues where appropriate. Our [past security advisories can be found here](https://github.com/inventree/InvenTree/security/advisories).
|
||||
|
||||
## Technical measures
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ title: InvenTree Single Sign On
|
||||
|
||||
## Single Sign On
|
||||
|
||||
InvenTree provides the possibility to use 3rd party services to authenticate users. This functionality makes use of [django-allauth](https://django-allauth.readthedocs.io/en/latest/) and supports a wide array of OpenID and OAuth [providers](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html).
|
||||
InvenTree provides the possibility to use 3rd party services to authenticate users. This functionality makes use of [django-allauth](https://docs.allauth.org/en/latest/) and supports a wide array of OpenID and OAuth [providers](https://docs.allauth.org/en/latest/socialaccount/providers/index.html).
|
||||
|
||||
!!! tip "Provider Documentation"
|
||||
There are a lot of technical considerations when configuring a particular SSO provider. A good starting point is the [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html)
|
||||
There are a lot of technical considerations when configuring a particular SSO provider. A good starting point is the [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html)
|
||||
|
||||
!!! warning "Advanced Users"
|
||||
The SSO functionality provided by django-allauth is powerful, but can prove challenging to configure. Please ensure that you understand the implications of enabling SSO for your InvenTree instance. Specific technical details of each available SSO provider are beyond the scope of this documentation - please refer to the [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) for more information.
|
||||
The SSO functionality provided by django-allauth is powerful, but can prove challenging to configure. Please ensure that you understand the implications of enabling SSO for your InvenTree instance. Specific technical details of each available SSO provider are beyond the scope of this documentation - please refer to the [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) for more information.
|
||||
|
||||
## SSO Configuration
|
||||
|
||||
@@ -31,8 +31,8 @@ There are two variables in the configuration file which define the operation of
|
||||
|
||||
| Environment Variable |Configuration File | Description | More Info |
|
||||
| --- | --- | --- | --- |
|
||||
| INVENTREE_SOCIAL_BACKENDS | `social_backends` | A *list* of provider backends enabled for the InvenTree instance | [django-allauth docs](https://django-allauth.readthedocs.io/en/latest/installation/quickstart.html) |
|
||||
| INVENTREE_SOCIAL_PROVIDERS | `social_providers` | A *dict* of settings specific to the installed providers | [provider documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) |
|
||||
| INVENTREE_SOCIAL_BACKENDS | `social_backends` | A *list* of provider backends enabled for the InvenTree instance | [django-allauth docs](https://docs.allauth.org/en/latest/installation/quickstart.html) |
|
||||
| INVENTREE_SOCIAL_PROVIDERS | `social_providers` | A *dict* of settings specific to the installed providers | [provider documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) |
|
||||
|
||||
In the example below, SSO provider modules are activated for *google*, *github* and *microsoft*. Specific configuration options are specified for the *microsoft* provider module:
|
||||
|
||||
@@ -44,7 +44,7 @@ In the example below, SSO provider modules are activated for *google*, *github*
|
||||
Note that the provider modules specified in `social_backends` must be prefixed with `allauth.socialaccounts.providers`
|
||||
|
||||
!!! warning "Provider Documentation"
|
||||
We do not provide any specific documentation for each provider module. Please refer to the [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) for more information.
|
||||
We do not provide any specific documentation for each provider module. Please refer to the [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) for more information.
|
||||
|
||||
!!! tip "Restart Server"
|
||||
As the [configuration file](../start/config.md) is only read when the server is launched, ensure you restart the server after editing the file.
|
||||
@@ -57,7 +57,7 @@ The next step is to create an external authentication app with your provider of
|
||||
The provider application will be created as part of your SSO provider setup. This is *not* the same as the *SocialApp* entry in the InvenTree admin interface.
|
||||
|
||||
!!! info "Read the Documentation"
|
||||
The [django-allauth documentation](https://django-allauth.readthedocs.io/en/latest/socialaccount/providers/index.html) is a good starting point here. There are also a number of good tutorials online (at least for the major supported SSO providers).
|
||||
The [django-allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) is a good starting point here. There are also a number of good tutorials online (at least for the major supported SSO providers).
|
||||
|
||||
In general, the external app will generate a *key* and *secret* pair - although different terminology may be used, depending on the provider.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ The InvenTree server tries to locate the `config.yaml` configuration file on sta
|
||||
!!! tip "Config File Location"
|
||||
When the InvenTree server boots, it will report the location where it expects to find the configuration file
|
||||
|
||||
The configuration file *template* can be found on [GitHub](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/config_template.yaml)
|
||||
The configuration file *template* can be found on [GitHub]({{ sourcefile("src/backend/InvenTree/config_template.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.
|
||||
|
||||
@@ -27,13 +27,13 @@ The following guide provides a streamlined production InvenTree installation, wi
|
||||
|
||||
### Required Files
|
||||
|
||||
The following files required for this setup are provided with the InvenTree source, located in the `/contrib/container/` directory of the [InvenTree source code](https://github.com/inventree/InvenTree/tree/master/contrib/container/):
|
||||
The following files required for this setup are provided with the InvenTree source, located in the `contrib/container/` directory of the [InvenTree source code]({{ sourcedir("/contrib/container/") }}):
|
||||
|
||||
| Filename | Description |
|
||||
| --- | --- |
|
||||
| [docker-compose.yml](https://raw.githubusercontent.com/inventree/InvenTree/master/contrib/container/docker-compose.yml)| The docker compose script |
|
||||
| [.env](https://raw.githubusercontent.com/inventree/InvenTree/master/contrib/container/.env) | Environment variables |
|
||||
| [Caddyfile](https://raw.githubusercontent.com/inventree/InvenTree/master/contrib/container/Caddyfile) | Caddy configuration file |
|
||||
| [docker-compose.yml]({{ sourcefile("contrib/container/docker-compose.yml", raw=True) }}) | The docker compose script |
|
||||
| [.env]({{ sourcefile("contrib/container/.env", raw=True) }}) | Environment variables |
|
||||
| [Caddyfile]({{ sourcefile("contrib/container/Caddyfile", raw=True) }}) | Caddy configuration file |
|
||||
|
||||
Download these files to a directory on your local machine.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Install required system packages (as superuser):
|
||||
The following packages are required on a debian system. A different distribution may require a slightly different set of packages
|
||||
|
||||
!!! info "Python Version"
|
||||
InvenTree requires a modern Python version check [here](https://github.com/inventree/InvenTree/blob/master/CONTRIBUTING.md#target-version) for the current minimums.
|
||||
InvenTree requires a modern Python version [check here]({{ sourcefile("CONTRIBUTING.md") }}) for the current minimums.
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
|
||||
@@ -15,6 +15,14 @@ wget -qO install.sh https://get.inventree.org && bash install.sh
|
||||
|
||||
This script does all manual steps without any input. The installation might take up to 5-10 minutes to finish.
|
||||
|
||||
#### Permission Denied Error
|
||||
|
||||
The above command may need to be run with `sudo` permissions, depending on the system configuration. So, if the script fails with a permission error, try:
|
||||
|
||||
```bash
|
||||
sudo wget -qO install.sh https://get.inventree.org && sudo bash install.sh
|
||||
```
|
||||
|
||||
### File Locations
|
||||
|
||||
The installer creates the following directories:
|
||||
|
||||
@@ -84,6 +84,12 @@ To display a list of the available InvenTree administration actions, run the fol
|
||||
invoke --list
|
||||
```
|
||||
|
||||
This provides a list of the available invoke commands - also displayed below:
|
||||
|
||||
```
|
||||
{{ invoke_commands() }}
|
||||
```
|
||||
|
||||
### Virtual Environment
|
||||
|
||||
Installing the required Python packages inside a virtual environment allows a local install separate to the system-wide Python installation. While not strictly necessary, using a virtual environment is **highly recommended** as it prevents conflicts between the different Python installations.
|
||||
|
||||
160
docs/main.py
160
docs/main.py
@@ -1,11 +1,171 @@
|
||||
"""Main entry point for the documentation build process."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
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:
|
||||
mkdocs_config = yaml.safe_load(f)
|
||||
repo_name = mkdocs_config['repo_name']
|
||||
|
||||
if raw:
|
||||
return f'https://raw.githubusercontent.com/{repo_name}'
|
||||
else:
|
||||
return f'https://github.com/{repo_name}'
|
||||
|
||||
|
||||
def check_link(url) -> bool:
|
||||
"""Check that a provided URL is valid.
|
||||
|
||||
We allow a number attempts and a lengthy timeout,
|
||||
as we do not want false negatives.
|
||||
"""
|
||||
CACHE_FILE = os.path.join(os.path.dirname(__file__), 'url_cache.txt')
|
||||
|
||||
# Keep a local cache file of URLs we have already checked
|
||||
if os.path.exists(CACHE_FILE):
|
||||
with open(CACHE_FILE, 'r') as f:
|
||||
cache = f.read().splitlines()
|
||||
|
||||
if url in cache:
|
||||
return True
|
||||
|
||||
attempts = 5
|
||||
|
||||
while attempts > 0:
|
||||
response = requests.head(url, timeout=5000)
|
||||
if response.status_code == 200:
|
||||
# Update the cache file
|
||||
with open(CACHE_FILE, 'a') as f:
|
||||
f.write(f'{url}\n')
|
||||
|
||||
return True
|
||||
|
||||
attempts -= 1
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_build_enviroment() -> str:
|
||||
"""Returns the branch we are currently building on, based on the environment variables of the various CI platforms."""
|
||||
# Check if we are in ReadTheDocs
|
||||
if os.environ.get('READTHEDOCS') == 'True':
|
||||
return os.environ.get('READTHEDOCS_GIT_IDENTIFIER')
|
||||
# We are in GitHub Actions
|
||||
elif os.environ.get('GITHUB_ACTIONS') == 'true':
|
||||
return os.environ.get('GITHUB_REF')
|
||||
else:
|
||||
return 'master'
|
||||
|
||||
|
||||
def define_env(env):
|
||||
"""Define custom environment variables for the documentation build process."""
|
||||
|
||||
@env.macro
|
||||
def sourcedir(dirname, branch=None):
|
||||
"""Return a link to a directory within the source code repository.
|
||||
|
||||
Arguments:
|
||||
- dirname: The name of the directory to link to (relative to the top-level directory)
|
||||
|
||||
Returns:
|
||||
- A fully qualified URL to the source code directory on GitHub
|
||||
|
||||
Raises:
|
||||
- FileNotFoundError: If the directory does not exist, or the generated URL is invalid
|
||||
"""
|
||||
if branch == None:
|
||||
branch = get_build_enviroment()
|
||||
|
||||
if dirname.startswith('/'):
|
||||
dirname = dirname[1:]
|
||||
|
||||
# This file exists at ./docs/main.py, so any directory we link to must be relative to the top-level directory
|
||||
here = os.path.dirname(__file__)
|
||||
root = os.path.abspath(os.path.join(here, '..'))
|
||||
|
||||
directory = os.path.join(root, dirname)
|
||||
directory = os.path.abspath(directory)
|
||||
|
||||
if not os.path.exists(directory) or not os.path.isdir(directory):
|
||||
raise FileNotFoundError(f'Source directory {dirname} does not exist.')
|
||||
|
||||
repo_url = get_repo_url()
|
||||
|
||||
url = f'{repo_url}/tree/{branch}/{dirname}'
|
||||
|
||||
# Check that the URL exists before returning it
|
||||
if not check_link(url):
|
||||
raise FileNotFoundError(f'URL {url} does not exist.')
|
||||
|
||||
return url
|
||||
|
||||
@env.macro
|
||||
def sourcefile(filename, branch=None, raw=False):
|
||||
"""Return a link to a file within the source code repository.
|
||||
|
||||
Arguments:
|
||||
- filename: The name of the file to link to (relative to the top-level directory)
|
||||
|
||||
Returns:
|
||||
- A fully qualified URL to the source code file on GitHub
|
||||
|
||||
Raises:
|
||||
- FileNotFoundError: If the file does not exist, or the generated URL is invalid
|
||||
"""
|
||||
if branch == None:
|
||||
branch = get_build_enviroment()
|
||||
|
||||
if filename.startswith('/'):
|
||||
filename = filename[1:]
|
||||
|
||||
# This file exists at ./docs/main.py, so any file we link to must be relative to the top-level directory
|
||||
here = os.path.dirname(__file__)
|
||||
root = os.path.abspath(os.path.join(here, '..'))
|
||||
|
||||
file_path = os.path.join(root, filename)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f'Source file {filename} does not exist.')
|
||||
|
||||
repo_url = get_repo_url(raw=raw)
|
||||
|
||||
if raw:
|
||||
url = f'{repo_url}/{branch}/{filename}'
|
||||
else:
|
||||
url = f'{repo_url}/blob/{branch}/{filename}'
|
||||
|
||||
# Check that the URL exists before returning it
|
||||
if not check_link(url):
|
||||
raise FileNotFoundError(f'URL {url} does not exist.')
|
||||
|
||||
return url
|
||||
|
||||
@env.macro
|
||||
def invoke_commands():
|
||||
"""Provides an output of the available commands."""
|
||||
here = os.path.dirname(__file__)
|
||||
base = os.path.join(here, '..')
|
||||
base = os.path.abspath(base)
|
||||
tasks = os.path.join(base, 'tasks.py')
|
||||
output = os.path.join(here, 'invoke-commands.txt')
|
||||
|
||||
command = f'invoke -f {tasks} --list > {output}'
|
||||
|
||||
assert subprocess.call(command, shell=True) == 0
|
||||
|
||||
with open(output, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
return content
|
||||
|
||||
@env.macro
|
||||
def listimages(subdir):
|
||||
"""Return a listing of all asset files in the provided subdir."""
|
||||
|
||||
@@ -1022,8 +1022,12 @@ if SITE_URL:
|
||||
logger.info('Using Site URL: %s', SITE_URL)
|
||||
|
||||
# Check that the site URL is valid
|
||||
validator = URLValidator()
|
||||
validator(SITE_URL)
|
||||
try:
|
||||
validator = URLValidator()
|
||||
validator(SITE_URL)
|
||||
except Exception:
|
||||
print(f"Invalid SITE_URL value: '{SITE_URL}'. InvenTree server cannot start.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Enable or disable multi-site framework
|
||||
SITE_MULTI = get_boolean_setting('INVENTREE_SITE_MULTI', 'site_multi', False)
|
||||
@@ -1322,7 +1326,7 @@ PLUGIN_TESTING_SETUP = get_setting(
|
||||
) # 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', 5
|
||||
'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
|
||||
) # How often should plugin loading be tried?
|
||||
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from dulwich.repo import NotGitRepository, Repo
|
||||
from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
|
||||
|
||||
# InvenTree software version
|
||||
INVENTREE_SW_VERSION = '0.15.0 dev'
|
||||
INVENTREE_SW_VERSION = '0.15.7'
|
||||
|
||||
# Discover git
|
||||
try:
|
||||
@@ -104,7 +104,7 @@ def inventreeDocUrl():
|
||||
|
||||
def inventreeAppUrl():
|
||||
"""Return URL for InvenTree app site."""
|
||||
return f'{inventreeDocUrl()}/app/app/'
|
||||
return f'https://docs.inventree.org/app/'
|
||||
|
||||
|
||||
def inventreeCreditsUrl():
|
||||
|
||||
@@ -237,6 +237,16 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
The Build object is provided to the serializer context.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass."""
|
||||
fields = [
|
||||
'quantity',
|
||||
'batch_code',
|
||||
'serial_numbers',
|
||||
'location',
|
||||
'auto_allocate',
|
||||
]
|
||||
|
||||
quantity = serializers.DecimalField(
|
||||
max_digits=15,
|
||||
decimal_places=5,
|
||||
@@ -639,6 +649,14 @@ class OverallocationChoice():
|
||||
class BuildCompleteSerializer(serializers.Serializer):
|
||||
"""DRF serializer for marking a BuildOrder as complete."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'accept_overallocated',
|
||||
'accept_unallocated',
|
||||
'accept_incomplete',
|
||||
]
|
||||
|
||||
def get_context_data(self):
|
||||
"""Retrieve extra context data for this serializer.
|
||||
|
||||
@@ -732,6 +750,13 @@ class BuildUnallocationSerializer(serializers.Serializer):
|
||||
- bom_item: Filter against a particular BOM line item
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'build_line',
|
||||
'output',
|
||||
]
|
||||
|
||||
build_line = serializers.PrimaryKeyRelatedField(
|
||||
queryset=BuildLine.objects.all(),
|
||||
many=False,
|
||||
|
||||
@@ -271,7 +271,7 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = ([],)
|
||||
fields = []
|
||||
|
||||
def get_context_data(self):
|
||||
"""Return custom context information about the order."""
|
||||
|
||||
@@ -484,6 +484,9 @@ class PurchaseOrderTest(OrderTest):
|
||||
|
||||
url = reverse('api-po-cancel', kwargs={'pk': po.pk})
|
||||
|
||||
# Get an OPTIONS request from the endpoint
|
||||
self.options(url, data={'context': True}, expected_code=200)
|
||||
|
||||
# Try to cancel the PO, but without required permissions
|
||||
self.post(url, {}, expected_code=403)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import decimal
|
||||
import hashlib
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
@@ -3757,6 +3758,12 @@ class PartParameter(InvenTree.models.InvenTreeMetadataModel):
|
||||
except ValueError:
|
||||
self.data_numeric = None
|
||||
|
||||
if self.data_numeric is not None and type(self.data_numeric) is float:
|
||||
# Prevent out of range numbers, etc
|
||||
# Ref: https://github.com/inventree/InvenTree/issues/7593
|
||||
if math.isnan(self.data_numeric) or math.isinf(self.data_numeric):
|
||||
self.data_numeric = None
|
||||
|
||||
part = models.ForeignKey(
|
||||
Part,
|
||||
on_delete=models.CASCADE,
|
||||
|
||||
@@ -47,6 +47,25 @@ class TestParams(TestCase):
|
||||
t3.full_clean()
|
||||
t3.save() # pragma: no cover
|
||||
|
||||
def test_invalid_numbers(self):
|
||||
"""Test that invalid floating point numbers are correctly handled."""
|
||||
p = Part.objects.first()
|
||||
t = PartParameterTemplate.objects.create(name='Yaks')
|
||||
|
||||
valid_floats = ['-12', '1.234', '17', '3e45', '-12e34']
|
||||
|
||||
for value in valid_floats:
|
||||
param = PartParameter(part=p, template=t, data=value)
|
||||
param.full_clean()
|
||||
self.assertIsNotNone(param.data_numeric)
|
||||
|
||||
invalid_floats = ['88E6352', 'inf', '-inf', 'nan', '3.14.15', '3eee3']
|
||||
|
||||
for value in invalid_floats:
|
||||
param = PartParameter(part=p, template=t, data=value)
|
||||
param.full_clean()
|
||||
self.assertIsNone(param.data_numeric)
|
||||
|
||||
def test_metadata(self):
|
||||
"""Unit tests for the metadata field."""
|
||||
for model in [PartParameterTemplate]:
|
||||
|
||||
@@ -60,6 +60,9 @@ def register_event(event, *args, **kwargs):
|
||||
|
||||
# Determine if there are any plugins which are interested in responding
|
||||
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'):
|
||||
# Check if the plugin registry needs to be reloaded
|
||||
registry.check_reload()
|
||||
|
||||
with transaction.atomic():
|
||||
for slug, plugin in registry.plugins.items():
|
||||
if not plugin.mixin_enabled('events'):
|
||||
|
||||
@@ -219,51 +219,15 @@ class PluginsRegistry:
|
||||
"""
|
||||
logger.info('Loading plugins')
|
||||
|
||||
registered_successful = False
|
||||
blocked_plugin = None
|
||||
retry_counter = settings.PLUGIN_RETRY
|
||||
|
||||
while not registered_successful:
|
||||
try:
|
||||
# We are using the db so for migrations etc we need to try this block
|
||||
self._init_plugins(blocked_plugin)
|
||||
self._activate_plugins(full_reload=full_reload)
|
||||
registered_successful = True
|
||||
except (OperationalError, ProgrammingError): # pragma: no cover
|
||||
# Exception if the database has not been migrated yet
|
||||
logger.info('Database not accessible while loading plugins')
|
||||
break
|
||||
except IntegrationPluginError as error:
|
||||
logger.exception(
|
||||
'[PLUGIN] Encountered an error with %s:\n%s',
|
||||
error.path,
|
||||
error.message,
|
||||
)
|
||||
log_error({error.path: error.message}, 'load')
|
||||
blocked_plugin = error.path # we will not try to load this app again
|
||||
|
||||
# Initialize apps without any plugins
|
||||
self._clean_registry()
|
||||
self._clean_installed_apps()
|
||||
self._activate_plugins(force_reload=True, full_reload=full_reload)
|
||||
|
||||
# We do not want to end in an endless loop
|
||||
retry_counter -= 1
|
||||
|
||||
if retry_counter <= 0: # pragma: no cover
|
||||
if settings.PLUGIN_TESTING:
|
||||
print('[PLUGIN] Max retries, breaking loading')
|
||||
break
|
||||
if settings.PLUGIN_TESTING:
|
||||
print(
|
||||
f'[PLUGIN] Above error occurred during testing - {retry_counter}/{settings.PLUGIN_RETRY} retries left'
|
||||
)
|
||||
|
||||
# now the loading will re-start up with init
|
||||
|
||||
# disable full reload after the first round
|
||||
if full_reload:
|
||||
full_reload = False
|
||||
try:
|
||||
self._init_plugins()
|
||||
self._activate_plugins(full_reload=full_reload)
|
||||
except (OperationalError, ProgrammingError, IntegrityError):
|
||||
# Exception if the database has not been migrated yet, or is not ready
|
||||
pass
|
||||
except IntegrationPluginError:
|
||||
# Plugin specific error has already been handled
|
||||
pass
|
||||
|
||||
# ensure plugins_loaded is True
|
||||
self.plugins_loaded = True
|
||||
@@ -478,18 +442,13 @@ class PluginsRegistry:
|
||||
# endregion
|
||||
|
||||
# region general internal loading / activating / deactivating / unloading
|
||||
def _init_plugins(self, disabled: str = None):
|
||||
"""Initialise all found plugins.
|
||||
def _init_plugin(self, plugin, configs: dict):
|
||||
"""Initialise a single plugin.
|
||||
|
||||
Args:
|
||||
disabled (str, optional): Loading path of disabled app. Defaults to None.
|
||||
|
||||
Raises:
|
||||
error: IntegrationPluginError
|
||||
plugin: Plugin module
|
||||
"""
|
||||
# Imports need to be in this level to prevent early db model imports
|
||||
from InvenTree import version
|
||||
from plugin.models import PluginConfig
|
||||
|
||||
def safe_reference(plugin, key: str, active: bool = True):
|
||||
"""Safe reference to plugin dicts."""
|
||||
@@ -503,6 +462,99 @@ class PluginsRegistry:
|
||||
self.plugins_inactive[key] = plugin.db
|
||||
self.plugins_full[key] = plugin
|
||||
|
||||
# These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
|
||||
plg_name = plugin.NAME
|
||||
plg_key = slugify(
|
||||
plugin.SLUG if getattr(plugin, 'SLUG', None) else plg_name
|
||||
) # keys are slugs!
|
||||
|
||||
logger.info("Loading plugin '%s'", plg_key)
|
||||
|
||||
if plg_key in configs:
|
||||
plg_db = configs[plg_key]
|
||||
else:
|
||||
plg_db = self.get_plugin_config(plg_key, plg_name)
|
||||
|
||||
plugin.db = plg_db
|
||||
|
||||
# Check if this is a 'builtin' plugin
|
||||
builtin = plugin.check_is_builtin()
|
||||
|
||||
package_name = None
|
||||
|
||||
# Extract plugin package name
|
||||
if getattr(plugin, 'is_package', False):
|
||||
package_name = getattr(plugin, 'package_name', None)
|
||||
|
||||
# Auto-enable builtin plugins
|
||||
if builtin and plg_db and not plg_db.active:
|
||||
plg_db.active = True
|
||||
plg_db.save()
|
||||
|
||||
# Save the package_name attribute to the plugin
|
||||
if plg_db.package_name != package_name:
|
||||
plg_db.package_name = package_name
|
||||
plg_db.save()
|
||||
|
||||
# Determine if this plugin should be loaded:
|
||||
# - If PLUGIN_TESTING is enabled
|
||||
# - If this is a 'builtin' plugin
|
||||
# - If this plugin has been explicitly enabled by the user
|
||||
if settings.PLUGIN_TESTING or builtin or (plg_db and plg_db.active):
|
||||
# Initialize package - we can be sure that an admin has activated the plugin
|
||||
logger.debug('Loading plugin `%s`', plg_name)
|
||||
|
||||
try:
|
||||
t_start = time.time()
|
||||
plg_i: InvenTreePlugin = plugin()
|
||||
dt = time.time() - t_start
|
||||
logger.debug('Loaded plugin `%s` in %.3fs', plg_name, dt)
|
||||
except Exception as error:
|
||||
handle_error(
|
||||
error, log_name='init'
|
||||
) # log error and raise it -> disable plugin
|
||||
|
||||
logger.warning('Plugin `%s` could not be loaded', plg_name)
|
||||
|
||||
# Safe extra attributes
|
||||
plg_i.is_package = getattr(plg_i, 'is_package', False)
|
||||
|
||||
plg_i.pk = plg_db.pk if plg_db else None
|
||||
plg_i.db = plg_db
|
||||
|
||||
# Run version check for plugin
|
||||
if (plg_i.MIN_VERSION or plg_i.MAX_VERSION) and not plg_i.check_version():
|
||||
# Disable plugin
|
||||
safe_reference(plugin=plg_i, key=plg_key, active=False)
|
||||
|
||||
p = plg_name
|
||||
v = version.inventreeVersion()
|
||||
_msg = _(
|
||||
f"Plugin '{p}' is not compatible with the current InvenTree version {v}"
|
||||
)
|
||||
if v := plg_i.MIN_VERSION:
|
||||
_msg += _(f'Plugin requires at least version {v}')
|
||||
if v := plg_i.MAX_VERSION:
|
||||
_msg += _(f'Plugin requires at most version {v}')
|
||||
# Log to error stack
|
||||
log_error(_msg, reference='init')
|
||||
else:
|
||||
safe_reference(plugin=plg_i, key=plg_key)
|
||||
else: # pragma: no cover
|
||||
safe_reference(plugin=plugin, key=plg_key, active=False)
|
||||
|
||||
def _init_plugins(self):
|
||||
"""Initialise all found plugins.
|
||||
|
||||
Args:
|
||||
disabled (str, optional): Loading path of disabled app. Defaults to None.
|
||||
|
||||
Raises:
|
||||
error: IntegrationPluginError
|
||||
"""
|
||||
# Imports need to be in this level to prevent early db model imports
|
||||
from plugin.models import PluginConfig
|
||||
|
||||
logger.debug('Starting plugin initialization')
|
||||
|
||||
# Fetch and cache list of existing plugin configuration instances
|
||||
@@ -510,102 +562,32 @@ class PluginsRegistry:
|
||||
|
||||
# Initialize plugins
|
||||
for plg in self.plugin_modules:
|
||||
# These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
|
||||
plg_name = plg.NAME
|
||||
plg_key = slugify(
|
||||
plg.SLUG if getattr(plg, 'SLUG', None) else plg_name
|
||||
) # keys are slugs!
|
||||
# Attempt to load each individual plugin
|
||||
attempts = settings.PLUGIN_RETRY
|
||||
|
||||
try:
|
||||
if plg_key in plugin_configs:
|
||||
# Configuration already exists
|
||||
plg_db = plugin_configs[plg_key]
|
||||
else:
|
||||
# Configuration needs to be created
|
||||
plg_db = self.get_plugin_config(plg_key, plg_name)
|
||||
except (OperationalError, ProgrammingError) as error:
|
||||
# Exception if the database has not been migrated yet - check if test are running - raise if not
|
||||
if not settings.PLUGIN_TESTING:
|
||||
raise error # pragma: no cover
|
||||
plg_db = None
|
||||
except IntegrityError as error: # pragma: no cover
|
||||
logger.exception('Error initializing plugin `%s`: %s', plg_name, error)
|
||||
handle_error(error, log_name='init')
|
||||
|
||||
# Append reference to plugin
|
||||
plg.db = plg_db
|
||||
|
||||
# Check if this is a 'builtin' plugin
|
||||
builtin = plg.check_is_builtin()
|
||||
|
||||
package_name = None
|
||||
|
||||
# Extract plugin package name
|
||||
if getattr(plg, 'is_package', False):
|
||||
package_name = getattr(plg, 'package_name', None)
|
||||
|
||||
# Auto-enable builtin plugins
|
||||
if builtin and plg_db and not plg_db.active:
|
||||
plg_db.active = True
|
||||
plg_db.save()
|
||||
|
||||
# Save the package_name attribute to the plugin
|
||||
if plg_db.package_name != package_name:
|
||||
plg_db.package_name = package_name
|
||||
plg_db.save()
|
||||
|
||||
# Determine if this plugin should be loaded:
|
||||
# - If PLUGIN_TESTING is enabled
|
||||
# - If this is a 'builtin' plugin
|
||||
# - If this plugin has been explicitly enabled by the user
|
||||
if settings.PLUGIN_TESTING or builtin or (plg_db and plg_db.active):
|
||||
# Check if the plugin was blocked -> threw an error; option1: package, option2: file-based
|
||||
if disabled and disabled in (plg.__name__, plg.__module__):
|
||||
safe_reference(plugin=plg, key=plg_key, active=False)
|
||||
continue # continue -> the plugin is not loaded
|
||||
|
||||
# Initialize package - we can be sure that an admin has activated the plugin
|
||||
logger.debug('Loading plugin `%s`', plg_name)
|
||||
while attempts > 0:
|
||||
attempts -= 1
|
||||
|
||||
try:
|
||||
t_start = time.time()
|
||||
plg_i: InvenTreePlugin = plg()
|
||||
dt = time.time() - t_start
|
||||
logger.debug('Loaded plugin `%s` in %.3fs', plg_name, dt)
|
||||
self._init_plugin(plg, plugin_configs)
|
||||
break
|
||||
except IntegrationPluginError as error:
|
||||
# Error has been handled downstream
|
||||
pass
|
||||
except Exception as error:
|
||||
# Handle the error, log it and try again
|
||||
handle_error(
|
||||
error, log_name='init'
|
||||
) # log error and raise it -> disable plugin
|
||||
logger.warning('Plugin `%s` could not be loaded', plg_name)
|
||||
|
||||
# Safe extra attributes
|
||||
plg_i.is_package = getattr(plg_i, 'is_package', False)
|
||||
|
||||
plg_i.pk = plg_db.pk if plg_db else None
|
||||
plg_i.db = plg_db
|
||||
|
||||
# Run version check for plugin
|
||||
if (
|
||||
plg_i.MIN_VERSION or plg_i.MAX_VERSION
|
||||
) and not plg_i.check_version():
|
||||
# Disable plugin
|
||||
safe_reference(plugin=plg_i, key=plg_key, active=False)
|
||||
|
||||
p = plg_name
|
||||
v = version.inventreeVersion()
|
||||
_msg = _(
|
||||
f"Plugin '{p}' is not compatible with the current InvenTree version {v}"
|
||||
error, log_name='init', do_raise=settings.PLUGIN_TESTING
|
||||
)
|
||||
if v := plg_i.MIN_VERSION:
|
||||
_msg += _(f'Plugin requires at least version {v}')
|
||||
if v := plg_i.MAX_VERSION:
|
||||
_msg += _(f'Plugin requires at most version {v}')
|
||||
# Log to error stack
|
||||
log_error(_msg, reference='init')
|
||||
else:
|
||||
safe_reference(plugin=plg_i, key=plg_key)
|
||||
else: # pragma: no cover
|
||||
safe_reference(plugin=plg, key=plg_key, active=False)
|
||||
|
||||
if attempts == 0:
|
||||
logger.exception(
|
||||
'[PLUGIN] Encountered an error with %s:\n%s',
|
||||
error.path,
|
||||
str(error),
|
||||
)
|
||||
|
||||
logger.debug('Finished plugin initialization')
|
||||
|
||||
def __get_mixin_order(self):
|
||||
"""Returns a list of mixin classes, in the order that they should be activated."""
|
||||
|
||||
@@ -273,15 +273,17 @@ class RegistryTests(TestCase):
|
||||
# Reload to rediscover plugins
|
||||
registry.reload_plugins(full_reload=True, collect=True)
|
||||
|
||||
self.assertEqual(len(registry.errors), 3)
|
||||
self.assertEqual(len(registry.errors), 2)
|
||||
|
||||
# There should be at least one discovery error in the module `broken_file`
|
||||
self.assertGreater(len(registry.errors.get('discovery')), 0)
|
||||
self.assertEqual(
|
||||
registry.errors.get('discovery')[0]['broken_file'],
|
||||
"name 'bb' is not defined",
|
||||
)
|
||||
|
||||
# There should be at least one load error with an intentional KeyError
|
||||
self.assertGreater(len(registry.errors.get('load')), 0)
|
||||
self.assertGreater(len(registry.errors.get('init')), 0)
|
||||
self.assertEqual(
|
||||
registry.errors.get('load')[0]['broken_sample'], "'This is a dummy error'"
|
||||
registry.errors.get('init')[0]['broken_sample'], "'This is a dummy error'"
|
||||
)
|
||||
|
||||
@@ -170,6 +170,18 @@ def uploaded_image(
|
||||
width = kwargs.get('width', None)
|
||||
height = kwargs.get('height', None)
|
||||
|
||||
if width is not None:
|
||||
try:
|
||||
width = int(width)
|
||||
except ValueError:
|
||||
width = None
|
||||
|
||||
if height is not None:
|
||||
try:
|
||||
height = int(height)
|
||||
except ValueError:
|
||||
height = None
|
||||
|
||||
if width is not None and height is not None:
|
||||
# Resize the image, width *and* height are provided
|
||||
img = img.resize((width, height))
|
||||
@@ -185,10 +197,12 @@ def uploaded_image(
|
||||
img = img.resize((wsize, height))
|
||||
|
||||
# Optionally rotate the image
|
||||
rotate = kwargs.get('rotate', None)
|
||||
|
||||
if rotate is not None:
|
||||
img = img.rotate(rotate)
|
||||
if rotate := kwargs.get('rotate', None):
|
||||
try:
|
||||
rotate = int(rotate)
|
||||
img = img.rotate(rotate)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Return a base-64 encoded image
|
||||
img_data = report.helpers.encode_image_base64(img)
|
||||
|
||||
@@ -229,17 +229,17 @@ class StockItemResource(InvenTreeResource):
|
||||
is_building = Field(
|
||||
attribute='is_building',
|
||||
column_name=_('Building'),
|
||||
widget=widgets.IntegerWidget(),
|
||||
widget=widgets.BooleanWidget(),
|
||||
)
|
||||
review_needed = Field(
|
||||
attribute='review_needed',
|
||||
column_name=_('Review Needed'),
|
||||
widget=widgets.IntegerWidget(),
|
||||
widget=widgets.BooleanWidget(),
|
||||
)
|
||||
delete_on_deplete = Field(
|
||||
attribute='delete_on_deplete',
|
||||
column_name=_('Delete on Deplete'),
|
||||
widget=widgets.IntegerWidget(),
|
||||
widget=widgets.BooleanWidget(),
|
||||
)
|
||||
|
||||
# Date management
|
||||
|
||||
@@ -1402,6 +1402,22 @@ class StockTrackingList(ListAPI):
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_delta_model_map(self) -> dict:
|
||||
"""Return a mapping of delta models to their respective models and serializers.
|
||||
|
||||
This is used to generate additional context information for the historical data,
|
||||
with some attempt at caching so that we can reduce the number of database hits.
|
||||
"""
|
||||
return {
|
||||
'part': (Part, PartBriefSerializer),
|
||||
'location': (StockLocation, StockSerializers.LocationSerializer),
|
||||
'customer': (Company, CompanySerializer),
|
||||
'purchaseorder': (PurchaseOrder, PurchaseOrderSerializer),
|
||||
'salesorder': (SalesOrder, SalesOrderSerializer),
|
||||
'returnorder': (ReturnOrder, ReturnOrderSerializer),
|
||||
'buildorder': (Build, BuildSerializer),
|
||||
}
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""List all stock tracking entries."""
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
@@ -1415,84 +1431,36 @@ class StockTrackingList(ListAPI):
|
||||
|
||||
data = serializer.data
|
||||
|
||||
# Attempt to add extra context information to the historical data
|
||||
delta_models = self.get_delta_model_map()
|
||||
|
||||
# Construct a set of related models we need to lookup for later
|
||||
related_model_lookups = {key: set() for key in delta_models.keys()}
|
||||
|
||||
# Run a first pass through the data to determine which related models we need to lookup
|
||||
for item in data:
|
||||
deltas = item['deltas']
|
||||
|
||||
if not deltas:
|
||||
deltas = {}
|
||||
for key in delta_models.keys():
|
||||
if key in deltas:
|
||||
related_model_lookups[key].add(deltas[key])
|
||||
|
||||
# Add part detail
|
||||
if 'part' in deltas:
|
||||
try:
|
||||
part = Part.objects.get(pk=deltas['part'])
|
||||
serializer = PartBriefSerializer(part)
|
||||
deltas['part_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
for key in delta_models.keys():
|
||||
model, serializer = delta_models[key]
|
||||
|
||||
# Add location detail
|
||||
if 'location' in deltas:
|
||||
try:
|
||||
location = StockLocation.objects.get(pk=deltas['location'])
|
||||
serializer = StockSerializers.LocationSerializer(location)
|
||||
deltas['location_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
# Fetch all related models in one go
|
||||
related_models = model.objects.filter(pk__in=related_model_lookups[key])
|
||||
|
||||
# Add stockitem detail
|
||||
if 'stockitem' in deltas:
|
||||
try:
|
||||
stockitem = StockItem.objects.get(pk=deltas['stockitem'])
|
||||
serializer = StockSerializers.StockItemSerializer(stockitem)
|
||||
deltas['stockitem_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
# Construct a mapping of pk -> serialized data
|
||||
related_data = {obj.pk: serializer(obj).data for obj in related_models}
|
||||
|
||||
# Add customer detail
|
||||
if 'customer' in deltas:
|
||||
try:
|
||||
customer = Company.objects.get(pk=deltas['customer'])
|
||||
serializer = CompanySerializer(customer)
|
||||
deltas['customer_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
# Now, update the data with the serialized data
|
||||
for item in data:
|
||||
deltas = item['deltas']
|
||||
|
||||
# Add PurchaseOrder detail
|
||||
if 'purchaseorder' in deltas:
|
||||
try:
|
||||
order = PurchaseOrder.objects.get(pk=deltas['purchaseorder'])
|
||||
serializer = PurchaseOrderSerializer(order)
|
||||
deltas['purchaseorder_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add SalesOrder detail
|
||||
if 'salesorder' in deltas:
|
||||
try:
|
||||
order = SalesOrder.objects.get(pk=deltas['salesorder'])
|
||||
serializer = SalesOrderSerializer(order)
|
||||
deltas['salesorder_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add ReturnOrder detail
|
||||
if 'returnorder' in deltas:
|
||||
try:
|
||||
order = ReturnOrder.objects.get(pk=deltas['returnorder'])
|
||||
serializer = ReturnOrderSerializer(order)
|
||||
deltas['returnorder_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add BuildOrder detail
|
||||
if 'buildorder' in deltas:
|
||||
try:
|
||||
order = Build.objects.get(pk=deltas['buildorder'])
|
||||
serializer = BuildSerializer(order)
|
||||
deltas['buildorder_detail'] = serializer.data
|
||||
except Exception:
|
||||
pass
|
||||
if key in deltas:
|
||||
item['deltas'][f'{key}_detail'] = related_data.get(
|
||||
deltas[key], None
|
||||
)
|
||||
|
||||
if page is not None:
|
||||
return self.get_paginated_response(data)
|
||||
|
||||
@@ -1678,6 +1678,9 @@ class StockItem(
|
||||
- Tracking history for the *other* item is deleted
|
||||
- Any allocations (build order, sales order) are moved to this StockItem
|
||||
"""
|
||||
if isinstance(other_items, StockItem):
|
||||
other_items = [other_items]
|
||||
|
||||
if len(other_items) == 0:
|
||||
return
|
||||
|
||||
@@ -1685,7 +1688,7 @@ class StockItem(
|
||||
tree_ids = {self.tree_id}
|
||||
|
||||
user = kwargs.get('user', None)
|
||||
location = kwargs.get('location', None)
|
||||
location = kwargs.get('location', self.location)
|
||||
notes = kwargs.get('notes', None)
|
||||
|
||||
parent_id = self.parent.pk if self.parent else None
|
||||
@@ -1693,6 +1696,9 @@ class StockItem(
|
||||
for other in other_items:
|
||||
# If the stock item cannot be merged, return
|
||||
if not self.can_merge(other, raise_error=raise_error, **kwargs):
|
||||
logger.warning(
|
||||
'Stock item <%s> could not be merge into <%s>', other.pk, self.pk
|
||||
)
|
||||
return
|
||||
|
||||
tree_ids.add(other.tree_id)
|
||||
@@ -1722,7 +1728,7 @@ class StockItem(
|
||||
user,
|
||||
quantity=self.quantity,
|
||||
notes=notes,
|
||||
deltas={'location': location.pk},
|
||||
deltas={'location': location.pk if location else None},
|
||||
)
|
||||
|
||||
self.location = location
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
|
||||
{% settings_value "TEST_STATION_DATA" as test_station_fields %}
|
||||
|
||||
{% if item.part.trackable %}
|
||||
loadStockTestResultsTable(
|
||||
$("#test-result-table"), {
|
||||
part: {{ item.part.id }},
|
||||
@@ -248,6 +249,7 @@
|
||||
url: '{% url "api-stockitem-testreport-list" %}',
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_staff %}
|
||||
$("#delete-test-results").click(function() {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% for line in lines %}
|
||||
<tr style="height: 2.5rem; border-bottom: 1px solid">
|
||||
<td style='padding-left: 1em;'>
|
||||
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if part.description %} - <em>{{ part.description }}</em>{% endif %}
|
||||
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if line.part.description %} - <em>{{ line.part.description }}</em>{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
{% decimal line.required %} {% include "part/part_units.html" with part=line.part %}
|
||||
|
||||
@@ -298,7 +298,8 @@ function constructDeleteForm(fields, options) {
|
||||
* - closeText: Text for the "close" button
|
||||
* - fields: list of fields to display, with the following options
|
||||
* - filters: API query filters
|
||||
* - onEdit: callback or array of callbacks which get fired when field is edited
|
||||
* - onEdit: callback or array of callbacks which get fired when field is edited - does not get triggered until the field loses focus, ref: https://api.jquery.com/change/
|
||||
* - onInput: callback or array of callbacks which get fired when an input is detected in the field
|
||||
* - secondary: Define a secondary modal form for this field
|
||||
* - label: Specify custom label
|
||||
* - help_text: Specify custom help_text
|
||||
@@ -1642,6 +1643,23 @@ function addFieldCallback(name, field, options) {
|
||||
});
|
||||
}
|
||||
|
||||
if(field.onInput){
|
||||
|
||||
el.on('input', function(){
|
||||
var value = getFormFieldValue(name, field, options);
|
||||
let onInputHandlers = field.onInput;
|
||||
|
||||
if (!Array.isArray(onInputHandlers)) {
|
||||
onInputHandlers = [onInputHandlers];
|
||||
}
|
||||
|
||||
for (const onInput of onInputHandlers) {
|
||||
onInput(value, name, field, options);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// attach field callback for nested fields
|
||||
if(field.type === "nested object") {
|
||||
for (const [c_name, c_field] of Object.entries(field.children)) {
|
||||
|
||||
@@ -343,7 +343,7 @@ function poLineItemFields(options={}) {
|
||||
reference: {},
|
||||
purchase_price: {
|
||||
icon: 'fa-dollar-sign',
|
||||
onEdit: function(value, name, field, opts) {
|
||||
onInput: function(value, name, field, opts) {
|
||||
updateFieldValue('auto_pricing', value === '', {}, opts);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user, login
|
||||
from django.contrib.auth import get_user, login, logout
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.urls import include, path, re_path
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from allauth.account.adapter import get_adapter
|
||||
from dj_rest_auth.views import LoginView, LogoutView
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
|
||||
from rest_framework import exceptions, permissions
|
||||
@@ -17,6 +18,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
import InvenTree.helpers
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import (
|
||||
ListAPI,
|
||||
@@ -216,7 +218,22 @@ class GroupList(ListCreateAPI):
|
||||
class Login(LoginView):
|
||||
"""API view for logging in via API."""
|
||||
|
||||
...
|
||||
def process_login(self):
|
||||
"""Process the login request, ensure that MFA is enforced if required."""
|
||||
# Normal login process
|
||||
ret = super().process_login()
|
||||
|
||||
# Now check if MFA is enforced
|
||||
user = self.request.user
|
||||
adapter = get_adapter(self.request)
|
||||
|
||||
# User requires 2FA or MFA is enforced globally - no logins via API
|
||||
if adapter.has_2fa_enabled(user) or InvenTreeSetting.get_setting(
|
||||
'LOGIN_ENFORCE_MFA'
|
||||
):
|
||||
logout(self.request)
|
||||
raise exceptions.PermissionDenied('MFA required for this user')
|
||||
return ret
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.2.12 on 2024-05-23 16:40
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def clear_sessions(apps, schema_editor):
|
||||
"""Clear all user sessions."""
|
||||
|
||||
try:
|
||||
engine = import_module(settings.SESSION_ENGINE)
|
||||
engine.SessionStore.clear_expired()
|
||||
print('Cleared all user sessions to deal with GHSA-2crp-q9pc-457j')
|
||||
except Exception:
|
||||
# Database may not be ready yet, so this does not matter anyhow
|
||||
pass
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("users", "0010_alter_apitoken_key"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
clear_sessions, reverse_code=migrations.RunPython.noop,
|
||||
)
|
||||
]
|
||||
@@ -253,9 +253,9 @@ distlib==0.3.8 \
|
||||
--hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \
|
||||
--hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64
|
||||
# via virtualenv
|
||||
django==4.2.12 \
|
||||
--hash=sha256:6a6b4aff8a2db2dc7dcc5650cb2c7a7a0d1eb38e2aa2335fdf001e41801e9797 \
|
||||
--hash=sha256:7640e86835d44ae118c2916a803d8081f40e214ee18a5a92a0202994ca60a4b4
|
||||
django==4.2.14 \
|
||||
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
|
||||
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
|
||||
# via
|
||||
# django-admin-shell
|
||||
# django-slowtests
|
||||
|
||||
@@ -329,9 +329,9 @@ diff-match-patch==20230430 \
|
||||
# via django-import-export
|
||||
dj-rest-auth==6.0.0 \
|
||||
--hash=sha256:760b45f3a07cd6182e6a20fe07d0c55230c5f950167df724d7914d0dd8c50133
|
||||
django==4.2.12 \
|
||||
--hash=sha256:6a6b4aff8a2db2dc7dcc5650cb2c7a7a0d1eb38e2aa2335fdf001e41801e9797 \
|
||||
--hash=sha256:7640e86835d44ae118c2916a803d8081f40e214ee18a5a92a0202994ca60a4b4
|
||||
django==4.2.14 \
|
||||
--hash=sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240 \
|
||||
--hash=sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96
|
||||
# via
|
||||
# dj-rest-auth
|
||||
# django-allauth
|
||||
|
||||
@@ -36,7 +36,7 @@ if (IS_DEV_OR_DEMO) {
|
||||
}
|
||||
|
||||
export const docLinks = {
|
||||
app: 'https://docs.inventree.org/en/latest/app/app/',
|
||||
app: 'https://docs.inventree.org/app/',
|
||||
getting_started: 'https://docs.inventree.org/en/latest/getting_started/',
|
||||
api: 'https://docs.inventree.org/en/latest/api/api/',
|
||||
developer: 'https://docs.inventree.org/en/latest/develop/starting/',
|
||||
|
||||
100
tasks.py
100
tasks.py
@@ -230,7 +230,12 @@ def plugins(c, uv=False):
|
||||
@task(help={'uv': 'Use UV package manager (experimental)'})
|
||||
def install(c, uv=False):
|
||||
"""Installs required python packages."""
|
||||
print("Installing required python packages from 'src/backend/requirements.txt'")
|
||||
INSTALL_FILE = 'src/backend/requirements.txt'
|
||||
|
||||
print(f"Installing required python packages from '{INSTALL_FILE}'")
|
||||
|
||||
if not Path(INSTALL_FILE).is_file():
|
||||
raise FileNotFoundError(f"Requirements file '{INSTALL_FILE}' not found")
|
||||
|
||||
# Install required Python packages with PIP
|
||||
if not uv:
|
||||
@@ -238,13 +243,13 @@ def install(c, uv=False):
|
||||
'pip3 install --no-cache-dir --disable-pip-version-check -U pip setuptools'
|
||||
)
|
||||
c.run(
|
||||
'pip3 install --no-cache-dir --disable-pip-version-check -U --require-hashes -r src/backend/requirements.txt'
|
||||
f'pip3 install --no-cache-dir --disable-pip-version-check -U --require-hashes -r {INSTALL_FILE}'
|
||||
)
|
||||
else:
|
||||
c.run(
|
||||
'pip3 install --no-cache-dir --disable-pip-version-check -U uv setuptools'
|
||||
)
|
||||
c.run('uv pip install -U --require-hashes -r src/backend/requirements.txt')
|
||||
c.run(f'uv pip install -U --require-hashes -r {INSTALL_FILE}')
|
||||
|
||||
# Run plugins install
|
||||
plugins(c, uv=uv)
|
||||
@@ -403,7 +408,7 @@ def restore(
|
||||
ignore_database=False,
|
||||
):
|
||||
"""Restore the database and media files."""
|
||||
base_cmd = '--no-input --uncompress -v 2'
|
||||
base_cmd = '--noinput --uncompress -v 2'
|
||||
|
||||
if path:
|
||||
base_cmd += f' -I {path}'
|
||||
@@ -449,6 +454,12 @@ def migrate(c):
|
||||
print('InvenTree database migrations completed!')
|
||||
|
||||
|
||||
@task(help={'app': 'Specify an app to show migrations for (leave blank for all apps)'})
|
||||
def showmigrations(c, app=''):
|
||||
"""Show the migration status of the database."""
|
||||
manage(c, f'showmigrations {app}', pty=True)
|
||||
|
||||
|
||||
@task(
|
||||
post=[clean_settings, translate_stats],
|
||||
help={
|
||||
@@ -759,18 +770,31 @@ def wait(c):
|
||||
return manage(c, 'wait_for_db')
|
||||
|
||||
|
||||
@task(pre=[wait], help={'address': 'Server address:port (default=0.0.0.0:8000)'})
|
||||
def gunicorn(c, address='0.0.0.0:8000'):
|
||||
@task(
|
||||
pre=[wait],
|
||||
help={
|
||||
'address': 'Server address:port (default=0.0.0.0:8000)',
|
||||
'workers': 'Specify number of worker threads (override config file)',
|
||||
},
|
||||
)
|
||||
def gunicorn(c, address='0.0.0.0:8000', workers=None):
|
||||
"""Launch a gunicorn webserver.
|
||||
|
||||
Note: This server will not auto-reload in response to code changes.
|
||||
"""
|
||||
c.run(
|
||||
'gunicorn -c ./docker/gunicorn.conf.py InvenTree.wsgi -b {address} --chdir ./InvenTree'.format(
|
||||
address=address
|
||||
),
|
||||
pty=True,
|
||||
)
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
config_file = os.path.join(here, 'contrib', 'container', 'gunicorn.conf.py')
|
||||
chdir = os.path.join(here, 'src', 'backend', 'InvenTree')
|
||||
|
||||
cmd = f'gunicorn -c {config_file} InvenTree.wsgi -b {address} --chdir {chdir}'
|
||||
|
||||
if workers:
|
||||
cmd += f' --workers={workers}'
|
||||
|
||||
print('Starting Gunicorn Server:')
|
||||
print(cmd)
|
||||
|
||||
c.run(cmd, pty=True)
|
||||
|
||||
|
||||
@task(pre=[wait], help={'address': 'Server address:port (default=127.0.0.1:8000)'})
|
||||
@@ -1197,6 +1221,34 @@ def frontend_download(
|
||||
|
||||
handle_extract(dst.name)
|
||||
|
||||
def check_already_current(tag=None, sha=None):
|
||||
"""Check if the currently available frontend is already the requested one."""
|
||||
ref = 'tag' if tag else 'commit'
|
||||
|
||||
if tag:
|
||||
current = managePyDir().joinpath('web', 'static', 'web', '.vite', 'tag.txt')
|
||||
elif sha:
|
||||
current = managePyDir().joinpath('web', 'static', 'web', '.vite', 'sha.txt')
|
||||
else:
|
||||
raise ValueError('Either tag or sha needs to be set')
|
||||
|
||||
if not current.exists():
|
||||
print(
|
||||
f'Current frontend information for {ref} is not available - this is expected in some cases'
|
||||
)
|
||||
return False
|
||||
|
||||
current_content = current.read_text().strip()
|
||||
ref_value = tag or sha
|
||||
if current_content == ref_value:
|
||||
print(f'Frontend {ref} is already `{ref_value}`')
|
||||
return True
|
||||
else:
|
||||
print(
|
||||
f'Frontend {ref} is not expected `{ref_value}` but `{current_content}` - new version will be downloaded'
|
||||
)
|
||||
return False
|
||||
|
||||
# if zip file is specified, try to extract it directly
|
||||
if file:
|
||||
handle_extract(file)
|
||||
@@ -1213,8 +1265,24 @@ def frontend_download(
|
||||
['git', 'rev-parse', 'HEAD'], encoding='utf-8'
|
||||
).strip()
|
||||
except Exception:
|
||||
print("[ERROR] Cannot get current ref via 'git rev-parse HEAD'")
|
||||
return
|
||||
# .deb Packages contain extra information in the VERSION file
|
||||
version_file = localDir().joinpath('VERSION')
|
||||
if not version_file.exists():
|
||||
return
|
||||
from dotenv import dotenv_values # noqa: WPS433
|
||||
|
||||
content = dotenv_values(version_file)
|
||||
if (
|
||||
'INVENTREE_PKG_INSTALLER' in content
|
||||
and content['INVENTREE_PKG_INSTALLER'] == 'PKG'
|
||||
):
|
||||
ref = content.get('INVENTREE_COMMIT_SHA')
|
||||
print(
|
||||
f'[INFO] Running in package environment, got commit "{ref}" from VERSION file'
|
||||
)
|
||||
else:
|
||||
print("[ERROR] Cannot get current ref via 'git rev-parse HEAD'")
|
||||
return
|
||||
|
||||
if ref is None and tag is None:
|
||||
print('[ERROR] Either ref or tag needs to be set.')
|
||||
@@ -1222,6 +1290,8 @@ def frontend_download(
|
||||
if tag:
|
||||
tag = tag.lstrip('v')
|
||||
try:
|
||||
if check_already_current(tag=tag):
|
||||
return
|
||||
handle_download(
|
||||
f'https://github.com/{repo}/releases/download/{tag}/frontend-build.zip'
|
||||
)
|
||||
@@ -1237,6 +1307,8 @@ Then try continuing by running: invoke frontend-download --file <path-to-downloa
|
||||
return
|
||||
|
||||
if ref:
|
||||
if check_already_current(sha=ref):
|
||||
return
|
||||
# get workflow run from all workflow runs on that particular ref
|
||||
workflow_runs = requests.get(
|
||||
f'https://api.github.com/repos/{repo}/actions/runs?head_sha={ref}',
|
||||
|
||||
Reference in New Issue
Block a user