diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml index 1d030f1ef4..17f48dbf79 100644 --- a/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -35,12 +35,12 @@ runs: using: 'composite' steps: - name: Checkout Code - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 # Python installs - name: Set up Python ${{ env.python_version }} if: ${{ inputs.python == 'true' }} - uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1 with: python-version: ${{ env.python_version }} cache: pip @@ -58,7 +58,7 @@ runs: # NPM installs - name: Install node.js ${{ env.node_version }} if: ${{ inputs.npm == 'true' }} - uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b # pin to v3.5.0 + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # pin to v3.8.2 with: node-version: ${{ env.node_version }} cache: 'npm' diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d44a811465..cd97cccf9f 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -7,7 +7,7 @@ name: Backport on: pull_request_target: - types: ["labeled", "closed"] + types: [ "labeled", "closed" ] jobs: backport: @@ -22,7 +22,7 @@ jobs: ) steps: - name: Backport Action - uses: sqren/backport-github-action@v8.9.3 + uses: sqren/backport-github-action@f54e19901f2a57f8b82360f2490d47ee82ec82c6 # pin@v9.2.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} auto_backport_label_prefix: backport-to- diff --git a/.github/workflows/check_translations.yaml b/.github/workflows/check_translations.yaml index b408ab5d74..37990fa230 100644 --- a/.github/workflows/check_translations.yaml +++ b/.github/workflows/check_translations.yaml @@ -25,9 +25,9 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Set Up Python ${{ env.python_version }} - uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1 with: python-version: ${{ env.python_version }} cache: 'pip' diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 54c8098ac0..7d29563ae9 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -20,7 +20,6 @@ on: push: branches: - 'master' - # pull_request: # branches: # - 'master' @@ -39,9 +38,9 @@ jobs: python_version: 3.9 steps: - name: Check out repo - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Set Up Python ${{ env.python_version }} - uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1 with: python-version: ${{ env.python_version }} - name: Version Check @@ -82,23 +81,23 @@ jobs: docker-compose down - name: Set up QEMU if: github.event_name != 'pull_request' - uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # pin@v2.1.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # pin@v3.0.0 - name: Set up Docker Buildx if: github.event_name != 'pull_request' - uses: docker/setup-buildx-action@95cb08cb2672c73d4ffd2f422e6d11953d2a9c70 # pin@v2.1.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # pin@v3.0.0 - name: Set up cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@7cc35d7fdbe70d4278a0c96779081e6fac665f88 # pin@v2.8.0 + uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # pin@v3.1.2 - name: Login to Dockerhub if: github.event_name != 'pull_request' - uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # pin@v2.1.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # pin@v3.0.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log into registry ghcr.io if: github.event_name != 'pull_request' - uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # pin@v2 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # pin@v3.0.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -107,7 +106,7 @@ jobs: - name: Extract Docker metadata if: github.event_name != 'pull_request' id: meta - uses: docker/metadata-action@12cce9efe0d49980455aaaca9b071c0befcdd702 # pin@v4.1.0 + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # pin@v5.0.0 with: images: | inventree/inventree @@ -116,7 +115,7 @@ jobs: - name: Build and Push id: build-and-push if: github.event_name != 'pull_request' - uses: docker/build-push-action@c56af957549030174b10d6867f20e78cfd7debc5 # pin@v3.2.0 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # pin@v5.0.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -133,5 +132,4 @@ jobs: if: ${{ false }} # github.event_name != 'pull_request' env: COSIGN_EXPERIMENTAL: "true" - run: cosign sign ${{ steps.meta.outputs.tags }}@${{ - steps.build-and-push.outputs.digest }} + run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }} diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 896eebd00d..20615eb8b7 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -4,9 +4,9 @@ name: QC on: push: - branches-ignore: ['l10*'] + branches-ignore: [ 'l10*' ] pull_request: - branches-ignore: ['l10*'] + branches-ignore: [ 'l10*' ] env: python_version: 3.9 @@ -32,20 +32,20 @@ jobs: frontend: ${{ steps.filter.outputs.frontend }} steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - server: - - 'InvenTree/**' - - 'requirements.txt' - - 'requirements-dev.txt' - migrations: - - '**/migrations/**' - - '.github/workflows**' - frontend: - - 'src/frontend/**' + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 + - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # pin@v2.11.1 + id: filter + with: + filters: | + server: + - 'InvenTree/**' + - 'requirements.txt' + - 'requirements-dev.txt' + migrations: + - '**/migrations/**' + - '.github/workflows**' + frontend: + - 'src/frontend/**' pep_style: name: Style [Python] @@ -55,7 +55,7 @@ jobs: if: needs.paths-filter.outputs.server == 'true' steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -67,10 +67,10 @@ jobs: name: Style - Classic UI [JS] runs-on: ubuntu-20.04 - needs: ['pep_style', 'pre-commit'] + needs: [ 'pep_style', 'pre-commit' ] steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.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' steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Set up Python ${{ env.python_version }} - uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1 with: python-version: ${{ env.python_version }} cache: 'pip' @@ -113,9 +113,9 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Set up Python ${{ env.python_version }} - uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1 with: python-version: ${{ env.python_version }} - name: Check Config @@ -145,7 +145,7 @@ jobs: INVENTREE_PYTHON_TEST_PASSWORD: testpassword steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -154,8 +154,7 @@ jobs: update: true npm: true - name: Download Python Code For `${{ env.wrapper_name }}` - run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} - ./${{ env.wrapper_name }} + run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }} - name: Start InvenTree Server run: | invoke delete-data -f @@ -176,7 +175,7 @@ jobs: continue-on-error: true steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -198,7 +197,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -214,7 +213,7 @@ jobs: - name: Coverage Tests run: invoke test --coverage - name: Upload Coverage Report - uses: coverallsapp/github-action@v2 + uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -248,7 +247,7 @@ jobs: - 6379:6379 steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -287,13 +286,12 @@ jobs: MYSQL_USER: inventree MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s - --health-retries=3 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 ports: - 3306:3306 steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -332,7 +330,7 @@ jobs: - 5432:5432 steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -356,47 +354,47 @@ jobs: INVENTREE_PLUGINS_ENABLED: false steps: - - uses: actions/checkout@v3 - name: Checkout Code - - name: Environment Setup - uses: ./.github/actions/setup - with: - install: true - - name: Fetch Database - run: git clone --depth 1 https://github.com/inventree/test-db ./test-db + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 + name: Checkout Code + - name: Environment Setup + uses: ./.github/actions/setup + with: + install: true + - name: Fetch Database + run: git clone --depth 1 https://github.com/inventree/test-db ./test-db - - name: Latest Database - run: | - cp test-db/latest.sqlite3 /home/runner/work/InvenTree/db.sqlite3 - chmod +rw /home/runner/work/InvenTree/db.sqlite3 - invoke migrate + - name: Latest Database + run: | + cp test-db/latest.sqlite3 /home/runner/work/InvenTree/db.sqlite3 + chmod +rw /home/runner/work/InvenTree/db.sqlite3 + invoke migrate - - name: 0.10.0 Database - run: | - rm /home/runner/work/InvenTree/db.sqlite3 - cp test-db/stable_0.10.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3 - chmod +rw /home/runner/work/InvenTree/db.sqlite3 - invoke migrate + - name: 0.10.0 Database + run: | + rm /home/runner/work/InvenTree/db.sqlite3 + cp test-db/stable_0.10.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3 + chmod +rw /home/runner/work/InvenTree/db.sqlite3 + invoke migrate - - name: 0.11.0 Database - run: | - rm /home/runner/work/InvenTree/db.sqlite3 - cp test-db/stable_0.11.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3 - chmod +rw /home/runner/work/InvenTree/db.sqlite3 - invoke migrate + - name: 0.11.0 Database + run: | + rm /home/runner/work/InvenTree/db.sqlite3 + cp test-db/stable_0.11.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3 + chmod +rw /home/runner/work/InvenTree/db.sqlite3 + invoke migrate - - name: 0.12.0 Database - run: | - rm /home/runner/work/InvenTree/db.sqlite3 - cp test-db/stable_0.12.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3 - chmod +rw /home/runner/work/InvenTree/db.sqlite3 - invoke migrate + - name: 0.12.0 Database + run: | + rm /home/runner/work/InvenTree/db.sqlite3 + cp test-db/stable_0.12.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3 + chmod +rw /home/runner/work/InvenTree/db.sqlite3 + invoke migrate platform_ui: name: Tests - Platform UI runs-on: ubuntu-20.04 timeout-minutes: 60 - needs: ['pre-commit', 'paths-filter'] + needs: [ 'pre-commit', 'paths-filter' ] if: needs.paths-filter.outputs.frontend == 'true' env: INVENTREE_DB_ENGINE: sqlite3 @@ -405,7 +403,7 @@ jobs: INVENTREE_PLUGINS_ENABLED: false steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -420,7 +418,7 @@ jobs: run: cd src/frontend && npx playwright install --with-deps - name: Run Playwright tests run: cd src/frontend && npx playwright test - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # pin@v3.1.3 if: always() with: name: playwright-report @@ -433,7 +431,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -446,7 +444,7 @@ jobs: run: | cd InvenTree/web/static zip -r frontend-build.zip web/ - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # pin@v3.1.3 with: name: frontend-build path: InvenTree/web/static/web diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc6631dc24..b22280b195 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,13 +13,13 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout Code - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Version Check run: | pip install requests python3 ci/version_check.py - name: Push to Stable Branch - uses: ad-m/github-push-action@4dcce6dea3e3c8187237fc86b7dfdc93e5aaae58 # pin@master + uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0 if: env.stable_release == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -29,7 +29,7 @@ jobs: publish-build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Environment Setup uses: ./.github/actions/setup with: @@ -42,7 +42,7 @@ jobs: run: | cd InvenTree/web/static/web zip -r ../frontend-build.zip * - - uses: svenstaro/upload-release-action@v2 + - uses: svenstaro/upload-release-action@1beeb572c19a9242f4361f4cee78f8e0d9aec5df # pin@2.7.0 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: InvenTree/web/static/frontend-build.zip diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index eefed27ed7..64433876ef 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,11 +14,10 @@ jobs: pull-requests: write steps: - - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # pin@v6.0.1 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # pin@v8.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue seems stale. Please react to show this is still - important.' + stale-issue-message: 'This issue seems stale. Please react to show this is still important.' stale-pr-message: 'This PR seems stale. Please react to show this is still important.' stale-issue-label: 'inactive' stale-pr-label: 'inactive' diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index a7be8e7886..16e0721dba 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -21,13 +21,13 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Set up Python 3.9 - uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # pin@v4.3.0 + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # pin@v4.7.1 with: python-version: 3.9 - name: Set up Node 16 - uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b # pin to v3.5.0 + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # pin to v3.8.2 with: node-version: 16 - name: Install Dependencies @@ -46,7 +46,7 @@ jobs: git add "*.po" git commit -m "updated translation base" - name: Push changes - uses: ad-m/github-push-action@4dcce6dea3e3c8187237fc86b7dfdc93e5aaae58 # pin@master + uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: l10 diff --git a/.github/workflows/update.yml.disabled b/.github/workflows/update.yml.disabled index 50cfe65196..a3cecb147e 100644 --- a/.github/workflows/update.yml.disabled +++ b/.github/workflows/update.yml.disabled @@ -9,14 +9,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Setup run: pip install -r requirements-dev.txt - name: Update requirements.txt run: pip-compile --output-file=requirements.txt requirements.in -U - name: Update requirements-dev.txt - run: pip-compile --generate-hashes --output-file=requirements-dev.txt - requirements-dev.in -U + run: pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in -U - uses: stefanzweifel/git-auto-commit-action@fd157da78fa13d9383e5580d1fd1184d89554b51 # pin@v4.15.1 with: commit_message: "[Bot] Updated dependency" diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index d272ac54e6..c3dd8a1897 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 140 +INVENTREE_API_VERSION = 141 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v141 -> 2023-10-23 : https://github.com/inventree/InvenTree/pull/5774 + - Changed 'part.responsible' from User to Owner + v140 -> 2023-10-20 : https://github.com/inventree/InvenTree/pull/5664 - Expand API token functionality - Multiple API tokens can be generated per user diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py index 310309a890..72e81f04d2 100644 --- a/InvenTree/InvenTree/config.py +++ b/InvenTree/InvenTree/config.py @@ -7,6 +7,7 @@ import os import random import shutil import string +import warnings from pathlib import Path logger = logging.getLogger('inventree') @@ -341,3 +342,58 @@ def get_custom_file(env_ref: str, conf_ref: str, log_ref: str, lookup_media: boo value = False return value + + +def get_frontend_settings(debug=True): + """Return a dictionary of settings for the frontend interface. + + Note that the new config settings use the 'FRONTEND' key, + whereas the legacy key was 'PUI' (platform UI) which is now deprecated + """ + + # Legacy settings + pui_settings = get_setting('INVENTREE_PUI_SETTINGS', 'pui_settings', {}, typecast=dict) + + if len(pui_settings) > 0: + warnings.warn( + "The 'INVENTREE_PUI_SETTINGS' key is deprecated. Please use 'INVENTREE_FRONTEND_SETTINGS' instead", + DeprecationWarning, stacklevel=2 + ) + + # New settings + frontend_settings = get_setting('INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict) + + # Merge settings + settings = {**pui_settings, **frontend_settings} + + # Set the base URL + if 'base_url' not in settings: + base_url = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', '') + + if base_url: + warnings.warn( + "The 'INVENTREE_PUI_URL_BASE' key is deprecated. Please use 'INVENTREE_FRONTEND_URL_BASE' instead", + DeprecationWarning, stacklevel=2 + ) + else: + base_url = get_setting('INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'platform') + + settings['base_url'] = base_url + + # Set the server list + settings['server_list'] = settings.get('server_list', []) + + # Set the debug flag + settings['debug'] = debug + + if 'environment' not in settings: + settings['environment'] = 'development' if debug else 'production' + + if debug and 'show_server_selector' not in settings: + # In debug mode, show server selector by default + settings['show_server_selector'] = True + elif len(settings['server_list']) == 0: + # If no servers are specified, show server selector + settings['show_server_selector'] = True + + return settings diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index c34c5416e8..7fac82e13f 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -64,7 +64,7 @@ class AuthRequiredMiddleware(object): elif request.path_info.startswith('/accounts/'): authorized = True - elif request.path_info.startswith(f'/{settings.PUI_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.PUI_URL_BASE}': + 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}': authorized = True elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys(): diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 206b4a697b..a808f6d603 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -1044,9 +1044,9 @@ CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', ' CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {}) -# Frontend settings -PUI_URL_BASE = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', 'platform') -PUI_SETTINGS = get_setting("INVENTREE_PUI_SETTINGS", "pui_settings", {}) +# Load settings for the frontend interface +FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG) +FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform') if DEBUG: logger.info("InvenTree running with DEBUG enabled") @@ -1076,5 +1076,5 @@ if CUSTOM_FLAGS: # Magic login django-sesame SESAME_MAX_AGE = 300 -# LOGIN_REDIRECT_URL = f"/{PUI_URL_BASE}/logged-in/" +# LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/" LOGIN_REDIRECT_URL = "/index/" diff --git a/InvenTree/InvenTree/social_auth_urls.py b/InvenTree/InvenTree/social_auth_urls.py index 304232ea9a..fe6a129673 100644 --- a/InvenTree/InvenTree/social_auth_urls.py +++ b/InvenTree/InvenTree/social_auth_urls.py @@ -96,7 +96,7 @@ for provider in providers.registry.get_list(): social_auth_urlpatterns += provider_urlpatterns -class SocialProvierListView(ListAPIView): +class SocialProviderListView(ListAPIView): """List of available social providers.""" permission_classes = (AllowAny,) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 5687d5f7a6..584f69a246 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -1216,6 +1216,6 @@ class MagicLoginTest(InvenTreeTestCase): self.assertEqual(resp.url, '/index/') # Note: 2023-08-08 - This test has been changed because "platform UI" is not generally available yet # TODO: In the future, the URL comparison will need to be reverted - # self.assertEqual(resp.url, f'/{settings.PUI_URL_BASE}/logged-in/') + # self.assertEqual(resp.url, f'/{settings.FRONTEND_URL_BASE}/logged-in/') # And we should be logged in again self.assertEqual(resp.wsgi_request.user, self.user) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b55bf78fbb..82a21fd331 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -16,29 +16,29 @@ from dj_rest_auth.registration.views import (ConfirmEmailView, from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView from sesame.views import LoginView -from build.api import build_api_urls +import build.api +import common.api +import company.api +import label.api +import order.api +import part.api +import plugin.api +import report.api +import stock.api +import users.api from build.urls import build_urls -from common.api import admin_api_urls, common_api_urls, settings_api_urls from common.urls import common_urls -from company.api import company_api_urls from company.urls import (company_urls, manufacturer_part_urls, supplier_part_urls) -from label.api import label_api_urls -from order.api import order_api_urls from order.urls import order_urls -from part.api import bom_api_urls, part_api_urls from part.urls import part_urls -from plugin.api import plugin_api_urls from plugin.urls import get_plugin_urls -from report.api import report_api_urls -from stock.api import stock_api_urls from stock.urls import stock_urls -from users.api import user_urls from web.urls import urlpatterns as platform_urls from .api import APISearchView, InfoView, NotFoundView from .magic_login import GetSimpleLoginView -from .social_auth_urls import SocialProvierListView, social_auth_urlpatterns +from .social_auth_urls import SocialProviderListView, social_auth_urlpatterns from .views import (AboutView, AppearanceSelectView, CustomConnectionsView, CustomEmailView, CustomLoginView, CustomPasswordResetFromKeyView, @@ -55,23 +55,23 @@ apipatterns = [ # Global search path('search/', APISearchView.as_view(), name='api-search'), - re_path(r'^settings/', include(settings_api_urls)), - re_path(r'^part/', include(part_api_urls)), - re_path(r'^bom/', include(bom_api_urls)), - re_path(r'^company/', include(company_api_urls)), - re_path(r'^stock/', include(stock_api_urls)), - re_path(r'^build/', include(build_api_urls)), - re_path(r'^order/', include(order_api_urls)), - re_path(r'^label/', include(label_api_urls)), - re_path(r'^report/', include(report_api_urls)), - re_path(r'^user/', include(user_urls)), - re_path(r'^admin/', include(admin_api_urls)), + re_path(r'^settings/', include(common.api.settings_api_urls)), + re_path(r'^part/', include(part.api.part_api_urls)), + re_path(r'^bom/', include(part.api.bom_api_urls)), + re_path(r'^company/', include(company.api.company_api_urls)), + re_path(r'^stock/', include(stock.api.stock_api_urls)), + re_path(r'^build/', include(build.api.build_api_urls)), + re_path(r'^order/', include(order.api.order_api_urls)), + re_path(r'^label/', include(label.api.label_api_urls)), + re_path(r'^report/', include(report.api.report_api_urls)), + re_path(r'^user/', include(users.api.user_urls)), + re_path(r'^admin/', include(common.api.admin_api_urls)), # Plugin endpoints - path('', include(plugin_api_urls)), + path('', include(plugin.api.plugin_api_urls)), # Common endpoints endpoint - path('', include(common_api_urls)), + path('', include(common.api.common_api_urls)), # OpenAPI Schema re_path('schema/', SpectacularAPIView.as_view(custom_settings={'SCHEMA_PATH_PREFIX': '/api/'}), name='schema'), @@ -83,7 +83,7 @@ apipatterns = [ path('auth/', include([ re_path(r'^registration/account-confirm-email/(?P[-:\w]+)/$', ConfirmEmailView.as_view(), name='account_confirm_email'), path('registration/', include('dj_rest_auth.registration.urls')), - path('providers/', SocialProvierListView.as_view(), name='social_providers'), + path('providers/', SocialProviderListView.as_view(), name='social_providers'), path('social/', include(social_auth_urlpatterns)), path('social/', SocialAccountListView.as_view(), name='social_account_list'), path('social//disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'), diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 5755949cac..1e51863a68 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -290,18 +290,17 @@ remote_login_header: HTTP_REMOTE_USER # logo: img/custom_logo.png # splash: img/custom_splash.jpg -# Platform UI options -# pui_settings: +# Frontend UI settings +# frontend_settings: +# base_url: 'frontend' # server_list: # my_server1: -# host: https://demo.inventree.org/api/ +# host: https://demo.inventree.org/ # name: InvenTree Demo # default_server: my_server1 # show_server_selector: false # sentry_dsn: https://84f0c3ea90c64e5092e2bf5dfe325725@o1047628.ingest.sentry.io/4504160008273920 # environment: development -# Base URL for serving Platform UI -# pui_url_base: 'platform' # Custom flags # InvenTree uses django-flags; read more in their docs at https://cfpb.github.io/django-flags/conditions/ diff --git a/InvenTree/part/migrations/0115_part_responsible_owner.py b/InvenTree/part/migrations/0115_part_responsible_owner.py new file mode 100644 index 0000000000..2605ac51f2 --- /dev/null +++ b/InvenTree/part/migrations/0115_part_responsible_owner.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.22 on 2023-10-23 01:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_auto_20231020_2356'), + ('part', '0114_alter_part_minimum_stock'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='responsible_owner', + field=models.ForeignKey(blank=True, help_text='Owner responsible for this part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parts_responsible', to='users.owner', verbose_name='Responsible'), + ), + ] diff --git a/InvenTree/part/migrations/0116_auto_20231023_0332.py b/InvenTree/part/migrations/0116_auto_20231023_0332.py new file mode 100644 index 0000000000..49f3f675a9 --- /dev/null +++ b/InvenTree/part/migrations/0116_auto_20231023_0332.py @@ -0,0 +1,76 @@ +# Generated by Django 3.2.22 on 2023-10-23 03:32 + +from django.db import migrations + + +def migrate_part_responsible_owner(apps, schema_editor): + """Copy existing part.responsible field to part.responsible_owner""" + + Owner = apps.get_model('users', 'Owner') + Part = apps.get_model('part', 'Part') + User = apps.get_model('auth', 'user') + ContentType = apps.get_model('contenttypes', 'contenttype') + + user_type = ContentType.objects.get_for_model(User) + + parts = Part.objects.exclude(responsible=None) + + for part in parts: + + # Find a corresponding Owner object, or create one if it does not exist + owner, _created = Owner.objects.get_or_create( + owner_type=user_type, + owner_id=part.responsible.id, + ) + + part.responsible_owner = owner + part.save() + + if parts.count() > 0: + print(f"Added 'responsible_owner' for {parts.count()} parts") + + +def reverse_owner_migration(apps, schema_editor): + """Reverse the owner migration: + + - Set the 'responsible' field to a selected user + - Only where 'responsible_owner' is set + - Only where 'responsible_owner' is a User object + """ + + Part = apps.get_model('part', 'Part') + User = apps.get_model('auth', 'user') + ContentType = apps.get_model('contenttypes', 'contenttype') + + user_type = ContentType.objects.get_for_model(User) + + parts = Part.objects.exclude(responsible_owner=None) + + for part in parts: + + if part.responsible_owner.owner_type == user_type: + + # Attempt to find matching user + try: + user = User.objects.get(pk=part.responsible_owner.owner_id) + part.responsible = user + part.save() + except User.DoesNotExist: + print("User does not exist:", part.responsible_owner.owner_id) + + if parts.count() > 0: + print(f"Added 'responsible' for {parts.count()} parts") + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0115_part_responsible_owner'), + ('users', '0005_owner_model'), + ] + + operations = [ + migrations.RunPython( + migrate_part_responsible_owner, + reverse_code=reverse_owner_migration, + ) + ] diff --git a/InvenTree/part/migrations/0117_remove_part_responsible.py b/InvenTree/part/migrations/0117_remove_part_responsible.py new file mode 100644 index 0000000000..add8405179 --- /dev/null +++ b/InvenTree/part/migrations/0117_remove_part_responsible.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.22 on 2023-10-23 05:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0116_auto_20231023_0332'), + ] + + operations = [ + migrations.RemoveField( + model_name='part', + name='responsible', + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 854deba245..2960d8335b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -41,6 +41,7 @@ import InvenTree.fields import InvenTree.ready import InvenTree.tasks import part.settings as part_settings +import users.models from build import models as BuildModels from common.models import InvenTreeSetting from common.settings import currency_code_default @@ -379,7 +380,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel) notes: Additional notes field for this part creation_date: Date that this part was added to the database creation_user: User who added this part to the database - responsible: User who is responsible for this part (optional) + responsible_owner: Owner (either user or group) which is responsible for this part (optional) last_stocktake: Date at which last stocktake was performed for this Part """ @@ -1036,7 +1037,13 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel) creation_user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Creation User'), related_name='parts_created') - responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Responsible'), help_text=_('User responsible for this part'), related_name='parts_responible') + responsible_owner = models.ForeignKey( + users.models.Owner, on_delete=models.SET_NULL, + blank=True, null=True, + verbose_name=_('Responsible'), + help_text=_('Owner responsible for this part'), + related_name='parts_responsible' + ) last_stocktake = models.DateField( blank=True, null=True, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index ab6881c1b0..f30ba199fd 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -26,6 +26,7 @@ import part.filters import part.stocktake import part.tasks import stock.models +import users.models from InvenTree.status_codes import BuildStatusGroups from InvenTree.tasks import offload_task @@ -695,6 +696,12 @@ class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serialize read_only=True, ) + responsible = serializers.PrimaryKeyRelatedField( + queryset=users.models.Owner.objects.all(), + required=False, allow_null=True, + source='responsible_owner', + ) + # Annotated fields allocated_to_build_orders = serializers.FloatField(read_only=True) allocated_to_sales_orders = serializers.FloatField(read_only=True) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index eaec63ce97..3dcfeebb78 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -384,11 +384,11 @@ {% include 'clip_link.html' with link=part.link new_window=True %} {% endif %} - {% if part.responsible %} + {% if part.responsible_owner %} {% trans "Responsible" %} - {{ part.responsible }} + {{ part.responsible_owner }} {% endif %} diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index b65ca1a1f8..e5a8fafb32 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -16,10 +16,10 @@ from django.utils.translation import gettext_lazy as _ import common.models import InvenTree.helpers import InvenTree.helpers_model +import plugin.models from common.settings import currency_code_default from InvenTree import settings, version from plugin import registry -from plugin.models import NotificationUserSetting, PluginSetting from plugin.plugin import InvenTreePlugin register = template.Library() @@ -346,14 +346,14 @@ def setting_object(key, *args, **kwargs): if 'plugin' in kwargs: # Note, 'plugin' is an instance of an InvenTreePlugin class - plugin = kwargs['plugin'] - if issubclass(plugin.__class__, InvenTreePlugin): - plugin = plugin.plugin_config() + plg = kwargs['plugin'] + if issubclass(plg.__class__, InvenTreePlugin): + plg = plg.plugin_config() - return PluginSetting.get_setting_object(key, plugin=plugin, cache=cache) + return plugin.models.PluginSetting.get_setting_object(key, plugin=plg, cache=cache) elif 'method' in kwargs: - return NotificationUserSetting.get_setting_object(key, user=kwargs['user'], method=kwargs['method'], cache=cache) + return plugin.models.NotificationUserSetting.get_setting_object(key, user=kwargs['user'], method=kwargs['method'], cache=cache) elif 'user' in kwargs: return common.models.InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'], cache=cache) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index ff15c81b76..e5c4e14392 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -649,7 +649,11 @@ class PluginsRegistry: try: logger.debug("Updating plugin registry hash: %s", str(self.registry_hash)) InvenTreeSetting.set_setting("_PLUGIN_REGISTRY_HASH", self.registry_hash, change_user=None) + except (OperationalError, ProgrammingError): + # Exception if the database has not been migrated yet, or is not ready + pass except Exception as exc: + # Some other exception, we want to know about it logger.exception("Failed to update plugin registry hash: %s", str(exc)) def calculate_plugin_hash(self): diff --git a/InvenTree/templates/js/translated/purchase_order.js b/InvenTree/templates/js/translated/purchase_order.js index 67bce86f42..31ade365b0 100644 --- a/InvenTree/templates/js/translated/purchase_order.js +++ b/InvenTree/templates/js/translated/purchase_order.js @@ -1673,7 +1673,11 @@ function loadPurchaseOrderTable(table, options) { sortable: true, sortName: 'supplier__name', formatter: function(value, row) { - return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/?display=purchase-orders`); + if (row.supplier_detail) { + return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/?display=purchase-orders`); + } else { + return '-'; + } } }, { @@ -1986,7 +1990,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { title: '{% trans "Part" %}', switchable: false, formatter: function(value, row, index, field) { - if (row.part) { + if (row.part_detail) { return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`); } else { return '-'; diff --git a/InvenTree/templates/js/translated/sales_order.js b/InvenTree/templates/js/translated/sales_order.js index 0b99d47732..61e1990c3b 100644 --- a/InvenTree/templates/js/translated/sales_order.js +++ b/InvenTree/templates/js/translated/sales_order.js @@ -1793,7 +1793,7 @@ function loadSalesOrderLineItemTable(table, options={}) { title: '{% trans "Part" %}', switchable: false, formatter: function(value, row, index, field) { - if (row.part) { + if (row.part_detail) { return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`); } else { return '-'; diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 910c20d7e1..ce8207863e 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -3062,8 +3062,10 @@ function loadInstalledInTable(table, options) { formatter: function(value, row) { var html = ''; - html += imageHoverIcon(row.part_detail.thumbnail); - html += renderLink(row.part_detail.full_name, `/stock/item/${row.pk}/`); + if (row.part_detail) { + html += imageHoverIcon(row.part_detail.thumbnail); + html += renderLink(row.part_detail.full_name, `/stock/item/${row.pk}/`); + } return html; } diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index 7554158b72..95d59a6e82 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -6,7 +6,6 @@ from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import Group -from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from users.models import ApiToken, Owner, RuleSet @@ -203,20 +202,22 @@ class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover users = form.cleaned_data['users'] # Check for users who are members of multiple groups - warning_message = '' + multiple_group_users = [] + for user in users: if user.groups.all().count() > 1: - warning_message += f'
- {user.username} is member of: ' - for idx, group in enumerate(user.groups.all()): - warning_message += f'{group.name}' - if idx < len(user.groups.all()) - 1: - warning_message += ', ' + multiple_group_users.append(user.username) # If any, display warning message when group is saved - if warning_message: - warning_message = mark_safe(_(f'The following users are members of multiple groups:' - f'{warning_message}')) - messages.add_message(request, messages.WARNING, warning_message) + if len(multiple_group_users) > 0: + + msg = _("The following users are members of multiple groups") + ": " + ", ".join(multiple_group_users) + + messages.add_message( + request, + messages.WARNING, + msg + ) def save_formset(self, request, form, formset, change): """Save the inline formset""" diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index cce30ecc91..803ed1c614 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -422,11 +422,7 @@ class RuleSet(models.Model): """Construct the correctly formatted permission string, given the app_model name, and the permission type.""" model, app = split_model(model) - return "{app}.{perm}_{model}".format( - app=app, - perm=permission, - model=model - ) + return f"{app}.{permission}_{model}" def __str__(self, debug=False): # pragma: no cover """Ruleset string representation.""" @@ -504,12 +500,7 @@ def update_group_roles(group, debug=False): # and create a simplified permission key string for p in group.permissions.all().prefetch_related('content_type'): (permission, app, model) = p.natural_key() - - permission_string = '{app}.{perm}'.format( - app=app, - perm=permission - ) - + permission_string = f"{app}.{permission}" group_permissions.add(permission_string) # List of permissions which must be added to the group @@ -527,7 +518,7 @@ def update_group_roles(group, debug=False): allowed: Whether or not the action is allowed """ if action not in ['view', 'add', 'change', 'delete']: # pragma: no cover - raise ValueError("Action {a} is invalid".format(a=action)) + raise ValueError(f"Action {action} is invalid") permission_string = RuleSet.get_model_permission_string(model, action) diff --git a/InvenTree/web/templatetags/spa_helper.py b/InvenTree/web/templatetags/spa_helper.py index 4d8ebd9ae7..edea064d89 100644 --- a/InvenTree/web/templatetags/spa_helper.py +++ b/InvenTree/web/templatetags/spa_helper.py @@ -1,4 +1,5 @@ """Template tag to render SPA imports.""" + import json from logging import getLogger from pathlib import Path @@ -10,11 +11,7 @@ from django.utils.safestring import mark_safe logger = getLogger("InvenTree") register = template.Library() -PUI_DEFAULTS = { - 'url_base': settings.PUI_URL_BASE, -} -PUI_DEFAULTS.update(getattr(settings, 'PUI_SETTINGS', {})) -PUI_SETTINGS = json.dumps(PUI_DEFAULTS) +FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS) @register.simple_tag @@ -30,11 +27,11 @@ def spa_bundle(): index = manifest_data.get("index.html") css_index = manifest_data.get("index.css") - dynmanic_files = index.get("dynamicImports", []) + dynamic_files = index.get("dynamicImports", []) imports_files = "".join( [ f'' - for file in dynmanic_files + for file in dynamic_files ] ) @@ -47,4 +44,4 @@ def spa_bundle(): @register.simple_tag def spa_settings(): """Render settings for spa.""" - return mark_safe(f"""""") + return mark_safe(f"""""") diff --git a/InvenTree/web/urls.py b/InvenTree/web/urls.py index d1e409eeab..35726815ac 100644 --- a/InvenTree/web/urls.py +++ b/InvenTree/web/urls.py @@ -20,12 +20,12 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html urlpatterns = [ - path(f'{settings.PUI_URL_BASE}/', include([ + path(f'{settings.FRONTEND_URL_BASE}/', include([ path("assets/", RedirectAssetView.as_view()), re_path(r"^(?P.*)/$", spa_view), path("set-password?uid=&token=", spa_view, name="password_reset_confirm"), path("", spa_view),] )), - path(settings.PUI_URL_BASE, spa_view, name='platform'), + path(settings.FRONTEND_URL_BASE, spa_view, name='platform'), path("assets/", RedirectAssetView.as_view()), ] diff --git a/docs/docs/app/barcode.md b/docs/docs/app/barcode.md index 83c2e3a053..876bade3c1 100644 --- a/docs/docs/app/barcode.md +++ b/docs/docs/app/barcode.md @@ -25,8 +25,90 @@ The following code types are known to be supported - Data Matrix - Aztec -## Actions +## Barcode Input Methods -The InvenTree app uses barcodes where possible to provide efficient stock control operations. +Barcodes can be scanned using the following methods: -If there is a new barcode feature you would like to see, [let us know on GitHub](https://github.com/inventree/InvenTree/issues?q=is%3Aopen+is%3Aissue+label%3Aapp)! +### Camera Input + +The camera input method allows you to scan barcodes using the device's internal camera. Both the forward and rear-facing cameras are supported. + +### Keyboard Input + +The keyboard wedge input method allows you to scan barcodes using any scanner which presents barcode data as keyboard input. This works with external bluetooth scanners, and also provides support for integrated barcode scanner devices which run Android natively. + +Note that if using keyboard wedge input mode, the scanner must be configured to append an enter (`\n`) character to the end of the barcode data. + +## Barcode Actions + +The InvenTree app uses barcodes where possible to provide efficient stock control operations. Some pages in the app will provide context-sensitive barcode actions. These actions are available from the *Barcode Actions* menu, which is displayed in the bottom right corner of the screen. + +### Global Scan + +Available from the global bottom menu, the *Scan Barcode* provides quick access for scanning a barcode already associated with an InvenTree database item (such as a stock item or location). + +If a match is found, the app will navigate to the relevant page. + +### Stock Location Actions + +From the [Stock Location detail page](./stock.md#stock-location-view), multiple barcode actions may be available: + +{% with id="location-actions", url="app/barcode_stock_location_actions.png", maxheight="240px", description="Stock location barcode actions" %} +{% include 'img.html' %} +{% endwith %} + +#### Assign Barcode + +Assign a custom barcode to the selected location. Scanning a barcode (which is not already associated with an item in the database) will result in that barcode being assigned to the selected location. + +#### Transfer Stock Location + +Transfer the currently selected stock location into another location. Scanning a valid barcode associated with a stock location will result in the current location being *moved* to the scanned location. + +#### Scan Received Parts + +Receive incoming purchase order items into the selected location. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into the selected location. + +#### Scan Items Into Location + +the *Scan Items Into Location* action allows you to scan items into the selected location. Scanning a valid barcode associated with a stock item (already in the database) will result in that item being transferred to the selected location. + +### Stock Item Actions + +From the [Stock Item detail page](./stock.md#stock-item-detail-view), the following barcode actions may be available: + +{% with id="item-actions", url="app/barcode_stock_item_actions.png", maxheight="240px", description="Stock item barcode actions" %} +{% include 'img.html' %} +{% endwith %} + +#### Assign Barcode + +Assign a custom barcode to the selected stock item. Scanning a barcode (which is not already associated with an item in the database) will result in that barcode being assigned to the selected stock item. + +#### Scan Into Location + +Scan the selected stock item into a stock location. Scanning a valid barcode associated with a stock location will result in the selected stock item being transferred to the scanned location. + +### Part Actions + +From the [Part detail page](./part.md#part-detail-view), the following barcode actions are available: + +{% with id="part-actions", url="app/barcode_part_actions.png", maxheight="240px", description="Part barcode actions" %} +{% include 'img.html' %} +{% endwith %} + +#### Assign Barcode + +Assign a custom barcode to the selected part. Scanning a barcode (which is not already associated with an item in the database) will result in that barcode being assigned to the selected part. + +### Purchase Order Actions + +From the [Purchase Order detail page](./po.md#purchase-order-detail) page, the following barcode actions are available: + +{% with id="po-actions", url="app/barcode_po_actions.png", maxheight="240px", description="Purchase order barcode actions" %} +{% include 'img.html' %} +{% endwith %} + +#### Scan Received Parts + +Receive incoming purchase order items against the selected purchase order. Scanning a *new* barcode which is associated with an item in an incoming purchase order will receive the item into stock. diff --git a/docs/docs/app/navigation.md b/docs/docs/app/navigation.md index aa38692a33..c86e4a01d9 100644 --- a/docs/docs/app/navigation.md +++ b/docs/docs/app/navigation.md @@ -48,7 +48,7 @@ The action opens the [Search](./search.md) s ### Scan Barcode -The action opens the [barcode scan](./barcode.md) window, which allows quick access to the barcode scanning functionality. +The action opens the [barcode scan](./barcode.md#global-scan) window, which allows quick access to the barcode scanning functionality. ## Context Actions diff --git a/docs/docs/app/privacy.md b/docs/docs/app/privacy.md index cd16cbaa02..79ac7ad81c 100644 --- a/docs/docs/app/privacy.md +++ b/docs/docs/app/privacy.md @@ -10,13 +10,26 @@ The InvenTree mobile app requires some extra permissions for complete functional ### User Profiles -The InvenTree app requires the user to enter profile data to connect with an InvenTree server: +For each *profile* configured in the app, the following information is stored locally on the device: -- Server address -- Account username -- Account password +- Server name (e.g. "InvenTree Demo") +- Server address (e.g. "https://demo.inventree.org) +- *API token* -Profile data is stored locally on the device, and can be deleted by the user if they wish. Profile data is only used for connection with an InvenTree server. +#### User Authentication + +The InvenTree app uses an API token for user authentication. This token is requested once from the server, and then stored locally on the device. + +To initially request the token, the user will be required to enter their username and password. + +!!! info "Password Storage" + The user's username and password are not stored locally, or used for any purpose other than requesting an API token + +#### Token Handling + +A separate API token is stored locally for each profile. This token can be deleted at any time from within the app settings - this will force the user to enter their login credentials again to request a new token. + +Additionally, the stored token may be revoked by the server, or expire. Either situation will again require the user to re-enter their username and password. ### Camera Permissions diff --git a/docs/docs/app/settings.md b/docs/docs/app/settings.md index c76d678778..807616eeb2 100644 --- a/docs/docs/app/settings.md +++ b/docs/docs/app/settings.md @@ -55,7 +55,7 @@ Configure audible app notifications: ## Barcode Settings -The *Barcode Settings* view allows you to configure options relating to barcode scanning: +The *Barcode Settings* view allows you to configure options relating to [barcode scanning](./barcode.md): {% with id="barcode_settings", url="app/barcode_settings.png", maxheight="240px", description="Barcode Settings" %} {% include 'img.html' %} @@ -63,6 +63,7 @@ The *Barcode Settings* view allows you to configure options relating to barcode | Option | Description | | --- | --- | +| Scanner Input | Select barcode capture mode | | Barcode Scan Delay | Delay between successive scans | ## Home Screen diff --git a/docs/docs/assets/images/app/barcode_part_actions.png b/docs/docs/assets/images/app/barcode_part_actions.png new file mode 100644 index 0000000000..4f4134b60c Binary files /dev/null and b/docs/docs/assets/images/app/barcode_part_actions.png differ diff --git a/docs/docs/assets/images/app/barcode_po_actions.png b/docs/docs/assets/images/app/barcode_po_actions.png new file mode 100644 index 0000000000..15b7e93fde Binary files /dev/null and b/docs/docs/assets/images/app/barcode_po_actions.png differ diff --git a/docs/docs/assets/images/app/barcode_settings.png b/docs/docs/assets/images/app/barcode_settings.png index 8d38e41a80..8d069f2fc3 100644 Binary files a/docs/docs/assets/images/app/barcode_settings.png and b/docs/docs/assets/images/app/barcode_settings.png differ diff --git a/docs/docs/assets/images/app/barcode_stock_item_actions.png b/docs/docs/assets/images/app/barcode_stock_item_actions.png new file mode 100644 index 0000000000..5628700f22 Binary files /dev/null and b/docs/docs/assets/images/app/barcode_stock_item_actions.png differ diff --git a/docs/docs/assets/images/app/barcode_stock_location_actions.png b/docs/docs/assets/images/app/barcode_stock_location_actions.png new file mode 100644 index 0000000000..1d41f42028 Binary files /dev/null and b/docs/docs/assets/images/app/barcode_stock_location_actions.png differ diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 9f7a3657f8..77a70aff75 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -20,8 +20,8 @@ export const doClassicLogin = async (username: string, password: string) => { const token = await axios .get(apiUrl(ApiPaths.user_token), { auth: { username, password }, - baseURL: host.toString(), - timeout: 5000, + baseURL: host, + timeout: 2000, params: { name: 'inventree-web-app' } @@ -43,6 +43,9 @@ export const doClassicLogin = async (username: string, password: string) => { return true; }; +/** + * Logout the user (invalidate auth token) + */ export const doClassicLogout = async () => { // TODO @matmair - logout from the server session // Set token in context @@ -117,7 +120,7 @@ export function handleReset(navigate: any, values: { email: string }) { export function checkLoginState(navigate: any, redirect?: string) { api .get(apiUrl(ApiPaths.user_token), { - timeout: 5000, + timeout: 2000, params: { name: 'inventree-web-app' } @@ -137,8 +140,7 @@ export function checkLoginState(navigate: any, redirect?: string) { navigate('/login'); } }) - .catch((error) => { - console.error('Error fetching login information:', error); + .catch(() => { navigate('/login'); }); } diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx index 48afa14def..53322dc2b2 100644 --- a/src/frontend/src/main.tsx +++ b/src/frontend/src/main.tsx @@ -14,7 +14,7 @@ declare global { server_list: HostList; default_server: string; show_server_selector: boolean; - url_base: string; + base_url: string; sentry_dsn?: string; environment?: string; }; @@ -25,6 +25,17 @@ export const IS_DEV = import.meta.env.DEV; export const IS_DEMO = import.meta.env.VITE_DEMO === 'true'; export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO; +// Filter out any settings that are not defined +let loaded_vals = (window.INVENTREE_SETTINGS || {}) as any; +Object.keys(loaded_vals).forEach((key) => { + if (loaded_vals[key] === undefined) { + delete loaded_vals[key]; + // check for empty server list + } else if (key === 'server_list' && loaded_vals[key].length === 0) { + delete loaded_vals[key]; + } +}); + window.INVENTREE_SETTINGS = { server_list: { 'mantine-cqj63coxn': { @@ -40,11 +51,11 @@ window.INVENTREE_SETTINGS = { } : {}) }, - default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn', // use demo server for demo mode + default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn', show_server_selector: IS_DEV_OR_DEMO, // merge in settings that are already set via django's spa_view or for development - ...((window.INVENTREE_SETTINGS || {}) as any) + ...loaded_vals }; if (window.INVENTREE_SETTINGS.sentry_dsn) { @@ -56,7 +67,7 @@ if (window.INVENTREE_SETTINGS.sentry_dsn) { }); } -export const url_base = window.INVENTREE_SETTINGS.url_base || 'platform'; +export const base_url = window.INVENTREE_SETTINGS.base_url || 'platform'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( @@ -66,5 +77,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( // Redirect to base url if on / if (window.location.pathname === '/') { - window.location.replace(`/${url_base}`); + window.location.replace(`/${base_url}`); } diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 47fe1bbd5f..d49ff31aa8 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -15,9 +15,12 @@ export const useServerApiState = create((set, get) => ({ setServer: (newServer: ServerAPIProps) => set({ server: newServer }), fetchServerApiState: async () => { // Fetch server data - await api.get(apiUrl(ApiPaths.api_server_info)).then((response) => { - set({ server: response.data }); - }); + await api + .get(apiUrl(ApiPaths.api_server_info)) + .then((response) => { + set({ server: response.data }); + }) + .catch(() => {}); } })); diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index 05ae161660..37f0eb8f09 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -32,7 +32,7 @@ export const useUserState = create((set, get) => ({ // Fetch user data await api .get(apiUrl(ApiPaths.user_me), { - timeout: 5000 + timeout: 2000 }) .then((response) => { const user: UserProps = { diff --git a/src/frontend/src/views/DesktopAppView.tsx b/src/frontend/src/views/DesktopAppView.tsx index 379343bc61..c0598c04c7 100644 --- a/src/frontend/src/views/DesktopAppView.tsx +++ b/src/frontend/src/views/DesktopAppView.tsx @@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom'; import { queryClient, setApiDefaults } from '../App'; import { BaseContext } from '../contexts/BaseContext'; import { defaultHostList } from '../defaults/defaultHostList'; -import { url_base } from '../main'; +import { base_url } from '../main'; import { routes } from '../router'; import { useLocalState } from '../states/LocalState'; import { useSessionState } from '../states/SessionState'; @@ -27,10 +27,6 @@ export default function DesktopAppView() { ]); // Local state initialization - if (Object.keys(hostList).length === 0) { - console.log('Loading default host list'); - useLocalState.setState({ hostList: defaultHostList }); - } setApiDefaults(); // Server Session @@ -38,6 +34,11 @@ export default function DesktopAppView() { const sessionState = useSessionState.getState(); const [token] = sessionState.token ? [sessionState.token] : [null]; useEffect(() => { + if (Object.keys(hostList).length === 0) { + console.log('Loading default host list', defaultHostList); + useLocalState.setState({ hostList: defaultHostList }); + } + if (token && !fetchedServerSession) { setFetchedServerSession(true); fetchUserState(); @@ -49,7 +50,7 @@ export default function DesktopAppView() { return ( - {routes} + {routes} ); diff --git a/src/frontend/tests/ui_plattform.spec.ts b/src/frontend/tests/ui_plattform.spec.ts index 616ad5b460..10c276dbd1 100644 --- a/src/frontend/tests/ui_plattform.spec.ts +++ b/src/frontend/tests/ui_plattform.spec.ts @@ -11,9 +11,4 @@ test('Basic Platform UI test', async ({ page }) => { await page.goto('./platform/'); await expect(page).toHaveTitle('InvenTree Demo Server'); - await expect( - page.getByRole('heading', { - name: 'Welcome to your Dashboard, Ally Access' - }) - ).toBeVisible(); });