Merge remote-tracking branch 'origin/master' into report_merge

This commit is contained in:
Tristan Le
2025-04-18 16:30:30 +00:00
485 changed files with 77940 additions and 67293 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ RUN apt install -y \
yarn nodejs npm yarn nodejs npm
# Update to the latest stable node version # Update to the latest stable node version
RUN npm install -g n --ignore-scripts && n lts RUN npm install -g n@10.1.0 --ignore-scripts && n lts
RUN yarn config set network-timeout 600000 -g RUN yarn config set network-timeout 600000 -g
+1 -1
View File
@@ -27,7 +27,7 @@ python3 -m pip install --upgrade pip
pip3 install --ignore-installed --upgrade invoke Pillow pip3 install --ignore-installed --upgrade invoke Pillow
# install base level packages # install base level packages
pip3 install -Ur --require-hashes contrib/container/requirements.txt pip3 install -Ur contrib/container/requirements.txt --require-hashes
# Run initial InvenTree server setup # Run initial InvenTree server setup
invoke update -s invoke update -s
+7 -3
View File
@@ -64,7 +64,9 @@ runs:
- name: Install Specific Python Dependencies - name: Install Specific Python Dependencies
if: ${{ inputs.pip-dependency }} if: ${{ inputs.pip-dependency }}
shell: bash shell: bash
run: uv pip install ${{ inputs.pip-dependency }} run: uv pip install ${PIP_DEPS}
env:
PIP_DEPS: ${{ inputs.pip-dependency }}
# NPM installs # NPM installs
- name: Install node.js ${{ env.node_version }} - name: Install node.js ${{ env.node_version }}
@@ -78,8 +80,10 @@ runs:
shell: bash shell: bash
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install ${{ inputs.apt-dependency }} sudo apt-get install ${APT_DEPS}
sudo apt-get install ${{ inputs.apt-dependency }} sudo apt-get install ${APT_DEPS}
env:
APT_DEPS: ${{ inputs.apt-dependency }}
# Invoke commands # Invoke commands
- name: Install dev requirements - name: Install dev requirements
-1
View File
@@ -52,7 +52,6 @@ updates:
- package-ecosystem: npm - package-ecosystem: npm
directories: directories:
- /src/frontend - /src/frontend
- /src/backend
schedule: schedule:
interval: weekly interval: weekly
groups: groups:
+1
View File
@@ -120,6 +120,7 @@ jobs:
test -f data/config.yaml test -f data/config.yaml
test -f data/plugins.txt test -f data/plugins.txt
test -f data/secret_key.txt test -f data/secret_key.txt
test -f data/oidc.pem
- name: Run Unit Tests - name: Run Unit Tests
run: | run: |
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> contrib/container/docker.dev.env echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> contrib/container/docker.dev.env
+4 -5
View File
@@ -193,7 +193,6 @@ jobs:
diff -u src/backend/InvenTree/schema.yml api.yaml && echo "no difference in API schema " || exit 2 diff -u src/backend/InvenTree/schema.yml api.yaml && echo "no difference in API schema " || exit 2
- name: Check schema - including warnings - name: Check schema - including warnings
run: invoke dev.schema run: invoke dev.schema
continue-on-error: true
- name: Extract version for publishing - name: Extract version for publishing
id: version id: version
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true' if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
@@ -344,7 +343,7 @@ jobs:
- name: Coverage Tests - name: Coverage Tests
run: invoke dev.test --coverage --translations run: invoke dev.test --coverage --translations
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # pin@v5.4.0 uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@v5.4.2
if: always() if: always()
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -487,7 +486,7 @@ jobs:
- name: Run Tests - name: Run Tests
run: invoke dev.test --migrations --report --coverage --translations run: invoke dev.test --migrations --report --coverage --translations
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # pin@v5.4.0 uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@v5.4.2
if: always() if: always()
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -619,7 +618,7 @@ jobs:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # pin@v5.4.0 uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@v5.4.2
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -682,7 +681,7 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # pin@v3 uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # pin@v3
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor
+1 -1
View File
@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+2 -1
View File
@@ -7,6 +7,7 @@ __pycache__/
.Python .Python
env/ env/
inventree-env/ inventree-env/
.venv/
./build/ ./build/
.cache/ .cache/
develop-eggs/ develop-eggs/
@@ -18,7 +19,6 @@ share/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
@@ -57,6 +57,7 @@ data.json
# Key file # Key file
secret_key.txt secret_key.txt
oidc.pem
# IDE / development files # IDE / development files
.idea/ .idea/
+23
View File
@@ -40,6 +40,29 @@
"django": true, "django": true,
"justMyCode": false "justMyCode": false
}, },
{
"name": "InvenTree invoke schema",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/.venv/lib/python3.9/site-packages/invoke/__main__.py",
"cwd": "${workspaceFolder}",
"args": [
"dev.schema","--ignore-warnings"
],
"justMyCode": false
},
{
"name": "schema generation",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/src/backend/InvenTree/manage.py",
"args": [
"schema",
"--file","src/frontend/schema.yml"
],
"django": true,
"justMyCode": false
},
{ {
"name": "InvenTree Frontend - Vite", "name": "InvenTree Frontend - Vite",
"type": "chrome", "type": "chrome",
+3 -2
View File
@@ -37,6 +37,7 @@ ENV INVENTREE_BACKEND_DIR="${INVENTREE_HOME}/src/backend"
# InvenTree configuration files # InvenTree configuration files
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml" ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt" ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
ENV INVENTREE_OIDC_PRIVATE_KEY_FILE="${INVENTREE_DATA_DIR}/oidc.pem"
ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DATA_DIR}/plugins.txt" ENV INVENTREE_PLUGIN_FILE="${INVENTREE_DATA_DIR}/plugins.txt"
# Worker configuration (can be altered by user) # Worker configuration (can be altered by user)
@@ -108,7 +109,7 @@ RUN ./install_build_packages.sh --no-cache --virtual .build-deps && \
FROM prebuild AS frontend FROM prebuild AS frontend
RUN apk add --no-cache --update nodejs npm yarn bash RUN apk add --no-cache --update nodejs npm yarn bash
RUN npm install -g --ignore-scripts n RUN npm install -g --ignore-scripts n@10.1.0
RUN bash -c "n lts" RUN bash -c "n lts"
RUN yarn config set network-timeout 600000 -g RUN yarn config set network-timeout 600000 -g
COPY src ${INVENTREE_HOME}/src COPY src ${INVENTREE_HOME}/src
@@ -152,7 +153,7 @@ RUN pip install --require-hashes -r base_requirements.txt --no-cache
# Install nodejs / npm / yarn # Install nodejs / npm / yarn
RUN apk add --no-cache --update nodejs npm yarn bash RUN apk add --no-cache --update nodejs npm yarn bash
RUN npm install -g --ignore-scripts n RUN npm install -g --ignore-scripts n@10.1.0
RUN bash -c "n lts" RUN bash -c "n lts"
RUN yarn config set network-timeout 600000 -g RUN yarn config set network-timeout 600000 -g
+3
View File
@@ -181,6 +181,7 @@ function detect_envs() {
export INVENTREE_PLUGINS_ENABLED=true export INVENTREE_PLUGINS_ENABLED=true
export INVENTREE_PLUGIN_FILE=${CONF_DIR}/plugins.txt export INVENTREE_PLUGIN_FILE=${CONF_DIR}/plugins.txt
export INVENTREE_SECRET_KEY_FILE=${CONF_DIR}/secret_key.txt export INVENTREE_SECRET_KEY_FILE=${CONF_DIR}/secret_key.txt
export INVENTREE_OIDC_PRIVATE_KEY_FILE=${CONF_DIR}/oidc.pem
export INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE:-sqlite3} export INVENTREE_DB_ENGINE=${INVENTREE_DB_ENGINE:-sqlite3}
export INVENTREE_DB_NAME=${INVENTREE_DB_NAME:-${DATA_DIR}/database.sqlite3} export INVENTREE_DB_NAME=${INVENTREE_DB_NAME:-${DATA_DIR}/database.sqlite3}
@@ -339,6 +340,8 @@ function set_env() {
sed -i s=#plugin_file:\ \'/path/to/plugins.txt\'=plugin_file:\ \'${INVENTREE_PLUGIN_FILE}\'=g ${INVENTREE_CONFIG_FILE} sed -i s=#plugin_file:\ \'/path/to/plugins.txt\'=plugin_file:\ \'${INVENTREE_PLUGIN_FILE}\'=g ${INVENTREE_CONFIG_FILE}
# Secret key file # Secret key file
sed -i s=#secret_key_file:\ \'/etc/inventree/secret_key.txt\'=secret_key_file:\ \'${INVENTREE_SECRET_KEY_FILE}\'=g ${INVENTREE_CONFIG_FILE} sed -i s=#secret_key_file:\ \'/etc/inventree/secret_key.txt\'=secret_key_file:\ \'${INVENTREE_SECRET_KEY_FILE}\'=g ${INVENTREE_CONFIG_FILE}
# OIDC private key file
sed -i s=#oidc_private_key_file:\ \'/etc/inventree/oidc.pem\'=oidc_private_key_file:\ \'${INVENTREE_OIDC_PRIVATE_KEY_FILE}\'=g ${INVENTREE_CONFIG_FILE}
# Debug mode # Debug mode
sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_FILE} sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_FILE}
+44 -4
View File
@@ -33,7 +33,7 @@ invoke dev.schema -help
Users must be authenticated to gain access to the InvenTree API. The API accepts either basic username:password authentication, or token authentication. Token authentication is recommended as it provides much faster API access. Users must be authenticated to gain access to the InvenTree API. The API accepts either basic username:password authentication, or token authentication. Token authentication is recommended as it provides much faster API access.
!!! warning "Permissions" !!! warning "Permissions"
API access is restricted based on the permissions assigned to the user. API access is restricted based on the permissions assigned to the user or scope of the application.
### Basic Auth ### Basic Auth
@@ -44,9 +44,9 @@ Users can authenticate against the API using basic authentication - specifically
Each user is assigned an authentication token which can be used to access the API. This token is persistent for that user (unless invalidated by an administrator) and can be used across multiple sessions. Each user is assigned an authentication token which can be used to access the API. This token is persistent for that user (unless invalidated by an administrator) and can be used across multiple sessions.
!!! info "Token Administration" !!! info "Token Administration"
User tokens can be created and/or invalidated via the Admin interface. User tokens can be created and/or invalidated via the user settings, admin center or admin interface.
### Requesting a Token #### Requesting a Token
If a user does not know their access token, it can be requested via the API interface itself, using a basic authentication request. If a user does not know their access token, it can be requested via the API interface itself, using a basic authentication request.
@@ -66,7 +66,7 @@ HTTP_200_OK
} }
``` ```
### Using a Token #### Using a Token
After reception of a valid authentication token, it can be subsequently used to perform token-based authentication. After reception of a valid authentication token, it can be subsequently used to perform token-based authentication.
@@ -95,6 +95,46 @@ headers = {
response = request.get('http://localhost:8080/api/part/', data=data, headers=headers) response = request.get('http://localhost:8080/api/part/', data=data, headers=headers)
``` ```
### oAuth2 / OIDC
!!! warning "Experimental"
This is an experimental feature that needs to be specifically enabled. See [Experimental features](../settings/experimental.md) for more information.
InvenTree has built-in support for using [oAuth2](https://oauth.net/2/) and OpenID Connect (OIDC) for authentication to the API. This enables using the instance as a very limited identity provider.
A default application using a public client with PKCE enabled ships with each instance. Intended to be used with the python api and configured with very wide scopes this can also be used for quick tests - the cliend_id is `zDFnsiRheJIOKNx6aCQ0quBxECg1QBHtVFDPloJ6`.
#### Managing applications
Superusers can register new applications and manage existing ones using a small application under the subpath `/o/applications/`.
It is recommended to:
- read the spec (RFC 6749 / 6750) and/or best practices (RFC 9700) before choosing client types
- chose scopes as narrow as possible
- configure redirection URIs as exact as possible
#### Scopes
InvenTree's oAuth scopes are strongly related to the [user roles](#user-roles).
Names consist of 1. type, 2. kind and 3. (opt) role, separated by colons.
There are 3 types:
- a: administrative scopes - used for administrating the server - these can be staff or superuser scopes
- g: general scopes - give wide access to the basic building blocks of InvenTree
- r: role scopes - map to specific actions (2) and roles (3)
Examples:
```bash
a:superuser
g:read
r:change:part
r:delete:stock
```
!!! info "Read the API docs"
The API [documentation](#documentation) and [schema](./schema.md) list the required scopes for every API endpoint / interaction in the security sections.
## Authorization ## Authorization
### User Roles ### User Roles
+1 -1
View File
@@ -4,7 +4,7 @@ title: Pricing Support
## Pricing ## Pricing
Pricing is an inherently complex topic, often subject to the particular requirements of the user. InvenTree attempts to provide a comprehensive pricing architecture which is useful without being proscriptive. Pricing is an inherently complex topic, often subject to the particular requirements of the user. InvenTree attempts to provide a comprehensive pricing architecture which is useful without being prescriptive.
InvenTree provides support for multiple currencies, allowing pricing information to be stored with base currency rates. InvenTree provides support for multiple currencies, allowing pricing information to be stored with base currency rates.
+4 -5
View File
@@ -18,7 +18,8 @@ If you add a lot of code (over ~1000 LOC) maybe split it into multiple plugins t
Great. Now please read the [plugin documentation](./plugins.md) to get an overview of the architecture. It is rather short as a the (builtin) mixins come with extensive docstrings. Great. Now please read the [plugin documentation](./plugins.md) to get an overview of the architecture. It is rather short as a the (builtin) mixins come with extensive docstrings.
### Pick your building blocks ### Pick your building blocks
Consider the usecase for your plugin and define the exact function of the plugin, maybe write it down in a short readme. Then pick the mixins you need (they help reduce custom code and keep the system reliable if internal calls change).
Consider the use-case for your plugin and define the exact function of the plugin, maybe write it down in a short readme. Then pick the mixins you need (they help reduce custom code and keep the system reliable if internal calls change).
- Is it just a simple REST-endpoint that runs a function ([ActionMixin](./plugins/action.md)) or a parser for a custom barcode format ([BarcodeMixin](./plugins/barcode.md))? - Is it just a simple REST-endpoint that runs a function ([ActionMixin](./plugins/action.md)) or a parser for a custom barcode format ([BarcodeMixin](./plugins/barcode.md))?
- How does the user interact with the plugin? Is it a UI separate from the main InvenTree UI ([UrlsMixin](./plugins/urls.md)), does it need multiple pages with navigation-links ([NavigationMixin](./plugins/navigation.md)). - How does the user interact with the plugin? Is it a UI separate from the main InvenTree UI ([UrlsMixin](./plugins/urls.md)), does it need multiple pages with navigation-links ([NavigationMixin](./plugins/navigation.md)).
@@ -30,7 +31,7 @@ Consider the usecase for your plugin and define the exact function of the plugin
- Do you need the full power of Django with custom models and all the complexity that comes with that welcome to the danger zone and [AppMixin](./plugins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance. - Do you need the full power of Django with custom models and all the complexity that comes with that welcome to the danger zone and [AppMixin](./plugins/app.md). The plugin will be treated as a app by django and can maybe rack the whole instance.
### Define the metadata ### Define the metadata
Do not forget to [declare the metadata](./plugins.md#plugin-options) for your plugin, those will be used in the settings. At least provide a weblink so users can file issues / reach you. Do not forget to [declare the metadata](./plugins.md#plugin-options) for your plugin, those will be used in the settings. At least provide a web link so users can file issues / reach you.
### Development guidelines ### Development guidelines
If you want to make your life easier, try to follow these guidelines; break where it makes sense for your use case. If you want to make your life easier, try to follow these guidelines; break where it makes sense for your use case.
@@ -139,9 +140,7 @@ from plugin.mixins import ActionMixin
class SampleActionPlugin(ActionMixin, InvenTreePlugin): class SampleActionPlugin(ActionMixin, InvenTreePlugin):
""" """Use docstrings for everything."""
Use docstrings for everything... pls
"""
NAME = "SampleActionPlugin" NAME = "SampleActionPlugin"
ACTION_NAME = "sample" ACTION_NAME = "sample"
+3 -35
View File
@@ -4,40 +4,8 @@ title: Third Party Integrations
## Third Party Integrations ## Third Party Integrations
A list of known third-party InvenTree extensions is provided below. If you have an extension that should be listed here, contact the InvenTree team on [GitHub](https://github.com/inventree/). A list of known third-party InvenTree extensions is provided [on our website](https://inventree.org/extend/integrate/) If you have an extension that should be listed here, contact the InvenTree team on [GitHub](https://github.com/inventree/).
### Ki-nTree ## Available Plugins
[Ki-nTree](https://github.com/sparkmicro/Ki-nTree/) is a fantastic tool for automated creation of [KiCad](https://www.kicad.org/) library parts, with direct integration with InvenTree. Refer to the [InvenTree website](https://inventree.org/plugins.html) for a (non exhaustive) list of plugins that are available for InvenTree. This includes both official and third-party plugins.
### PK2InvenTree
[PK2InvenTree](https://github.com/rgilham/PK2InvenTree) is an open-source tool for migrating an existing [PartKeepr](https://github.com/partkeepr/PartKeepr) database to InvenTree.
### Digikey-Inventree-Integration
[Digikey-Inventree-Integration](https://github.com/EUdds/Digikey-Inventree-Integration) is a simple project that takes a digikey part number to creates a part in InvenTree.
### F360-InvenTree
[F360-InvenTree](https://github.com/matmair/F360-InvenTree/) is a tool for creating links between Autodesk Fusion 360 components and InvenTree parts.
Still under heavy development.
### DigitalOcean droplet
[InvenTree droplet](https://inventree.org/digitalocean) is a 1-click solution to deploy InvenTree in the cloud with DigitalOcean. You still have to administer and update your instance.
The source code for this droplet can be found in [inventree_droplet](https://github.com/invenhost/inventree_droplet).
### InvenTree zebra plugin
[InvenTree zebra plugin](https://github.com/SergeoLacruz/inventree-zebra-plugin) is a plugin to print labels with zebra printers.
Currently only the GK420T printer is supported.
### InvenTree Apprise
[InvenTree Apprise](https://github.com/matmair/inventree-apprise) is a plugin to send notifications via Apprise. This enables a wide variety of targets.
## First party plugins
### InvenTree brother plugin
[InvenTree brother plugin](https://github.com/inventree/inventree-brother-plugin) is a plugin to print labels with brother Q series printers.
+5 -4
View File
@@ -4,7 +4,7 @@ title: Plugins
## InvenTree Plugin Architecture ## InvenTree Plugin Architecture
The InvenTree server code supports an extensible plugin architecture, allowing custom plugins to be integrated directly into the database server. This allows development of complex behaviours which are decoupled from core InvenTree code. The InvenTree server code supports an extensible plugin architecture, allowing custom plugins to be integrated directly into the database server. This allows development of complex behaviors which are decoupled from core InvenTree code.
Plugins can be added from multiple sources: Plugins can be added from multiple sources:
@@ -35,8 +35,8 @@ create-inventree-plugin
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. 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" !!! warning "Name Change"
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. The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`.
### Imports ### Imports
@@ -66,7 +66,7 @@ Mixins are split up internally to keep the source tree clean and enable better t
#### Models and other internal InvenTree APIs #### Models and other internal InvenTree APIs
!!! warning "Danger Zone" !!! warning "Danger Zone"
The APIs outside of the `plugin` namespace are not structured for public usage and require a more in-depth knowledge of the Django framework. Please ask in GitHub discussions of the `ÌnvenTree` org if you are not sure you are using something the intended way. The APIs outside of the `plugin` namespace are not structured for public usage and require a more in-depth knowledge of the Django framework. Please ask in GitHub discussions of the `InvenTree` org if you are not sure you are using something the intended way.
We do not provide stable interfaces to models or any other internal python APIs. If you need to integrate into these parts please make yourself familiar with the codebase. We follow general Django patterns and only stray from them in limited, special cases. We do not provide stable interfaces to models or any other internal python APIs. If you need to integrate into these parts please make yourself familiar with the codebase. We follow general Django patterns and only stray from them in limited, special cases.
If you need to react to state changes please use the [EventMixin](./plugins/event.md). If you need to react to state changes please use the [EventMixin](./plugins/event.md).
@@ -121,6 +121,7 @@ Supported mixin classes are:
| [ReportMixin](./plugins/report.md) | Add custom context data to reports | | [ReportMixin](./plugins/report.md) | Add custom context data to reports |
| [ScheduleMixin](./plugins/schedule.md) | Schedule periodic tasks | | [ScheduleMixin](./plugins/schedule.md) | Schedule periodic tasks |
| [SettingsMixin](./plugins/settings.md) | Integrate user configurable settings | | [SettingsMixin](./plugins/settings.md) | Integrate user configurable settings |
| [UserInterfaceMixin](./plugins/ui.md) | Add custom user interface features |
| [UrlsMixin](./plugins/urls.md) | Respond to custom URL endpoints | | [UrlsMixin](./plugins/urls.md) | Respond to custom URL endpoints |
| [ValidationMixin](./plugins/validation.md) | Provide custom validation of database models | | [ValidationMixin](./plugins/validation.md) | Provide custom validation of database models |
+1 -1
View File
@@ -7,4 +7,4 @@ title: App Mixin
If this mixin is added to a plugin the directory the plugin class is defined in is added to the list of `INSTALLED_APPS` in the InvenTree server configuration. If this mixin is added to a plugin the directory the plugin class is defined in is added to the list of `INSTALLED_APPS` in the InvenTree server configuration.
!!! warning "Danger Zone" !!! warning "Danger Zone"
Only use this mixin if you have an understanding of djangos [app system]({% include "django.html" %}/ref/applications). Plugins with this mixin are deeply integrated into InvenTree and can cause difficult to reproduce or long-running errors. Use the built-in testing functions of django to make sure your code does not cause unwanted behaviour in InvenTree before releasing. Only use this mixin if you have an understanding of Django's [app system]({% include "django.html" %}/ref/applications). Plugins with this mixin are deeply integrated into InvenTree and can cause difficult to reproduce or long-running errors. Use the built-in testing functions of Django to make sure your code does not cause unwanted behaviour in InvenTree before releasing.
+1 -1
View File
@@ -5,7 +5,7 @@ title: Navigation Mixin
## NavigationMixin ## NavigationMixin
Use the class constant `NAVIGATION` for a array of links that should be added to InvenTrees navigation header. Use the class constant `NAVIGATION` for a array of links that should be added to InvenTrees navigation header.
The array must contain at least one dict that at least define a name and a link for each element. The link must be formatted for a URL pattern name lookup - links to external sites are not possible directly. The optional icon must be a class reference to an icon (InvenTree ships with fontawesome 4 by default). The array must contain at least one dict that at least define a name and a link for each element. The link must be formatted for a URL pattern name lookup - links to external sites are not possible directly. The optional icon must be a class reference to an icon.
``` python ``` python
class MyNavigationPlugin(NavigationMixin, InvenTreePlugin): class MyNavigationPlugin(NavigationMixin, InvenTreePlugin):
+44
View File
@@ -217,3 +217,47 @@ We are working to develop and distribute a library of custom InvenTree component
### Examples ### Examples
Refer to some of the existing InvenTree plugins linked above for examples of building custom UI plugins using the Mantine component library for seamless integration. Refer to some of the existing InvenTree plugins linked above for examples of building custom UI plugins using the Mantine component library for seamless integration.
## Building a User Interface Plugin
The technology stack which allows custom plugins to hook into the InvenTree user interface utilizes the following components:
- [React](https://react.dev)
- [Mantine](https://mantine.dev)
- [TypeScript](https://www.typescriptlang.org/)
- [Vite](https://vitejs.dev/)
While you don't need to be an expert in all of these technologies, it is recommended that you have a basic understanding of how they work together to build the InvenTree user interface. To get started, you should familiarize yourself with the frontend code (at `./src/frontend/`) as well as the vite configuration for the [InvenTree plugin creator](httsps://github.com/inventree/plugin-creator).
### Bundled with InvenTree
If a plugin is bundled with a separate copy of React libraries, issues may arise either due to version mismatches or because the React context is not shared between the InvenTree core and the plugin. This can lead to issues with rendering components, as the React context may not be shared between the two libraries.
To avoid issues, the InvenTree UI provides globally accessible components, which can be used as external modules by the plugin. This allows the plugin to use the same React context as the InvenTree core, and ensures that the plugin is compatible with the InvenTree user interface.
The following modules are provided as global objects at runtime:
- `React`
- `ReactDOM`
- `ReactDOMClient`
Additionally, for the Mantine library, the following modules are provided as global objects at runtime:
- `@mantine/core`
- `@mantine/hooks`
- `@mantine/notifications`
To use these modules in your plugin, they must be correctly *externalized* in the Vite configuration. Getting this right is crucial to ensure that the plugin is compatible with the InvenTree user interface. The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides a good starting point for this configuration, and can be used to generate a new plugin with the correct configuration.
!!! info "Bundled Version"
Keep in mind that the version of React and Mantine used in the InvenTree core may differ from the version used in your plugin. It is recommended to use the same version as the InvenTree core to avoid compatibility issues.
### Plugin Creator
The [InvenTree plugin creator](https://github.com/inventree/plugin-creator) provides an out-of-the-box setup for creating InvenTree plugins which integrate into the user interface. This includes a pre-configured Vite setup, which allows you to quickly get started with building your own custom UI plugins.
Using the plugin creator tool is the recommended way to get started with building custom UI plugins for InvenTree, as it provides a solid foundation to build upon. It is also the only method which is officially supported by the InvenTree development team!
### DIY
Of course, you can also build your own custom UI plugins from scratch. This is a more advanced option, and requires a good understanding of the InvenTree codebase, as well as the technologies used to build the user interface. You are free to use other web technologies, however if you choose to do this, don't expect any support from the InvenTree development team. We will only provide support for plugins which are built using the recommended stack, and which follow the guidelines outlined in this documentation.
+1 -1
View File
@@ -69,7 +69,7 @@ from plugin.mixins import ValidationMixin
import part.models import part.models
class MyValidationMixin(Validationixin, InvenTreePlugin): class MyValidationMixin(ValidationMixin, InvenTreePlugin):
"""Custom validation plugin.""" """Custom validation plugin."""
def validate_model_instance(self, instance, deltas=None): def validate_model_instance(self, instance, deltas=None):
+21
View File
@@ -0,0 +1,21 @@
---
title: Resources InvenTree receives
---
The InvenTree project is grateful to receive resources from various vendors free of charge.
Individuals and companies can also support via [GitHub sponsors](https://github.com/sponsors/inventree).
## Current supporters
- [DigitalOcean](https://inventree.org/digitalocean) - Cloud hosting provider, used to host the InvenTree demo instance
- [Crowdin](https://crowdin.com/) - Translation platform, used to manage the [InvenTree translations](../develop/contributing.md#translations) across backend, frontend and app
- [SonarQube Cloud](https://sonarcloud.io/) - Code quality and security analysis, used to track the code quality of the various components
- [Packager.io](https://packager.io/) - Linux package builder/hosting service, used to host the InvenTree debian packages
- [Codecov](https://codecov.io) - Code coverage as a service, used to track the code coverage of the various components
- [Netlify](https://www.netlify.com/) - Static site hosting provider, used to test deploy the frontend and website
- [Depot](https://depot.dev/?utm_source=inventree) - Docker build accelerator, used to build the multi-arch images for the InvenTree docker image
## Past supporters
Non cromprehensive list of past supporters. The project stops consuming resources for various reasons, this does not mean they are not good resources.
- [Coveralls](https://coveralls.io/) - Code coverage as a service
- [Deepsource](https://deepsource.io/) - Code quality and security analysis
+46
View File
@@ -32,6 +32,18 @@ As the `django.db.models.QuerySet` is not a generic class, we would loose type i
Models that implement the `InvenTreeReportMixin` must have an explicit return type annotation for the `report_context` function. Models that implement the `InvenTreeReportMixin` must have an explicit return type annotation for the `report_context` function.
#### INVE-E5
**Rulesets have issues - Backend**
The rulesets used for managing user/group/oAuth permissions have an issue.
This might be caused by an addition or removal of models to the code base. Running the test suit should surface more logs with the error code indicating the exact infractions.
#### INVE-E6
**Scopes have issues - Backend**
The scopes used for oAuth permissions have an issue and do not match the rulesets.
This might be caused by an addition or removal of models to the code base or changes to the rulesets. Running the test suit should surface more logs with the error code indicating the exact infractions.
### INVE-W (InvenTree Warning) ### INVE-W (InvenTree Warning)
Warnings - These are non-critical errors which should be addressed when possible. Warnings - These are non-critical errors which should be addressed when possible.
@@ -52,6 +64,40 @@ See [INVE-W1](#inve-w1)
See [INVE-W1](#inve-w1) See [INVE-W1](#inve-w1)
#### INVE-W4
**Server is running in debug mode - Backend**
InvenTree is running in debug mode. This is **not** recommended for production use, as it exposes sensitive information and makes the server more vulnerable to attacks. Debug mode is not intended for production/exposed instances, **even for short duration**.
It is recommended to run InvenTree in production mode for better security and performance. See [Debug Mode Information](../start/intro.md#debug-mode).
#### INVE-W5
**Background worker process not running - Backend**
The background worker seems to not be running. This is detected by a heartbeat that runs all 5 minutes - this error triggers after not being run in the last 10 minutes.
Check if the process for background workers is running and reaching the database. Steps vary between deployment methods.
See [Background Worker Information](../start/processes.md#background-worker).
#### INVE-W6
**Server restart required - Backend**
The server needs a restart due to changes in settings. Steps very between deployment methods.
#### INVE-W7
**Email settings not configured - Backend**
Not all required settings for sending emails are configured. Not having an email provider configured might lead to degraded processes as password reset, update notifications and user notifications can not work. Setting up email is recommended.
See [Email information](../start/config.md#email-settings).
#### INVE-W8
**Database Migrations required - Backend**
There are database migrations waiting to be applied. This might lead to integrity and availability issues. Applying migrations as soon as possible is recommended.
Some deployment methods support [auto applying of updates](../start/config.md#auto-update). See also [Perform Database Migrations](../start/install.md#perform-database-migrations).
Steps very between deployment methods.
### INVE-I (InvenTree Information) ### INVE-I (InvenTree Information)
Information — These are not errors but information messages. They might point out potential issues or just provide information. Information — These are not errors but information messages. They might point out potential issues or just provide information.
+17
View File
@@ -0,0 +1,17 @@
---
title: Experimental Features
---
## Feature Flags
InvenTree ships with django-flags and enables path (parameter), user, session, date or settings based feature flags. This allows admins to slowly test and roll out new features on their instance without running parallel instances.
Additional flags can be provided via the the `INVENTREE_FLAGS` environment key (see [configuration](../start/config.md#environment-variables)).
Superusers can configure run-time conditions [as per django-flags](https://cfpb.github.io/django-flags/conditions/) docs under `/admin/flags/flagstate/`.
## Current Experimental Features
| Feature | Key | Description |
| ------- | ----------- |
| oAuth provider / api | OIDC | Use oAuth and OIDC to authenticate users with the API - [read more](../api/api.md#oauth2--oidc). |
+1
View File
@@ -38,6 +38,7 @@ Customize settings for search results:
| ---- | ----------- | ------- | ----- | | ---- | ----------- | ------- | ----- |
{{ usersetting("SEARCH_WHOLE") }} {{ usersetting("SEARCH_WHOLE") }}
{{ usersetting("SEARCH_REGEX") }} {{ usersetting("SEARCH_REGEX") }}
{{ usersetting("SEARCH_NOTES") }}
{{ usersetting("SEARCH_PREVIEW_RESULTS") }} {{ usersetting("SEARCH_PREVIEW_RESULTS") }}
{{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }} {{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }}
{{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }} {{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }}
+13 -9
View File
@@ -218,31 +218,35 @@ You can either specify the password directly using `INVENTREE_ADMIN_PASSWORD`, o
!!! info "Administrator Account" !!! info "Administrator Account"
Providing `INVENTREE_ADMIN` credentials will result in the provided account being created with *superuser* permissions when InvenTree is started. Providing `INVENTREE_ADMIN` credentials will result in the provided account being created with *superuser* permissions when InvenTree is started.
## Secret Key ## Secret Key material
InvenTree requires a secret key for providing cryptographic signing - this should be a secret (and unpredictable) value. InvenTree requires secret keys for providing cryptographic signing and oidc private keys- this should be a secret (and unpredictable) value.
!!! info "Auto-Generated Key" !!! info "Auto-Generated material"
If none of the following options are specified, InvenTree will automatically generate a secret key file (stored in `secret_key.txt`) on first run. If none of the following options are specified, InvenTree will automatically generate a secret key file (stored in `secret_key.txt`) and a oidc key file (stored in `oidc.pem`) on first run.
The secret key can be provided in multiple ways, with the following (descending) priorities: The secret key material can be provided in multiple ways, with the following (descending) priorities:
**Pass Secret Key via Environment Variable** **Pass Secret Key Material via Environment Variable**
A secret key string can be passed directly using the environment variable `INVENTREE_SECRET_KEY` A secret key string can be passed directly using the environment variable `INVENTREE_SECRET_KEY`
A oidc private key can be passed directly using the environment variable `INVENTREE_OIDC_PRIVATE_KEY`
**Pass Secret Key File via Environment Variable** **Pass Secret Key Material File via Environment Variable**
A file containing the secret key can be passed via the environment variable `INVENTREE_SECRET_KEY_FILE` A file containing the secret key can be passed via the environment variable `INVENTREE_SECRET_KEY_FILE`
A PEM-encoded file containing the oidc private key can be passed via the environment variable `INVENTREE_OIDC_PRIVATE_KEY_FILE`
**Fallback to Default Secret Key File** **Fallback to Default Secret Key Material**
If not specified via environment variables, the fallback secret_key file (automatically generated as part of InvenTree installation) will be used. If not specified via environment variables, the fallback files (automatically generated as part of InvenTree installation) will be used.
| Environment Variable | Configuration File | Description | Default | | Environment Variable | Configuration File | Description | Default |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| INVENTREE_SECRET_KEY | secret_key | Raw secret key value | *Not specified* | | INVENTREE_SECRET_KEY | secret_key | Raw secret key value | *Not specified* |
| INVENTREE_SECRET_KEY_FILE | secret_key_file | File containing secret key value | *Not specified* | | INVENTREE_SECRET_KEY_FILE | secret_key_file | File containing secret key value | *Not specified* |
| INVENTREE_OIDC_PRIVATE_KEY | oidc_private_key | Raw private key value | *Not specified* |
| INVENTREE_OIDC_PRIVATE_KEY_FILE | oidc_private_key_file | File containing private key value in PEM format | *Not specified* |
## Database Options ## Database Options
+2 -2
View File
@@ -125,9 +125,9 @@ Running in DEBUG mode provides many handy development features, however it is st
So, for a production setup, you should set `INVENTREE_DEBUG=false` in the [configuration options](./config.md). So, for a production setup, you should set `INVENTREE_DEBUG=false` in the [configuration options](./config.md).
### Potential Issues ### Turning Debug Mode off
Turning off DEBUG mode creates further work for the system administrator. In particular, when running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled*, the InvenTree server can no longer run as a monolithic process. When running in DEBUG mode, the InvenTree web server natively manages *static* and *media* files, which means that when DEBUG mode is *disabled*, the proxy setup has to be configured to handle this.
!!! info "Read More" !!! info "Read More"
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details Refer to the [proxy server documentation](./processes.md#proxy-server) for more details
+15 -12
View File
@@ -126,17 +126,18 @@ def define_env(env):
"""Define custom environment variables for the documentation build process.""" """Define custom environment variables for the documentation build process."""
@env.macro @env.macro
def sourcedir(dirname, branch=None): def sourcedir(dirname: str, branch=None):
"""Return a link to a directory within the source code repository. """Return a link to a directory within the source code repository.
Arguments: Arguments:
- dirname: The name of the directory to link to (relative to the top-level directory) dirname: The name of the directory to link to (relative to the top-level directory)
branch: The branch of the repository to link to (defaults to the current build environment)
Returns: Returns:
- A fully qualified URL to the source code directory on GitHub A fully qualified URL to the source code directory on GitHub
Raises: Raises:
- FileNotFoundError: If the directory does not exist, or the generated URL is invalid FileNotFoundError: If the directory does not exist, or the generated URL is invalid
""" """
if branch == None: if branch == None:
branch = get_build_environment() branch = get_build_environment()
@@ -169,13 +170,15 @@ def define_env(env):
"""Return a link to a file within the source code repository. """Return a link to a file within the source code repository.
Arguments: Arguments:
- filename: The name of the file to link to (relative to the top-level directory) filename: The name of the file to link to (relative to the top-level directory)
branch: The branch of the repository to link to (defaults to the current build environment)
raw: If True, return the raw URL to the file (defaults to False)
Returns: Returns:
- A fully qualified URL to the source code file on GitHub A fully qualified URL to the source code file on GitHub
Raises: Raises:
- FileNotFoundError: If the file does not exist, or the generated URL is invalid FileNotFoundError: If the file does not exist, or the generated URL is invalid
""" """
if branch == None: if branch == None:
branch = get_build_environment() branch = get_build_environment()
@@ -247,9 +250,9 @@ def define_env(env):
"""Include a file in the documentation, in a 'collapse' block. """Include a file in the documentation, in a 'collapse' block.
Arguments: Arguments:
- filename: The name of the file to include (relative to the top-level directory) filename: The name of the file to include (relative to the top-level directory)
- title: title: The title of the collapse block in the documentation
- fmt: fmt: The format of the included file (e.g., 'python', 'html', etc.)
""" """
here = os.path.dirname(__file__) here = os.path.dirname(__file__)
path = os.path.join(here, '..', filename) path = os.path.join(here, '..', filename)
@@ -299,7 +302,7 @@ def define_env(env):
"""Extract information on a particular global setting. """Extract information on a particular global setting.
Arguments: Arguments:
- key: The name of the global setting to extract information for. key: The name of the global setting to extract information for.
""" """
global GLOBAL_SETTINGS global GLOBAL_SETTINGS
setting = GLOBAL_SETTINGS[key] setting = GLOBAL_SETTINGS[key]
@@ -311,7 +314,7 @@ def define_env(env):
"""Extract information on a particular user setting. """Extract information on a particular user setting.
Arguments: Arguments:
- key: The name of the user setting to extract information for. key: The name of the user setting to extract information for.
""" """
global USER_SETTINGS global USER_SETTINGS
setting = USER_SETTINGS[key] setting = USER_SETTINGS[key]
+2
View File
@@ -89,6 +89,7 @@ nav:
- Project: - Project:
- Governance: project/governance.md - Governance: project/governance.md
- Project Security: project/security.md - Project Security: project/security.md
- Resources: project/resources.md
- Security: security.md - Security: security.md
- Install: - Install:
- Introduction: start/intro.md - Introduction: start/intro.md
@@ -160,6 +161,7 @@ nav:
- Single Sign on: settings/SSO.md - Single Sign on: settings/SSO.md
- Multi Factor Authentication: settings/MFA.md - Multi Factor Authentication: settings/MFA.md
- Email: settings/email.md - Email: settings/email.md
- Experimental Features: settings/experimental.md
- Export Data: settings/export.md - Export Data: settings/export.md
- Import Data: settings/import.md - Import Data: settings/import.md
- Operations: - Operations:
+1 -1
View File
@@ -5,4 +5,4 @@ mkdocs-git-revision-date-localized-plugin>=1.1,<2.0
mkdocs-simple-hooks>=0.1,<1.0 mkdocs-simple-hooks>=0.1,<1.0
mkdocs-include-markdown-plugin mkdocs-include-markdown-plugin
neoteroi-mkdocs neoteroi-mkdocs
mkdocstrings[python]>=0.25.0,<=0.29.0 mkdocstrings[python]>=0.25.0,<=0.29.1
+6 -7
View File
@@ -331,9 +331,9 @@ mkdocs-macros-plugin==1.3.7 \
--hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \ --hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \
--hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523 --hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523
# via -r docs/requirements.in # via -r docs/requirements.in
mkdocs-material==9.6.9 \ mkdocs-material==9.6.11 \
--hash=sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1 \ --hash=sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff \
--hash=sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c --hash=sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263
# via -r docs/requirements.in # via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \ mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
@@ -343,9 +343,9 @@ mkdocs-simple-hooks==0.1.5 \
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \ --hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a --hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
# via -r docs/requirements.in # via -r docs/requirements.in
mkdocstrings[python]==0.29.0 \ mkdocstrings[python]==0.29.1 \
--hash=sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e \ --hash=sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6 \
--hash=sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d --hash=sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42
# via # via
# -r docs/requirements.in # -r docs/requirements.in
# mkdocstrings-python # mkdocstrings-python
@@ -497,7 +497,6 @@ typing-extensions==4.12.2 \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via # via
# anyio # anyio
# mkdocstrings
# mkdocstrings-python # mkdocstrings-python
# rich # rich
urllib3==2.3.0 \ urllib3==2.3.0 \
+7 -3
View File
@@ -18,11 +18,16 @@ src = ["src/backend/InvenTree"]
"__init__.py" = ["D104"] "__init__.py" = ["D104"]
[tool.ruff.lint] [tool.ruff.lint]
select = ["A", "B", "C", "C4", "D", "F", "I", "N", "SIM", "PIE", "PLE", "PLW", "RUF", "UP", "W"] select = ["A", "B", "C", "C4", "D", "F", "I", "N", "SIM", "PIE", "PLE", "PLW", "RUF", "UP", "W",
#"DOC201", "DOC202", # enforce return docs
"DOC402","DOC403", # enforce yield docs
#"DOC501","DOC502", # enforce raise
]
# Things that should be enabled in the future: # Things that should be enabled in the future:
# - LOG # - LOG
# - DJ # for Django stuff # - DJ # for Django stuff
# - S # for security stuff (bandit) # - S # for security stuff (bandit)
# - D401 - Imperative docstrings
ignore = [ ignore = [
"PLE1205", "PLE1205",
@@ -51,8 +56,6 @@ ignore = [
"N806", "N806",
# - N812 - lowercase imported as non-lowercase # - N812 - lowercase imported as non-lowercase
"N812", "N812",
# - D417 Missing argument descriptions in the docstring
"D417",
# - RUF032 - decimal-from-float-literal # - RUF032 - decimal-from-float-literal
"RUF032", "RUF032",
"RUF045", "RUF045",
@@ -69,6 +72,7 @@ ignore = [
[tool.ruff.lint.pydocstyle] [tool.ruff.lint.pydocstyle]
convention = "google" convention = "google"
ignore-var-parameters = true
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
split-on-trailing-comma = false split-on-trailing-comma = false
+62 -54
View File
@@ -1,7 +1,6 @@
"""Main JSON interface views.""" """Main JSON interface views."""
import json import json
import sys
from pathlib import Path from pathlib import Path
from django.conf import settings from django.conf import settings
@@ -11,24 +10,22 @@ from django.utils.translation import gettext_lazy as _
import structlog import structlog
from django_q.models import OrmQ from django_q.models import OrmQ
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import permissions, serializers from rest_framework import serializers
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.views import APIView from rest_framework.views import APIView
import common.models
import InvenTree.version import InvenTree.version
import users.models
from common.settings import get_global_setting from common.settings import get_global_setting
from InvenTree import helpers, ready from InvenTree import helpers
from InvenTree.auth_overrides import registration_enabled from InvenTree.auth_overrides import registration_enabled
from InvenTree.mixins import ListCreateAPI from InvenTree.mixins import ListCreateAPI
from InvenTree.sso import sso_registration_enabled from InvenTree.sso import sso_registration_enabled
from part.models import Part
from plugin.serializers import MetadataSerializer from plugin.serializers import MetadataSerializer
from users.models import ApiToken from users.models import ApiToken
from users.permissions import check_user_permission
from .helpers import plugins_info from .helpers import plugins_info
from .helpers_email import is_email_configured from .helpers_email import is_email_configured
@@ -81,8 +78,8 @@ def read_license_file(path: Path) -> list:
class LicenseViewSerializer(serializers.Serializer): class LicenseViewSerializer(serializers.Serializer):
"""Serializer for license information.""" """Serializer for license information."""
backend = serializers.CharField(help_text='Backend licenses texts', read_only=True) backend = serializers.ListField(help_text='Backend licenses texts', read_only=True)
frontend = serializers.CharField( frontend = serializers.ListField(
help_text='Frontend licenses texts', read_only=True help_text='Frontend licenses texts', read_only=True
) )
@@ -90,7 +87,7 @@ class LicenseViewSerializer(serializers.Serializer):
class LicenseView(APIView): class LicenseView(APIView):
"""Simple JSON endpoint for InvenTree license information.""" """Simple JSON endpoint for InvenTree license information."""
permission_classes = [permissions.IsAuthenticated] permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
@extend_schema(responses={200: OpenApiResponse(response=LicenseViewSerializer)}) @extend_schema(responses={200: OpenApiResponse(response=LicenseViewSerializer)})
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -115,7 +112,7 @@ class VersionViewSerializer(serializers.Serializer):
api = serializers.IntegerField() api = serializers.IntegerField()
commit_hash = serializers.CharField() commit_hash = serializers.CharField()
commit_date = serializers.CharField() commit_date = serializers.CharField()
commit_branch = serializers.CharField() commit_branch = serializers.CharField(allow_null=True)
python = serializers.CharField() python = serializers.CharField()
django = serializers.CharField() django = serializers.CharField()
@@ -124,7 +121,6 @@ class VersionViewSerializer(serializers.Serializer):
doc = serializers.URLField() doc = serializers.URLField()
code = serializers.URLField() code = serializers.URLField()
credit = serializers.URLField()
app = serializers.URLField() app = serializers.URLField()
bug = serializers.URLField() bug = serializers.URLField()
@@ -137,7 +133,7 @@ class VersionViewSerializer(serializers.Serializer):
class VersionView(APIView): class VersionView(APIView):
"""Simple JSON endpoint for InvenTree version information.""" """Simple JSON endpoint for InvenTree version information."""
permission_classes = [permissions.IsAdminUser] permission_classes = [InvenTree.permissions.IsAdminOrAdminScope]
@extend_schema(responses={200: OpenApiResponse(response=VersionViewSerializer)}) @extend_schema(responses={200: OpenApiResponse(response=VersionViewSerializer)})
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@@ -157,7 +153,6 @@ class VersionView(APIView):
'links': { 'links': {
'doc': InvenTree.version.inventreeDocUrl(), 'doc': InvenTree.version.inventreeDocUrl(),
'code': InvenTree.version.inventreeGithubUrl(), 'code': InvenTree.version.inventreeGithubUrl(),
'credit': InvenTree.version.inventreeCreditsUrl(),
'app': InvenTree.version.inventreeAppUrl(), 'app': InvenTree.version.inventreeAppUrl(),
'bug': f'{InvenTree.version.inventreeGithubUrl()}issues', 'bug': f'{InvenTree.version.inventreeGithubUrl()}issues',
}, },
@@ -168,9 +163,9 @@ class VersionInformationSerializer(serializers.Serializer):
"""Serializer for a single version.""" """Serializer for a single version."""
version = serializers.CharField() version = serializers.CharField()
date = serializers.CharField() date = serializers.DateField()
gh = serializers.CharField() gh = serializers.CharField(allow_null=True)
text = serializers.CharField() text = serializers.ListField(child=serializers.CharField())
latest = serializers.BooleanField() latest = serializers.BooleanField()
class Meta: class Meta:
@@ -179,23 +174,44 @@ class VersionInformationSerializer(serializers.Serializer):
fields = '__all__' fields = '__all__'
class VersionApiSerializer(serializers.Serializer): @extend_schema(
"""Serializer for the version api endpoint.""" parameters=[
OpenApiParameter(
VersionInformationSerializer(many=True) name='versions',
type=int,
description='Number of versions to return.',
default=10,
),
OpenApiParameter(
name='start_version',
type=int,
description='First version to report. Defaults to return the latest {versions} versions.',
),
]
)
class VersionTextView(ListAPI): class VersionTextView(ListAPI):
"""Simple JSON endpoint for InvenTree version text.""" """Simple JSON endpoint for InvenTree version text."""
serializer_class = VersionInformationSerializer serializer_class = VersionInformationSerializer
permission_classes = [permissions.IsAdminUser] permission_classes = [InvenTree.permissions.IsAdminOrAdminScope]
# Specifically disable pagination for this view
pagination_class = None
@extend_schema(responses={200: OpenApiResponse(response=VersionApiSerializer)})
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
"""Return information about the InvenTree server.""" """Return information about the InvenTree server."""
return JsonResponse(inventreeApiText()) versions = request.query_params.get('versions')
start_version = request.query_params.get('start_version')
api_kwargs = {}
if versions is not None:
api_kwargs['versions'] = int(versions)
if start_version is not None:
api_kwargs['start_version'] = int(start_version)
version_data = inventreeApiText(**api_kwargs)
return JsonResponse(list(version_data.values()), safe=False)
class InfoApiSerializer(serializers.Serializer): class InfoApiSerializer(serializers.Serializer):
@@ -249,7 +265,7 @@ class InfoView(APIView):
Use to confirm that the server is running, etc. Use to confirm that the server is running, etc.
""" """
permission_classes = [permissions.AllowAny] permission_classes = [InvenTree.permissions.AllowAnyOrReadScope]
def worker_pending_tasks(self): def worker_pending_tasks(self):
"""Return the current number of outstanding background tasks.""" """Return the current number of outstanding background tasks."""
@@ -332,7 +348,7 @@ class InfoView(APIView):
class NotFoundView(APIView): class NotFoundView(APIView):
"""Simple JSON view when accessing an invalid API view.""" """Simple JSON view when accessing an invalid API view."""
permission_classes = [permissions.AllowAny] permission_classes = [InvenTree.permissions.AllowAnyOrReadScope]
def not_found(self, request): def not_found(self, request):
"""Return a 404 error.""" """Return a 404 error."""
@@ -582,6 +598,7 @@ class APISearchViewSerializer(serializers.Serializer):
search = serializers.CharField() search = serializers.CharField()
search_regex = serializers.BooleanField(default=False, required=False) search_regex = serializers.BooleanField(default=False, required=False)
search_whole = serializers.BooleanField(default=False, required=False) search_whole = serializers.BooleanField(default=False, required=False)
search_notes = serializers.BooleanField(default=False, required=False)
limit = serializers.IntegerField(default=1, required=False) limit = serializers.IntegerField(default=1, required=False)
offset = serializers.IntegerField(default=0, required=False) offset = serializers.IntegerField(default=0, required=False)
@@ -595,7 +612,7 @@ class APISearchView(GenericAPIView):
Is much more efficient and simplifies code! Is much more efficient and simplifies code!
""" """
permission_classes = [permissions.IsAuthenticated] permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = APISearchViewSerializer serializer_class = APISearchViewSerializer
def get_result_types(self): def get_result_types(self):
@@ -643,6 +660,7 @@ class APISearchView(GenericAPIView):
'search': '', 'search': '',
'search_regex': False, 'search_regex': False,
'search_whole': False, 'search_whole': False,
'search_notes': False,
'limit': 1, 'limit': 1,
'offset': 0, 'offset': 0,
} }
@@ -681,14 +699,9 @@ class APISearchView(GenericAPIView):
# Check permissions and update results dict with particular query # Check permissions and update results dict with particular query
model = view.serializer_class.Meta.model model = view.serializer_class.Meta.model
app_label = model._meta.app_label
model_name = model._meta.model_name
table = f'{app_label}_{model_name}'
try: try:
if users.models.RuleSet.check_table_permission( if check_user_permission(request.user, model, 'view'):
request.user, table, 'view'
):
results[key] = view.list(request, *args, **kwargs).data results[key] = view.list(request, *args, **kwargs).data
else: else:
results[key] = { results[key] = {
@@ -705,37 +718,32 @@ class APISearchView(GenericAPIView):
class MetadataView(RetrieveUpdateAPI): class MetadataView(RetrieveUpdateAPI):
"""Generic API endpoint for reading and editing metadata for a model.""" """Generic API endpoint for reading and editing metadata for a model."""
MODEL_REF = 'model' model = None # Placeholder for the model class
def get_model_type(self):
"""Return the model type associated with this API instance."""
model = self.kwargs.get(self.MODEL_REF, None)
if ready.isGeneratingSchema():
model = common.models.ProjectCode
if 'lookup_field' in self.kwargs:
# Set custom lookup field (instead of default 'pk' value) if supplied
self.lookup_field = self.kwargs.pop('lookup_field')
@classmethod
def as_view(cls, model, lookup_field=None, **initkwargs):
"""Override to ensure model specific rendering."""
if model is None: if model is None:
raise ValidationError( raise ValidationError(
f"MetadataView called without '{self.MODEL_REF}' parameter" "MetadataView defined without 'model' arg"
) # pragma: no cover ) # pragma: no cover
initkwargs['model'] = model
return model # Set custom lookup field (instead of default 'pk' value) if supplied
if lookup_field:
initkwargs['lookup_field'] = lookup_field
return super().as_view(**initkwargs)
def get_permission_model(self): def get_permission_model(self):
"""Return the 'permission' model associated with this view.""" """Return the 'permission' model associated with this view."""
return self.get_model_type() return self.model
def get_queryset(self): def get_queryset(self):
"""Return the queryset for this endpoint.""" """Return the queryset for this endpoint."""
return self.get_model_type().objects.all() return self.model.objects.all()
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
"""Return MetadataSerializer instance.""" """Return MetadataSerializer instance."""
# Detect if we are currently generating the OpenAPI schema # Detect if we are currently generating the OpenAPI schema
if 'spectacular' in sys.argv: return MetadataSerializer(self.model, *args, **kwargs)
return MetadataSerializer(Part, *args, **kwargs) # pragma: no cover
return MetadataSerializer(self.get_model_type(), *args, **kwargs)
+188 -162
View File
@@ -1,581 +1,607 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 331 INVENTREE_API_VERSION = 338
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v331 - 2025-04-01 : https://github.com/inventree/InvenTree/pull/9437
v338 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9333
- Adds oAuth2 support for the API
v337 -> 2025-04-15 : https://github.com/inventree/InvenTree/pull/9505
- Adds API endpoint with extra serial number information for a given StockItem object
v336 -> 2025-04-10 : https://github.com/inventree/InvenTree/pull/9492
- Fixed query and response serialization for units_all and version_text
- Fixed LicenseView and VersionInformation serialization
v335 -> 2025-04-09 : https://github.com/inventree/InvenTree/pull/9476
- Adds "roles" detail to the Group API endpoint
- Adds "users" detail to the Group API endpoint
- Adds "groups" detail to the User API endpoint
v334 -> 2025-04-08 : https://github.com/inventree/InvenTree/pull/9453
- Fixes various operationId and enum collisions and help texts
v333 -> 2025-04-03 : https://github.com/inventree/InvenTree/pull/9452
- Currency string is no longer restricted to a hardcoded enum
- Customizable status keys are no longer hardcoded enum values
v332 -> 2025-04-02 : https://github.com/inventree/InvenTree/pull/9393
- Adds 'search_notes' parameter to all searchable API endpoints
v331 -> 2025-04-01 : https://github.com/inventree/InvenTree/pull/9437
- Set correct types on various formerly-string PK fields as well permissions - Set correct types on various formerly-string PK fields as well permissions
- Include metadata request and response types - Include metadata request and response types
v330 - 2025-03-31 : https://github.com/inventree/InvenTree/pull/9420 v330 -> 2025-03-31 : https://github.com/inventree/InvenTree/pull/9420
- Deconflict operation id between single and bulk destroy operations - Deconflict operation id between single and bulk destroy operations
- Add request body definition for bulk destroy operations - Add request body definition for bulk destroy operations
v329 - 2025-03-30 : https://github.com/inventree/InvenTree/pull/9399 v329 -> 2025-03-30 : https://github.com/inventree/InvenTree/pull/9399
- Convert url path regex-specified PKs to int - Convert url path regex-specified PKs to int
v228 - 2025-03-29 : https://github.com/inventree/InvenTree/pull/9407 v328 -> 2025-03-29 : https://github.com/inventree/InvenTree/pull/9407
- Updates schema to include paging arguments - Updates schema to include paging arguments
v327 - 2025-03-20 : https://github.com/inventree/InvenTree/pull/9339 v327 -> 2025-03-20 : https://github.com/inventree/InvenTree/pull/9339
- Adds "is_mandatory" field to the Plugin API - Adds "is_mandatory" field to the Plugin API
- Adds ability to filter by "mandatory" status in the Plugin API - Adds ability to filter by "mandatory" status in the Plugin API
v326 - 2025-03-18 : https://github.com/inventree/InvenTree/pull/9096 v326 -> 2025-03-18 : https://github.com/inventree/InvenTree/pull/9096
- Overhaul the data-export API functionality - Overhaul the data-export API functionality
- Allow customization of data exporting via plugins - Allow customization of data exporting via plugins
- Consolidate LabelOutput and ReportOutput API endpoints into single DataOutput endpoint - Consolidate LabelOutput and ReportOutput API endpoints into single DataOutput endpoint
v325 - 2024-03-17 : https://github.com/inventree/InvenTree/pull/9244 v325 -> 2024-03-17 : https://github.com/inventree/InvenTree/pull/9244
- Adds the option for superusers to list all user tokens - Adds the option for superusers to list all user tokens
- Make list endpoints sortable, filterable and searchable - Make list endpoints sortable, filterable and searchable
v324 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9320 v324 -> 2025-03-17 : https://github.com/inventree/InvenTree/pull/9320
- Adds BulkUpdate support for the SalesOrderAllocation model - Adds BulkUpdate support for the SalesOrderAllocation model
- Adds BulkUpdate support for the PartCategory model - Adds BulkUpdate support for the PartCategory model
- Adds BulkUpdate support for the StockLocation model - Adds BulkUpdate support for the StockLocation model
v323 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9313 v323 -> 2025-03-17 : https://github.com/inventree/InvenTree/pull/9313
- Adds BulkUpdate support to the Part API endpoint - Adds BulkUpdate support to the Part API endpoint
- Remove legacy API endpoint to set part category for multiple parts - Remove legacy API endpoint to set part category for multiple parts
v322 - 2025-03-16 : https://github.com/inventree/InvenTree/pull/8933 v322 -> 2025-03-16 : https://github.com/inventree/InvenTree/pull/8933
- Add min_date and max_date query filters for orders, for use in calendar views - Add min_date and max_date query filters for orders, for use in calendar views
v321 - 2025-03-06 : https://github.com/inventree/InvenTree/pull/9236 v321 -> 2025-03-06 : https://github.com/inventree/InvenTree/pull/9236
- Adds conditionally-returned fields to the schema to match API behavior - Adds conditionally-returned fields to the schema to match API behavior
- Removes required flag for nullable read-only fields to match API behavior - Removes required flag for nullable read-only fields to match API behavior
v320 - 2025-03-05 : https://github.com/inventree/InvenTree/pull/9243 v320 -> 2025-03-05 : https://github.com/inventree/InvenTree/pull/9243
- Link fields are now up to 2000 chars long - Link fields are now up to 2000 chars long
v319 - 2025-03-04 : https://github.com/inventree/InvenTree/pull/9199 v319 -> 2025-03-04 : https://github.com/inventree/InvenTree/pull/9199
- Add detail API endpoint for the LabelOutput model - Add detail API endpoint for the LabelOutput model
- Add detail API endpoint for the ReportOutput model - Add detail API endpoint for the ReportOutput model
v318 - 2025-02-25 : https://github.com/inventree/InvenTree/pull/9116 v318 -> 2025-02-25 : https://github.com/inventree/InvenTree/pull/9116
- Adds user profile API endpoints - Adds user profile API endpoints
v317 - 2025-02-26 : https://github.com/inventree/InvenTree/pull/9143 v317 -> 2025-02-26 : https://github.com/inventree/InvenTree/pull/9143
- Default 'overdue' field to False in Build serializer - Default 'overdue' field to False in Build serializer
- Add allow_null to various fields in Build, Settings, Order, Part, and Stock serializers - Add allow_null to various fields in Build, Settings, Order, Part, and Stock serializers
- Add type hints to Users model to properly type fields - Add type hints to Users model to properly type fields
v316 - 2025-02-26 : https://github.com/inventree/InvenTree/pull/9185 v316 -> 2025-02-26 : https://github.com/inventree/InvenTree/pull/9185
- Allow 'icon' field to be nullified in the PartCategory API - Allow 'icon' field to be nullified in the PartCategory API
- Allow 'custom_icon' field to be nullified in the StockLocation API - Allow 'custom_icon' field to be nullified in the StockLocation API
v315 - 2025-02-22 : https://github.com/inventree/InvenTree/pull/9150 v315 -> 2025-02-22 : https://github.com/inventree/InvenTree/pull/9150
- Remove outdated 'url' field from some API endpoints - Remove outdated 'url' field from some API endpoints
v314 - 2025-02-17 : https://github.com/inventree/InvenTree/pull/6293 v314 -> 2025-02-17 : https://github.com/inventree/InvenTree/pull/6293
- Removes a considerable amount of old auth endpoints - Removes a considerable amount of old auth endpoints
- Introduces allauth-provided auth endpoints - Introduces allauth-provided auth endpoints
v313 - 2025-02-17 : https://github.com/inventree/InvenTree/pull/9087 v313 -> 2025-02-17 : https://github.com/inventree/InvenTree/pull/9087
- Adds instance id optionally to the info view endpoint - Adds instance id optionally to the info view endpoint
v312 - 2025-02-15 : https://github.com/inventree/InvenTree/pull/9079 v312 -> 2025-02-15 : https://github.com/inventree/InvenTree/pull/9079
- Remove old API endpoints associated with legacy BOM import functionality - Remove old API endpoints associated with legacy BOM import functionality
v311 - 2025-02-14 : https://github.com/inventree/InvenTree/pull/9076 v311 -> 2025-02-14 : https://github.com/inventree/InvenTree/pull/9076
- Adds "model_filters" attribute to settings API - Adds "model_filters" attribute to settings API
v310 - 2025-02-14 : https://github.com/inventree/InvenTree/pull/9077 v310 -> 2025-02-14 : https://github.com/inventree/InvenTree/pull/9077
- Adds 'is_variant' filter to the Part list API - Adds 'is_variant' filter to the Part list API
v309 - 2025-02-02 : https://github.com/inventree/InvenTree/pull/9008 v309 -> 2025-02-02 : https://github.com/inventree/InvenTree/pull/9008
- Bug fixes for the "Part" serializer - Bug fixes for the "Part" serializer
- Fixes for data import API endpoints - Fixes for data import API endpoints
v308 - 2025-02-01 : https://github.com/inventree/InvenTree/pull/9003 v308 -> 2025-02-01 : https://github.com/inventree/InvenTree/pull/9003
- Adds extra detail to the ReportOutput and LabelOutput API endpoints - Adds extra detail to the ReportOutput and LabelOutput API endpoints
- Allows ordering of output list endpoints - Allows ordering of output list endpoints
v307 - 2025-01-29 : https://github.com/inventree/InvenTree/pull/8969 v307 -> 2025-01-29 : https://github.com/inventree/InvenTree/pull/8969
- Extend Info Endpoint to include customizations - Extend Info Endpoint to include customizations
v306 - 2025-01-28 : https://github.com/inventree/InvenTree/pull/8966 v306 -> 2025-01-28 : https://github.com/inventree/InvenTree/pull/8966
- Adds "start_date" to PurchasesOrder API - Adds "start_date" to PurchasesOrder API
- Adds "start_date" to SalesOrder API - Adds "start_date" to SalesOrder API
- Adds "start_date" to ReturnOrder API - Adds "start_date" to ReturnOrder API
- Updated API filters - Updated API filters
v305 - 2025-01-26 : https://github.com/inventree/InvenTree/pull/8950 v305 -> 2025-01-26 : https://github.com/inventree/InvenTree/pull/8950
- Bug fixes for the SupplierPart API - Bug fixes for the SupplierPart API
- Refactoring for data export via API - Refactoring for data export via API
v304 - 2025-01-22 : https://github.com/inventree/InvenTree/pull/8940 v304 -> 2025-01-22 : https://github.com/inventree/InvenTree/pull/8940
- Adds "category" filter to build list API - Adds "category" filter to build list API
v303 - 2025-01-20 : https://github.com/inventree/InvenTree/pull/8915 v303 -> 2025-01-20 : https://github.com/inventree/InvenTree/pull/8915
- Adds "start_date" field to Build model and API endpoints - Adds "start_date" field to Build model and API endpoints
- Adds additional API filtering and sorting options for Build list - Adds additional API filtering and sorting options for Build list
v302 - 2025-01-18 : https://github.com/inventree/InvenTree/pull/8905 v302 -> 2025-01-18 : https://github.com/inventree/InvenTree/pull/8905
- Fix schema definition on the /label/print endpoint - Fix schema definition on the /label/print endpoint
v301 - 2025-01-14 : https://github.com/inventree/InvenTree/pull/8894 v301 -> 2025-01-14 : https://github.com/inventree/InvenTree/pull/8894
- Remove ui preferences from the API - Remove ui preferences from the API
v300 - 2025-01-13 : https://github.com/inventree/InvenTree/pull/8886 v300 -> 2025-01-13 : https://github.com/inventree/InvenTree/pull/8886
- Allow null value for 'expiry_date' field introduced in #8867 - Allow null value for 'expiry_date' field introduced in #8867
v299 - 2025-01-10 : https://github.com/inventree/InvenTree/pull/8867 v299 -> 2025-01-10 : https://github.com/inventree/InvenTree/pull/8867
- Adds 'expiry_date' field to the PurchaseOrderReceive API endpoint - Adds 'expiry_date' field to the PurchaseOrderReceive API endpoint
- Adds 'default_expiry` field to the PartBriefSerializer, affecting API endpoints which use it - Adds 'default_expiry` field to the PartBriefSerializer, affecting API endpoints which use it
v298 - 2025-01-07 : https://github.com/inventree/InvenTree/pull/8848 v298 -> 2025-01-07 : https://github.com/inventree/InvenTree/pull/8848
- Adds 'created_by' field to PurchaseOrder API endpoints - Adds 'created_by' field to PurchaseOrder API endpoints
- Adds 'created_by' field to SalesOrder API endpoints - Adds 'created_by' field to SalesOrder API endpoints
- Adds 'created_by' field to ReturnOrder API endpoints - Adds 'created_by' field to ReturnOrder API endpoints
v297 - 2024-12-29 : https://github.com/inventree/InvenTree/pull/8438 v297 -> 2024-12-29 : https://github.com/inventree/InvenTree/pull/8438
- Adjustments to the CustomUserState API endpoints and serializers - Adjustments to the CustomUserState API endpoints and serializers
v296 - 2024-12-25 : https://github.com/inventree/InvenTree/pull/8732 v296 -> 2024-12-25 : https://github.com/inventree/InvenTree/pull/8732
- Adjust default "part_detail" behavior for StockItem API endpoints - Adjust default "part_detail" behavior for StockItem API endpoints
v295 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8746 v295 -> 2024-12-23 : https://github.com/inventree/InvenTree/pull/8746
- Improve API documentation for build APIs - Improve API documentation for build APIs
v294 - 2024-12-23 : https://github.com/inventree/InvenTree/pull/8738 v294 -> 2024-12-23 : https://github.com/inventree/InvenTree/pull/8738
- Extends registration API documentation - Extends registration API documentation
v293 - 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658 v293 -> 2024-12-14 : https://github.com/inventree/InvenTree/pull/8658
- Adds new fields to the supplier barcode API endpoints - Adds new fields to the supplier barcode API endpoints
v292 - 2024-12-03 : https://github.com/inventree/InvenTree/pull/8625 v292 -> 2024-12-03 : https://github.com/inventree/InvenTree/pull/8625
- Add "on_order" and "in_stock" annotations to SupplierPart API - Add "on_order" and "in_stock" annotations to SupplierPart API
- Enhanced filtering for the SupplierPart API - Enhanced filtering for the SupplierPart API
v291 - 2024-11-30 : https://github.com/inventree/InvenTree/pull/8596 v291 -> 2024-11-30 : https://github.com/inventree/InvenTree/pull/8596
- Allow null / empty values for plugin settings - Allow null / empty values for plugin settings
v290 - 2024-11-29 : https://github.com/inventree/InvenTree/pull/8590 v290 -> 2024-11-29 : https://github.com/inventree/InvenTree/pull/8590
- Adds "quantity" field to ReturnOrderLineItem model and API - Adds "quantity" field to ReturnOrderLineItem model and API
v289 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570 v289 -> 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570
- Enable status change when transferring stock items - Enable status change when transferring stock items
v288 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8574 v288 -> 2024-11-27 : https://github.com/inventree/InvenTree/pull/8574
- Adds "consumed" filter to StockItem API - Adds "consumed" filter to StockItem API
v287 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8571 v287 -> 2024-11-27 : https://github.com/inventree/InvenTree/pull/8571
- Adds ability to set stock status when returning items from a customer - Adds ability to set stock status when returning items from a customer
v286 - 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054 v286 -> 2024-11-26 : https://github.com/inventree/InvenTree/pull/8054
- Adds "SelectionList" and "SelectionListEntry" API endpoints - Adds "SelectionList" and "SelectionListEntry" API endpoints
v285 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8559 v285 -> 2024-11-25 : https://github.com/inventree/InvenTree/pull/8559
- Adds better description for registration endpoints - Adds better description for registration endpoints
v284 - 2024-11-25 : https://github.com/inventree/InvenTree/pull/8544 v284 -> 2024-11-25 : https://github.com/inventree/InvenTree/pull/8544
- Adds new date filters to the StockItem API - Adds new date filters to the StockItem API
- Adds new date filters to the BuildOrder API - Adds new date filters to the BuildOrder API
- Adds new date filters to the SalesOrder API - Adds new date filters to the SalesOrder API
- Adds new date filters to the PurchaseOrder API - Adds new date filters to the PurchaseOrder API
- Adds new date filters to the ReturnOrder API - Adds new date filters to the ReturnOrder API
v283 - 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524 v283 -> 2024-11-20 : https://github.com/inventree/InvenTree/pull/8524
- Adds "note" field to the PartRelated API endpoint - Adds "note" field to the PartRelated API endpoint
v282 - 2024-11-19 : https://github.com/inventree/InvenTree/pull/8487 v282 -> 2024-11-19 : https://github.com/inventree/InvenTree/pull/8487
- Remove the "test statistics" API endpoints - Remove the "test statistics" API endpoints
- This is now provided via a custom plugin - This is now provided via a custom plugin
v281 - 2024-11-15 : https://github.com/inventree/InvenTree/pull/8480 v281 -> 2024-11-15 : https://github.com/inventree/InvenTree/pull/8480
- Fixes StockHistory API data serialization - Fixes StockHistory API data serialization
v280 - 2024-11-10 : https://github.com/inventree/InvenTree/pull/8461 v280 -> 2024-11-10 : https://github.com/inventree/InvenTree/pull/8461
- Makes schema for API information endpoint more informing - Makes schema for API information endpoint more informing
- Removes general not found endpoint - Removes general not found endpoint
v279 - 2024-11-09 : https://github.com/inventree/InvenTree/pull/8458 v279 -> 2024-11-09 : https://github.com/inventree/InvenTree/pull/8458
- Adds "order_outstanding" and "part" filters to the BuildLine API endpoint - Adds "order_outstanding" and "part" filters to the BuildLine API endpoint
- Adds "order_outstanding" filter to the SalesOrderLineItem API endpoint - Adds "order_outstanding" filter to the SalesOrderLineItem API endpoint
v278 - 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445 v278 -> 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445
- Updates to the SalesOrder API endpoints - Updates to the SalesOrder API endpoints
- Add "shipment count" information to the SalesOrder API endpoints - Add "shipment count" information to the SalesOrder API endpoints
- Allow null value for SalesOrderAllocation.shipment field - Allow null value for SalesOrderAllocation.shipment field
- Additional filtering options for allocation endpoints - Additional filtering options for allocation endpoints
v277 - 2024-11-01 : https://github.com/inventree/InvenTree/pull/8278 v277 -> 2024-11-01 : https://github.com/inventree/InvenTree/pull/8278
- Allow build order list to be filtered by "outstanding" (alias for "active") - Allow build order list to be filtered by "outstanding" (alias for "active")
v276 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403 v276 -> 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403
- Adds 'destination' field to the PurchaseOrder model and API endpoints - Adds 'destination' field to the PurchaseOrder model and API endpoints
v275 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396 v275 -> 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
- Adds SKU and MPN fields to the StockItem serializer - Adds SKU and MPN fields to the StockItem serializer
- Additional export options for the StockItem serializer - Additional export options for the StockItem serializer
v274 - 2024-10-29 : https://github.com/inventree/InvenTree/pull/8392 v274 -> 2024-10-29 : https://github.com/inventree/InvenTree/pull/8392
- Add more detailed information to NotificationEntry API serializer - Add more detailed information to NotificationEntry API serializer
v273 - 2024-10-28 : https://github.com/inventree/InvenTree/pull/8376 v273 -> 2024-10-28 : https://github.com/inventree/InvenTree/pull/8376
- Fixes for the BuildLine API endpoint - Fixes for the BuildLine API endpoint
v272 - 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343 v272 -> 2024-10-25 : https://github.com/inventree/InvenTree/pull/8343
- Adjustments to BuildLine API serializers - Adjustments to BuildLine API serializers
v271 - 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331 v271 -> 2024-10-22 : https://github.com/inventree/InvenTree/pull/8331
- Fixes for SalesOrderLineItem endpoints - Fixes for SalesOrderLineItem endpoints
v270 - 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307 v270 -> 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307
- Adds missing date fields from order API endpoint(s) - Adds missing date fields from order API endpoint(s)
v269 - 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295 v269 -> 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295
- Adds "include_variants" filter to the BuildOrder API endpoint - Adds "include_variants" filter to the BuildOrder API endpoint
- Adds "include_variants" filter to the SalesOrder API endpoint - Adds "include_variants" filter to the SalesOrder API endpoint
- Adds "include_variants" filter to the PurchaseOrderLineItem API endpoint - Adds "include_variants" filter to the PurchaseOrderLineItem API endpoint
- Adds "include_variants" filter to the ReturnOrder API endpoint - Adds "include_variants" filter to the ReturnOrder API endpoint
268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274 v268 -> 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
- Adds "in_stock" attribute to the StockItem serializer - Adds "in_stock" attribute to the StockItem serializer
267 - 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250 v267 -> 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
- Remove "allocations" field from the SalesOrderShipment API endpoint(s) - Remove "allocations" field from the SalesOrderShipment API endpoint(s)
- Add "allocated_items" field to the SalesOrderShipment API endpoint(s) - Add "allocated_items" field to the SalesOrderShipment API endpoint(s)
266 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249 v266 -> 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
- Tweak SalesOrderShipment API for more efficient data retrieval - Tweak SalesOrderShipment API for more efficient data retrieval
265 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228 v265 -> 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228
- Adds API endpoint for providing custom admin integration details for plugins - Adds API endpoint for providing custom admin integration details for plugins
264 - 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231 v264 -> 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231
- Adds Sales Order Shipment attachment model type - Adds Sales Order Shipment attachment model type
263 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194 v263 -> 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194
- Adds Sales Order Shipment report - Adds Sales Order Shipment report
262 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220 v262 -> 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
- Tweak permission requirements for uninstalling plugins via API - Tweak permission requirements for uninstalling plugins via API
261 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184 v261 -> 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
- Fixes for BuildOrder API serializers - Fixes for BuildOrder API serializers
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190 v260 -> 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
- Adds facility for server-side context data to be passed to client-side plugins - Adds facility for server-side context data to be passed to client-side plugins
v259 - 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137 v259 -> 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
- Implements new API endpoint for enabling custom UI features via plugins - Implements new API endpoint for enabling custom UI features via plugins
v258 - 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163 v258 -> 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
- Enhances the existing PartScheduling API endpoint - Enhances the existing PartScheduling API endpoint
- Adds a formal DRF serializer to the endpoint - Adds a formal DRF serializer to the endpoint
v257 - 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150 v257 -> 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
- Adds API endpoint for reporting barcode scan history - Adds API endpoint for reporting barcode scan history
v256 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704 v256 -> 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
- Adjustments for "stocktake" (stock history) API endpoints - Adjustments for "stocktake" (stock history) API endpoints
v255 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145 v255 -> 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
- Enables copying line items when duplicating an order - Enables copying line items when duplicating an order
v254 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470 v254 -> 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470
- Implements new API endpoints for enabling custom UI functionality via plugins - Implements new API endpoints for enabling custom UI functionality via plugins
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944 v253 -> 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
- Adjustments for user API endpoints - Adjustments for user API endpoints
v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040 v252 -> 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
- Add endpoint for listing all known units - Add endpoint for listing all known units
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018 v251 -> 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
- Adds "attach_to_model" field to the ReportTemplate model - Adds "attach_to_model" field to the ReportTemplate model
v250 - 2024-09-04 : https://github.com/inventree/InvenTree/pull/8069 v250 -> 2024-09-04 : https://github.com/inventree/InvenTree/pull/8069
- Fixes 'revision' field definition in Part serializer - Fixes 'revision' field definition in Part serializer
v249 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7978 v249 -> 2024-08-23 : https://github.com/inventree/InvenTree/pull/7978
- Sort status enums - Sort status enums
v248 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965 v248 -> 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965
- Small adjustments to labels for new custom status fields - Small adjustments to labels for new custom status fields
v247 - 2024-08-22 : https://github.com/inventree/InvenTree/pull/7956 v247 -> 2024-08-22 : https://github.com/inventree/InvenTree/pull/7956
- Adjust "attachment" field on StockItemTestResult serializer - Adjust "attachment" field on StockItemTestResult serializer
- Allow null values for attachment - Allow null values for attachment
v246 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862 v246 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862
- Adds custom status fields to various serializers - Adds custom status fields to various serializers
- Adds endpoints to admin custom status fields - Adds endpoints to admin custom status fields
v245 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7520 v245 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7520
- Documented pagination fields (no functional changes) - Documented pagination fields (no functional changes)
v244 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7941 v244 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7941
- Adds "create_child_builds" field to the Build API - Adds "create_child_builds" field to the Build API
- Write-only field to create child builds from the API - Write-only field to create child builds from the API
- Only available when creating a new build order - Only available when creating a new build order
v243 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7940 v243 -> 2024-08-21 : https://github.com/inventree/InvenTree/pull/7940
- Expose "ancestor" filter to the BuildOrder API - Expose "ancestor" filter to the BuildOrder API
v242 - 2024-08-20 : https://github.com/inventree/InvenTree/pull/7932 v242 -> 2024-08-20 : https://github.com/inventree/InvenTree/pull/7932
- Adds "level" attribute to BuildOrder serializer - Adds "level" attribute to BuildOrder serializer
- Allow ordering of BuildOrder API by "level" attribute - Allow ordering of BuildOrder API by "level" attribute
- Allow "parent" filter for BuildOrder API to have "cascade=True" option - Allow "parent" filter for BuildOrder API to have "cascade=True" option
v241 - 2024-08-18 : https://github.com/inventree/InvenTree/pull/7906 v241 -> 2024-08-18 : https://github.com/inventree/InvenTree/pull/7906
- Adjusts required fields for the MeUserDetail endpoint - Adjusts required fields for the MeUserDetail endpoint
v240 - 2024-08-16 : https://github.com/inventree/InvenTree/pull/7900 v240 -> 2024-08-16 : https://github.com/inventree/InvenTree/pull/7900
- Adjust "issued_by" filter for the BuildOrder list endpoint - Adjust "issued_by" filter for the BuildOrder list endpoint
- Adjust "assigned_to" filter for the BuildOrder list endpoint - Adjust "assigned_to" filter for the BuildOrder list endpoint
v239 - 2024-08-15 : https://github.com/inventree/InvenTree/pull/7888 v239 -> 2024-08-15 : https://github.com/inventree/InvenTree/pull/7888
- Adds "testable" field to the Part model - Adds "testable" field to the Part model
- Adds associated filters to various API endpoints - Adds associated filters to various API endpoints
v238 - 2024-08-14 : https://github.com/inventree/InvenTree/pull/7874 v238 -> 2024-08-14 : https://github.com/inventree/InvenTree/pull/7874
- Add "assembly" filter to BuildLine API endpoint - Add "assembly" filter to BuildLine API endpoint
v237 - 2024-08-13 : https://github.com/inventree/InvenTree/pull/7863 v237 -> 2024-08-13 : https://github.com/inventree/InvenTree/pull/7863
- Reimplement "bulk delete" operation for Attachment model - Reimplement "bulk delete" operation for Attachment model
- Fix permission checks for Attachment API endpoints - Fix permission checks for Attachment API endpoints
v236 - 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844 v236 -> 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844
- Adds "supplier_name" to the PurchaseOrder API serializer - Adds "supplier_name" to the PurchaseOrder API serializer
v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837 v235 -> 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
- Adds "on_order" quantity to SalesOrderLineItem serializer - Adds "on_order" quantity to SalesOrderLineItem serializer
- Adds "building" quantity to SalesOrderLineItem serializer - Adds "building" quantity to SalesOrderLineItem serializer
v234 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829 v234 -> 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829
- Fixes bug in the plugin metadata endpoint - Fixes bug in the plugin metadata endpoint
v233 - 2024-08-04 : https://github.com/inventree/InvenTree/pull/7807 v233 -> 2024-08-04 : https://github.com/inventree/InvenTree/pull/7807
- Adds new endpoints for managing state of build orders - Adds new endpoints for managing state of build orders
- Adds new endpoints for managing state of purchase orders - Adds new endpoints for managing state of purchase orders
- Adds new endpoints for managing state of sales orders - Adds new endpoints for managing state of sales orders
- Adds new endpoints for managing state of return orders - Adds new endpoints for managing state of return orders
v232 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793 v232 -> 2024-08-03 : https://github.com/inventree/InvenTree/pull/7793
- Allow ordering of SalesOrderShipment API by 'shipment_date' and 'delivery_date' - Allow ordering of SalesOrderShipment API by 'shipment_date' and 'delivery_date'
v231 - 2024-08-03 : https://github.com/inventree/InvenTree/pull/7794 v231 -> 2024-08-03 : https://github.com/inventree/InvenTree/pull/7794
- Optimize BuildItem and BuildLine serializers to improve API efficiency - Optimize BuildItem and BuildLine serializers to improve API efficiency
v230 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164 v230 -> 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164
- Adds test statistics endpoint - Adds test statistics endpoint
v229 - 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775 v229 -> 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775
- Add extra exportable fields to the BomItem serializer - Add extra exportable fields to the BomItem serializer
v228 - 2024-07-18 : https://github.com/inventree/InvenTree/pull/7684 v228 -> 2024-07-18 : https://github.com/inventree/InvenTree/pull/7684
- Adds "icon" field to the PartCategory.path and StockLocation.path API - Adds "icon" field to the PartCategory.path and StockLocation.path API
- Adds icon packages API endpoint - Adds icon packages API endpoint
v227 - 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/ v227 -> 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/
- Adds endpoints to list and revoke the tokens issued to the current user - Adds endpoints to list and revoke the tokens issued to the current user
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648 v226 -> 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
- Adds barcode generation API endpoint - Adds barcode generation API endpoint
v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671 v225 -> 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
- Adds "filters" field to DataImportSession API - Adds "filters" field to DataImportSession API
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667 v224 -> 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
- Add notes field to ManufacturerPart and SupplierPart API endpoints - Add notes field to ManufacturerPart and SupplierPart API endpoints
v223 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649 v223 -> 2024-07-14 : https://github.com/inventree/InvenTree/pull/7649
- Allow adjustment of "packaging" field when receiving items against a purchase order - Allow adjustment of "packaging" field when receiving items against a purchase order
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635 v222 -> 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
- Adjust the BomItem API endpoint to improve data import process - Adjust the BomItem API endpoint to improve data import process
v221 - 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636 v221 -> 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636
- Adds missing fields from StockItemBriefSerializer - Adds missing fields from StockItemBriefSerializer
- Adds missing fields from PartBriefSerializer - Adds missing fields from PartBriefSerializer
- Adds extra exportable fields to BuildItemSerializer - Adds extra exportable fields to BuildItemSerializer
v220 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7585 v220 -> 2024-07-11 : https://github.com/inventree/InvenTree/pull/7585
- Adds "revision_of" field to Part serializer - Adds "revision_of" field to Part serializer
- Adds new API filters for "revision" status - Adds new API filters for "revision" status
v219 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611 v219 -> 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611
- Adds new fields to the BuildItem API endpoints - Adds new fields to the BuildItem API endpoints
- Adds new ordering / filtering options to the BuildItem API endpoints - Adds new ordering / filtering options to the BuildItem API endpoints
v218 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619 v218 -> 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619
- Adds "can_build" field to the BomItem API - Adds "can_build" field to the BomItem API
v217 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7599 v217 -> 2024-07-09 : https://github.com/inventree/InvenTree/pull/7599
- Fixes bug in "project_code" field for order API endpoints - Fixes bug in "project_code" field for order API endpoints
v216 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7595 v216 -> 2024-07-08 : https://github.com/inventree/InvenTree/pull/7595
- Moves API endpoint for contenttype lookup by model name - Moves API endpoint for contenttype lookup by model name
v215 - 2024-07-09 : https://github.com/inventree/InvenTree/pull/7591 v215 -> 2024-07-09 : https://github.com/inventree/InvenTree/pull/7591
- Adds additional fields to the BuildLine serializer - Adds additional fields to the BuildLine serializer
v214 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7587 v214 -> 2024-07-08 : https://github.com/inventree/InvenTree/pull/7587
- Adds "default_location_detail" field to the Part API - Adds "default_location_detail" field to the Part API
v213 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7527 v213 -> 2024-07-06 : https://github.com/inventree/InvenTree/pull/7527
- Adds 'locked' field to Part API - Adds 'locked' field to Part API
v212 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7562 v212 -> 2024-07-06 : https://github.com/inventree/InvenTree/pull/7562
- Makes API generation more robust (no functional changes) - Makes API generation more robust (no functional changes)
v211 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/6911 v211 -> 2024-06-26 : https://github.com/inventree/InvenTree/pull/6911
- Adds API endpoints for managing data import and export - Adds API endpoints for managing data import and export
v210 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7518 v210 -> 2024-06-26 : https://github.com/inventree/InvenTree/pull/7518
- Adds translatable text to User API fields - Adds translatable text to User API fields
v209 - 2024-06-26 : https://github.com/inventree/InvenTree/pull/7514 v209 -> 2024-06-26 : https://github.com/inventree/InvenTree/pull/7514
- Add "top_level" filter to PartCategory API endpoint - Add "top_level" filter to PartCategory API endpoint
- Add "top_level" filter to StockLocation API endpoint - Add "top_level" filter to StockLocation API endpoint
v208 - 2024-06-19 : https://github.com/inventree/InvenTree/pull/7479 v208 -> 2024-06-19 : https://github.com/inventree/InvenTree/pull/7479
- Adds documentation for the user roles API endpoint (no functional changes) - Adds documentation for the user roles API endpoint (no functional changes)
v207 - 2024-06-09 : https://github.com/inventree/InvenTree/pull/7420 v207 -> 2024-06-09 : https://github.com/inventree/InvenTree/pull/7420
- Moves all "Attachment" models into a single table - Moves all "Attachment" models into a single table
- All "Attachment" operations are now performed at /api/attachment/ - All "Attachment" operations are now performed at /api/attachment/
- Add permissions information to /api/user/roles/ endpoint - Add permissions information to /api/user/roles/ endpoint
v206 - 2024-06-08 : https://github.com/inventree/InvenTree/pull/7417 v206 -> 2024-06-08 : https://github.com/inventree/InvenTree/pull/7417
- Adds "choices" field to the PartTestTemplate model - Adds "choices" field to the PartTestTemplate model
v205 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7284 v205 -> 2024-06-03 : https://github.com/inventree/InvenTree/pull/7284
- Added model_type and model_id fields to the "NotesImage" serializer - Added model_type and model_id fields to the "NotesImage" serializer
v204 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7393 v204 -> 2024-06-03 : https://github.com/inventree/InvenTree/pull/7393
- Fixes previous API update which resulted in inconsistent ordering of currency codes - Fixes previous API update which resulted in inconsistent ordering of currency codes
v203 - 2024-06-03 : https://github.com/inventree/InvenTree/pull/7390 v203 -> 2024-06-03 : https://github.com/inventree/InvenTree/pull/7390
- Currency codes are now configurable as a run-time setting - Currency codes are now configurable as a run-time setting
v202 - 2024-05-27 : https://github.com/inventree/InvenTree/pull/7343 v202 -> 2024-05-27 : https://github.com/inventree/InvenTree/pull/7343
- Adjust "required" attribute of Part.category field to be optional - Adjust "required" attribute of Part.category field to be optional
v201 - 2024-05-21 : https://github.com/inventree/InvenTree/pull/7074 v201 -> 2024-05-21 : https://github.com/inventree/InvenTree/pull/7074
- Major refactor of the report template / report printing interface - Major refactor of the report template / report printing interface
- This is a *breaking change* to the report template API - This is a *breaking change* to the report template API
v200 - 2024-05-20 : https://github.com/inventree/InvenTree/pull/7000 v200 -> 2024-05-20 : https://github.com/inventree/InvenTree/pull/7000
- Adds API endpoint for generating custom batch codes - Adds API endpoint for generating custom batch codes
- Adds API endpoint for generating custom serial numbers - Adds API endpoint for generating custom serial numbers
v199 - 2024-05-20 : https://github.com/inventree/InvenTree/pull/7264 v199 -> 2024-05-20 : https://github.com/inventree/InvenTree/pull/7264
- Expose "bom_valid" filter for the Part API - Expose "bom_valid" filter for the Part API
- Expose "starred" filter for the Part API - Expose "starred" filter for the Part API
v198 - 2024-05-19 : https://github.com/inventree/InvenTree/pull/7258 v198 -> 2024-05-19 : https://github.com/inventree/InvenTree/pull/7258
- Fixed lookup field conflicts in the plugins API - Fixed lookup field conflicts in the plugins API
v197 - 2024-05-14 : https://github.com/inventree/InvenTree/pull/7224 v197 -> 2024-05-14 : https://github.com/inventree/InvenTree/pull/7224
- Refactor the plugin API endpoints to use the plugin "key" for lookup, rather than the PK value - Refactor the plugin API endpoints to use the plugin "key" for lookup, rather than the PK value
v196 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7160 v196 -> 2024-05-05 : https://github.com/inventree/InvenTree/pull/7160
- Adds "location" field to BuildOutputComplete API endpoint - Adds "location" field to BuildOutputComplete API endpoint
v195 - 2024-05-03 : https://github.com/inventree/InvenTree/pull/7153 v195 -> 2024-05-03 : https://github.com/inventree/InvenTree/pull/7153
- Fixes bug in BuildOrderCancel API endpoint - Fixes bug in BuildOrderCancel API endpoint
v194 - 2024-05-01 : https://github.com/inventree/InvenTree/pull/7147 v194 -> 2024-05-01 : https://github.com/inventree/InvenTree/pull/7147
- Adds field description to the currency_exchange_retrieve API call - Adds field description to the currency_exchange_retrieve API call
v193 - 2024-04-30 : https://github.com/inventree/InvenTree/pull/7144 v193 -> 2024-04-30 : https://github.com/inventree/InvenTree/pull/7144
- Adds "assigned_to" filter to PurchaseOrder / SalesOrder / ReturnOrder API endpoints - Adds "assigned_to" filter to PurchaseOrder / SalesOrder / ReturnOrder API endpoints
v192 - 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106 v192 -> 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106
- Adds 'trackable' ordering option to BuildLineLabel API endpoint - Adds 'trackable' ordering option to BuildLineLabel API endpoint
v191 - 2024-04-22 : https://github.com/inventree/InvenTree/pull/7079 v191 -> 2024-04-22 : https://github.com/inventree/InvenTree/pull/7079
- Adds API endpoints for Contenttype model - Adds API endpoints for Contenttype model
v190 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7024 v190 -> 2024-04-19 : https://github.com/inventree/InvenTree/pull/7024
- Adds "active" field to the Company API endpoints - Adds "active" field to the Company API endpoints
- Allow company list to be filtered by "active" status - Allow company list to be filtered by "active" status
v189 - 2024-04-19 : https://github.com/inventree/InvenTree/pull/7066 v189 -> 2024-04-19 : https://github.com/inventree/InvenTree/pull/7066
- Adds "currency" field to CompanyBriefSerializer class - Adds "currency" field to CompanyBriefSerializer class
v188 - 2024-04-16 : https://github.com/inventree/InvenTree/pull/6970 v188 -> 2024-04-16 : https://github.com/inventree/InvenTree/pull/6970
- Adds session authentication support for the API - Adds session authentication support for the API
- Improvements for login / logout endpoints for better support of React web interface - Improvements for login / logout endpoints for better support of React web interface
v187 - 2024-04-10 : https://github.com/inventree/InvenTree/pull/6985 v187 -> 2024-04-10 : https://github.com/inventree/InvenTree/pull/6985
- Allow Part list endpoint to be sorted by pricing_min and pricing_max values - Allow Part list endpoint to be sorted by pricing_min and pricing_max values
- Allow BomItem list endpoint to be sorted by pricing_min and pricing_max values - Allow BomItem list endpoint to be sorted by pricing_min and pricing_max values
- Allow InternalPrice and SalePrice endpoints to be sorted by quantity - Allow InternalPrice and SalePrice endpoints to be sorted by quantity
- Adds total pricing values to BomItem serializer - Adds total pricing values to BomItem serializer
v186 - 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855 v186 -> 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855
- Adds license information to the API - Adds license information to the API
v185 - 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836 v185 -> 2024-03-24 : https://github.com/inventree/InvenTree/pull/6836
- Remove /plugin/activate endpoint - Remove /plugin/activate endpoint
- Update docstrings and typing for various API endpoints (no functional changes) - Update docstrings and typing for various API endpoints (no functional changes)
v184 - 2024-03-17 : https://github.com/inventree/InvenTree/pull/10464 v184 -> 2024-03-17 : https://github.com/inventree/InvenTree/pull/10464
- Add additional fields for tests (start/end datetime, test station) - Add additional fields for tests (start/end datetime, test station)
v183 - 2024-03-14 : https://github.com/inventree/InvenTree/pull/5972 v183 -> 2024-03-14 : https://github.com/inventree/InvenTree/pull/5972
- Adds "category_default_location" annotated field to part serializer - Adds "category_default_location" annotated field to part serializer
- Adds "part_detail.category_default_location" annotated field to stock item serializer - Adds "part_detail.category_default_location" annotated field to stock item serializer
- Adds "part_detail.category_default_location" annotated field to purchase order line serializer - Adds "part_detail.category_default_location" annotated field to purchase order line serializer
- Adds "parent_default_location" annotated field to category serializer - Adds "parent_default_location" annotated field to category serializer
v182 - 2024-03-13 : https://github.com/inventree/InvenTree/pull/6714 v182 -> 2024-03-13 : https://github.com/inventree/InvenTree/pull/6714
- Expose ReportSnippet model to the /report/snippet/ API endpoint - Expose ReportSnippet model to the /report/snippet/ API endpoint
- Expose ReportAsset model to the /report/asset/ API endpoint - Expose ReportAsset model to the /report/asset/ API endpoint
v181 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6541 v181 -> 2024-02-21 : https://github.com/inventree/InvenTree/pull/6541
- Adds "width" and "height" fields to the LabelTemplate API endpoint - Adds "width" and "height" fields to the LabelTemplate API endpoint
- Adds "page_size" and "landscape" fields to the ReportTemplate API endpoint - Adds "page_size" and "landscape" fields to the ReportTemplate API endpoint
v180 - 2024-3-02 : https://github.com/inventree/InvenTree/pull/6463 v180 -> 2024-3-02 : https://github.com/inventree/InvenTree/pull/6463
- Tweaks to API documentation to allow automatic documentation generation - Tweaks to API documentation to allow automatic documentation generation
v179 - 2024-03-01 : https://github.com/inventree/InvenTree/pull/6605 v179 -> 2024-03-01 : https://github.com/inventree/InvenTree/pull/6605
- Adds "subcategories" count to PartCategory serializer - Adds "subcategories" count to PartCategory serializer
- Adds "sublocations" count to StockLocation serializer - Adds "sublocations" count to StockLocation serializer
- Adds "image" field to PartBrief serializer - Adds "image" field to PartBrief serializer
- Adds "image" field to CompanyBrief serializer - Adds "image" field to CompanyBrief serializer
v178 - 2024-02-29 : https://github.com/inventree/InvenTree/pull/6604 v178 -> 2024-02-29 : https://github.com/inventree/InvenTree/pull/6604
- Adds "external_stock" field to the Part API endpoint - Adds "external_stock" field to the Part API endpoint
- Adds "external_stock" field to the BomItem API endpoint - Adds "external_stock" field to the BomItem API endpoint
- Adds "external_stock" field to the BuildLine API endpoint - Adds "external_stock" field to the BuildLine API endpoint
- Stock quantities represented in the BuildLine API endpoint are now filtered by Build.source_location - Stock quantities represented in the BuildLine API endpoint are now filtered by Build.source_location
v177 - 2024-02-27 : https://github.com/inventree/InvenTree/pull/6581 v177 -> 2024-02-27 : https://github.com/inventree/InvenTree/pull/6581
- Adds "subcategories" count to PartCategoryTree serializer - Adds "subcategories" count to PartCategoryTree serializer
- Adds "sublocations" count to StockLocationTree serializer - Adds "sublocations" count to StockLocationTree serializer
v176 - 2024-02-26 : https://github.com/inventree/InvenTree/pull/6535 v176 -> 2024-02-26 : https://github.com/inventree/InvenTree/pull/6535
- Adds the field "plugins_install_disabled" to the Server info API endpoint - Adds the field "plugins_install_disabled" to the Server info API endpoint
v175 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6538 v175 -> 2024-02-21 : https://github.com/inventree/InvenTree/pull/6538
- Adds "parts" count to PartParameterTemplate serializer - Adds "parts" count to PartParameterTemplate serializer
v174 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6536 v174 -> 2024-02-21 : https://github.com/inventree/InvenTree/pull/6536
- Expose PartCategory filters to the API documentation - Expose PartCategory filters to the API documentation
- Expose StockLocation filters to the API documentation - Expose StockLocation filters to the API documentation
v173 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6483 v173 -> 2024-02-20 : https://github.com/inventree/InvenTree/pull/6483
- Adds "merge_items" to the PurchaseOrderLine create API endpoint - Adds "merge_items" to the PurchaseOrderLine create API endpoint
- Adds "auto_pricing" to the PurchaseOrderLine create/update API endpoint - Adds "auto_pricing" to the PurchaseOrderLine create/update API endpoint
v172 - 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526 v172 -> 2024-02-20 : https://github.com/inventree/InvenTree/pull/6526
- Adds "enabled" field to the PartTestTemplate API endpoint - Adds "enabled" field to the PartTestTemplate API endpoint
- Adds "enabled" filter to the PartTestTemplate list - Adds "enabled" filter to the PartTestTemplate list
- Adds "enabled" filter to the StockItemTestResult list - Adds "enabled" filter to the StockItemTestResult list
v171 - 2024-02-19 : https://github.com/inventree/InvenTree/pull/6516 v171 -> 2024-02-19 : https://github.com/inventree/InvenTree/pull/6516
- Adds "key" as a filterable parameter to PartTestTemplate list endpoint - Adds "key" as a filterable parameter to PartTestTemplate list endpoint
v170 -> 2024-02-19 : https://github.com/inventree/InvenTree/pull/6514 v170 -> 2024-02-19 : https://github.com/inventree/InvenTree/pull/6514
+46
View File
@@ -365,6 +365,52 @@ def get_secret_key():
return key_data return key_data
def get_oidc_private_key():
"""Return the private key for OIDC authentication.
Following options are tested, in descending order of preference:
A) Check for environment variable INVENTREE_OIDC_PRIVATE_KEY or config yalue => Use raw key data
B) Check for environment variable INVENTREE_OIDC_PRIVATE_KEY_FILE or config value => Load key data from file
C) Create "oidc.pem" if it does not exist
"""
RSA_KEY = get_setting('INVENTREE_OIDC_PRIVATE_KEY', 'oidc_private_key')
if RSA_KEY:
logger.info('RSA_KEY loaded by INVENTREE_OIDC_PRIVATE_KEY') # pragma: no cover
return RSA_KEY
# Look for private key file
key_loc = Path(
get_setting(
'INVENTREE_OIDC_PRIVATE_KEY_FILE',
'oidc_private_key_file',
get_base_dir().joinpath('oidc.pem'),
)
)
if key_loc.exists():
return key_loc.read_text()
else:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Default location for private key file
logger.info("Generating oidc key file at '%s'", key_loc)
ensure_dir(key_loc.parent)
# Create a random key file
new_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
# Write our key to disk for safe keeping
with open(str(key_loc), 'wb') as f:
f.write(
new_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
RSA_KEY = key_loc.read_text()
return RSA_KEY
def get_custom_file( def get_custom_file(
env_ref: str, conf_ref: str, log_ref: str, lookup_media: bool = False env_ref: str, conf_ref: str, log_ref: str, lookup_media: bool = False
): ):
+11 -2
View File
@@ -32,14 +32,23 @@ class InvenTreeSearchFilter(filters.SearchFilter):
"""Return a set of search fields for the request, adjusted based on request params. """Return a set of search fields for the request, adjusted based on request params.
The following query params are available to 'augment' the search (in decreasing order of priority) The following query params are available to 'augment' the search (in decreasing order of priority)
- search_notes: If True, 'notes' is added to the search_fields if it isn't already present
- search_regex: If True, search is performed on 'regex' comparison - search_regex: If True, search is performed on 'regex' comparison
""" """
regex = InvenTree.helpers.str2bool( search_notes = InvenTree.helpers.str2bool(
request.query_params.get('search_regex', False) request.query_params.get('search_notes', False)
) )
search_fields = super().get_search_fields(view, request) search_fields = super().get_search_fields(view, request)
if search_notes and 'notes' not in search_fields:
# don't modify existing list, create a new object so further queries aren't affected
search_fields = [*search_fields, 'notes']
regex = InvenTree.helpers.str2bool(
request.query_params.get('search_regex', False)
)
fields = [] fields = []
if search_fields: if search_fields:
@@ -194,6 +194,7 @@ def format_money(
money (Money): The money object to format money (Money): The money object to format
decimal_places (int): Number of decimal places to use decimal_places (int): Number of decimal places to use
fmt (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html) fmt (str): Format pattern according LDML / the babel format pattern syntax (https://babel.pocoo.org/en/latest/numbers.html)
include_symbol (bool): Whether to include the currency symbol in the formatted output
Returns: Returns:
str: The formatted string str: The formatted string
+54 -7
View File
@@ -32,18 +32,67 @@ from .settings import MEDIA_URL, STATIC_URL
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
INT_CLIP_MAX = 0x7FFFFFFF
def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
"""Extract an integer out of reference.""" def extract_int(
reference, clip=INT_CLIP_MAX, try_hex=False, allow_negative=False
) -> int:
"""Extract an integer out of provided string.
Arguments:
reference: Input string to extract integer from
clip: Maximum value to return (default = 0x7FFFFFFF)
try_hex: Attempt to parse as hex if integer conversion fails (default = False)
allow_negative: Allow negative values (default = False)
"""
# Default value if we cannot convert to an integer # Default value if we cannot convert to an integer
ref_int = 0 ref_int = 0
def do_clip(value: int, clip: int, allow_negative: bool) -> int:
"""Perform clipping on the provided value.
Arguments:
value: Value to clip
clip: Maximum value to clip to
allow_negative: Allow negative values (default = False)
"""
if clip is None:
return value
clip = min(clip, INT_CLIP_MAX)
if value > clip:
return clip
elif value < -clip:
return -clip
if not allow_negative:
value = abs(value)
return value
reference = str(reference).strip() reference = str(reference).strip()
# Ignore empty string # Ignore empty string
if len(reference) == 0: if len(reference) == 0:
return 0 return 0
# Try naive integer conversion first
try:
ref_int = int(reference)
return do_clip(ref_int, clip, allow_negative)
except ValueError:
pass
# Hex?
if try_hex or reference.startswith('0x'):
try:
ref_int = int(reference, base=16)
return do_clip(ref_int, clip, allow_negative)
except ValueError:
pass
# Look at the start of the string - can it be "integerized"? # Look at the start of the string - can it be "integerized"?
result = re.match(r'^(\d+)', reference) result = re.match(r'^(\d+)', reference)
@@ -66,11 +115,7 @@ def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
# Ensure that the returned values are within the range that can be stored in an IntegerField # Ensure that the returned values are within the range that can be stored in an IntegerField
# Note: This will result in large values being "clipped" # Note: This will result in large values being "clipped"
if clip is not None: ref_int = do_clip(ref_int, clip, allow_negative)
if ref_int > clip:
ref_int = clip
elif ref_int < -clip:
ref_int = -clip
if not allow_negative and ref_int < 0: if not allow_negative and ref_int < 0:
ref_int = abs(ref_int) ref_int = abs(ref_int)
@@ -437,6 +482,7 @@ def increment_serial_number(serial, part=None):
Arguments: Arguments:
serial: The serial number which should be incremented serial: The serial number which should be incremented
part: Optional part object to provide additional context for incrementing the serial number
Returns: Returns:
incremented value, or None if incrementing could not be performed. incremented value, or None if incrementing could not be performed.
@@ -491,6 +537,7 @@ def extract_serial_numbers(
input_string: Input string with specified serial numbers (string, or integer) input_string: Input string with specified serial numbers (string, or integer)
expected_quantity: The number of (unique) serial numbers we expect expected_quantity: The number of (unique) serial numbers we expect
starting_value: Provide a starting value for the sequence (or None) starting_value: Provide a starting value for the sequence (or None)
part: Part that should be used as context
""" """
if starting_value is None: if starting_value is None:
starting_value = increment_serial_number(None, part=part) starting_value = increment_serial_number(None, part=part)
@@ -4,6 +4,7 @@ from django.conf import settings
from django.core import mail as django_mail from django.core import mail as django_mail
import structlog import structlog
from allauth.account.models import EmailAddress
import InvenTree.ready import InvenTree.ready
import InvenTree.tasks import InvenTree.tasks
@@ -30,15 +31,15 @@ def is_email_configured():
# Display warning unless in test mode # Display warning unless in test mode
if not testing: # pragma: no cover if not testing: # pragma: no cover
logger.debug('EMAIL_HOST is not configured') logger.debug('INVE-W7: EMAIL_HOST is not configured')
# Display warning unless in test mode # Display warning unless in test mode
if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover if not settings.EMAIL_HOST_USER and not testing: # pragma: no cover
logger.debug('EMAIL_HOST_USER is not configured') logger.debug('INVE-W7: EMAIL_HOST_USER is not configured')
# Display warning unless in test mode # Display warning unless in test mode
if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover if not settings.EMAIL_HOST_PASSWORD and testing: # pragma: no cover
logger.debug('EMAIL_HOST_PASSWORD is not configured') logger.debug('INVE-W7: EMAIL_HOST_PASSWORD is not configured')
# Email sender must be configured # Email sender must be configured
if not settings.DEFAULT_FROM_EMAIL: if not settings.DEFAULT_FROM_EMAIL:
@@ -56,7 +57,6 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
recipients = [recipients] recipients = [recipients]
import InvenTree.ready import InvenTree.ready
import InvenTree.status
if InvenTree.ready.isImportingData(): if InvenTree.ready.isImportingData():
# If we are importing data, don't send emails # If we are importing data, don't send emails
@@ -64,6 +64,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
if not is_email_configured() and not settings.TESTING: if not is_email_configured() and not settings.TESTING:
# Email is not configured / enabled # Email is not configured / enabled
logger.info('INVE-W7: Email will not be send, no mail server configured')
return return
# If a *from_email* is not specified, ensure that the default is set # If a *from_email* is not specified, ensure that the default is set
@@ -88,3 +89,19 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
html_message=html_message, html_message=html_message,
group='notification', group='notification',
) )
def get_email_for_user(user) -> str:
"""Find an email address for the specified user."""
# First check if the user has an associated email address
if user.email:
return user.email
# Otherwise, find first matching email
# Priority is given to primary or verified email addresses
if (
email := EmailAddress.objects.filter(user=user)
.order_by('-primary', '-verified')
.first()
):
return email.email
@@ -132,6 +132,7 @@ def get_shared_class_instance_state_mixin(get_state_key: Callable[[type], str]):
Arguments: Arguments:
key: The key for the shared state key: The key for the shared state
default: The default value to return if the key does not exist
""" """
return cache.get(self._get_key(key)) or default return cache.get(self._get_key(key)) or default
@@ -265,6 +265,7 @@ def notify_responsible(
sender, sender,
content: NotificationBody = InvenTreeNotificationBodies.NewOrder, content: NotificationBody = InvenTreeNotificationBodies.NewOrder,
exclude=None, exclude=None,
extra_users: Optional[list] = None,
): ):
"""Notify all responsible parties of a change in an instance. """Notify all responsible parties of a change in an instance.
@@ -276,15 +277,19 @@ def notify_responsible(
sender: Sender model reference sender: Sender model reference
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder. content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
exclude (User, optional): User instance that should be excluded. Defaults to None. exclude (User, optional): User instance that should be excluded. Defaults to None.
extra_users (list, optional): List of extra users to notify. Defaults to None.
""" """
import InvenTree.ready import InvenTree.ready
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations(): if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
return return
notify_users( users = [instance.responsible]
[instance.responsible], instance, sender, content=content, exclude=exclude
) if extra_users:
users.extend(extra_users)
notify_users(users, instance, sender, content=content, exclude=exclude)
def notify_users( def notify_users(
@@ -1,4 +1,4 @@
"""Check if there are any pending database migrations, and run them.""" """Extended schema generator."""
from pathlib import Path from pathlib import Path
from typing import TypeVar from typing import TypeVar
@@ -108,3 +108,5 @@ class Command(spectacular.Command):
settings.SPECTACULAR_SETTINGS['APPEND_COMPONENTS'] = components settings.SPECTACULAR_SETTINGS['APPEND_COMPONENTS'] = components
super().handle(*args, **kwargs) super().handle(*args, **kwargs)
return 'done'
+11 -21
View File
@@ -13,9 +13,9 @@ from rest_framework.utils import model_meta
import common.models import common.models
import InvenTree.permissions import InvenTree.permissions
import users.models
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from InvenTree.serializers import DependentField from InvenTree.serializers import DependentField
from users.permissions import check_user_permission
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
@@ -107,28 +107,16 @@ class InvenTreeMetadata(SimpleMetadata):
self.model = InvenTree.permissions.get_model_for_view(view) self.model = InvenTree.permissions.get_model_for_view(view)
# Construct the 'table name' from the model # Construct the 'table name' from the model
app_label = self.model._meta.app_label
tbl_label = self.model._meta.model_name tbl_label = self.model._meta.model_name
metadata['model'] = tbl_label metadata['model'] = tbl_label
table = f'{app_label}_{tbl_label}'
actions = metadata.get('actions', None) actions = metadata.get('actions', None)
if actions is None: if actions is None:
actions = {} actions = {}
check = users.models.RuleSet.check_table_permission
# Map the request method to a permission type # Map the request method to a permission type
rolemap = { rolemap = {**InvenTree.permissions.ACTION_MAP, 'OPTIONS': 'view'}
'GET': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
}
# let the view define a custom rolemap # let the view define a custom rolemap
if hasattr(view, 'rolemap'): if hasattr(view, 'rolemap'):
@@ -136,13 +124,15 @@ class InvenTreeMetadata(SimpleMetadata):
# Remove any HTTP methods that the user does not have permission for # Remove any HTTP methods that the user does not have permission for
for method, permission in rolemap.items(): for method, permission in rolemap.items():
result = check(user, table, permission) result = check_user_permission(user, self.model, permission)
if method in actions and not result: if method in actions and not result:
del actions[method] del actions[method]
# Add a 'DELETE' action if we are allowed to delete # Add a 'DELETE' action if we are allowed to delete
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'): if 'DELETE' in view.allowed_methods and check_user_permission(
user, self.model, 'delete'
):
actions['DELETE'] = {} actions['DELETE'] = {}
metadata['actions'] = actions metadata['actions'] = actions
@@ -170,11 +160,11 @@ class InvenTreeMetadata(SimpleMetadata):
- model_value is callable, and field_value is not (this indicates that the model value is translated) - model_value is callable, and field_value is not (this indicates that the model value is translated)
- model_value is not a string, and field_value is a string (this indicates that the model value is translated) - model_value is not a string, and field_value is a string (this indicates that the model value is translated)
Arguments: Args:
- field_name: The name of the field field_name (str): The name of the field.
- field_key: The property key to override field_key (str): The property key to override.
- field_value: The value of the field (if available) field_value: The value of the field (if available).
- model_value: The equivalent value of the model (if available) model_value: The equivalent value of the model (if available).
""" """
if field_value is None and model_value is not None: if field_value is None and model_value is not None:
return model_value return model_value
@@ -86,6 +86,11 @@ class AuthRequiredMiddleware:
response = self.get_response(request) response = self.get_response(request)
return response return response
# oAuth2 requests are handled by the oAuth2 library
if request.path_info.startswith('/o/'):
response = self.get_response(request)
return response
# Is the function exempt from auth requirements? # Is the function exempt from auth requirements?
path_func = resolve(request.path).func path_func = resolve(request.path).func
+274 -22
View File
@@ -1,10 +1,29 @@
"""Permission set for InvenTree.""" """Permission set for InvenTree."""
from functools import wraps from functools import wraps
from typing import Optional
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements
from oauth2_provider.contrib.rest_framework.authentication import OAuth2Authentication
from rest_framework import permissions from rest_framework import permissions
import users.models import users.permissions
import users.ruleset
from users.oauth2_scopes import (
DEFAULT_READ,
DEFAULT_STAFF,
DEFAULT_SUPERUSER,
get_granular_scope,
)
ACTION_MAP = {
'GET': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
'OPTIONS': DEFAULT_READ,
}
def get_model_for_view(view): def get_model_for_view(view):
@@ -21,7 +40,126 @@ def get_model_for_view(view):
raise AttributeError(f'Serializer class not specified for {view.__class__}') raise AttributeError(f'Serializer class not specified for {view.__class__}')
class RolePermission(permissions.BasePermission): def map_scope(
roles: Optional[list[str]] = None,
only_read=False,
read_name=DEFAULT_READ,
map_read: Optional[list[str]] = None,
map_read_name=DEFAULT_READ,
) -> dict:
"""Generate the required scopes for OAS permission views.
Args:
roles (Optional[list[str]]): A list of roles or tables to generate granular scopes for.
only_read (bool): If True, only the read scope will be returned for all actions.
read_name (str): The read scope name to use when `only_read` is True.
map_read (Optional[list[str]]): A list of HTTP methods that should map to the default read scope (use if some actions requirea differing role).
map_read_name (str): The read scope name to use for methods specified in `map_read` when `map_read` is specified.
Returns:
dict: A dictionary mapping HTTP methods to their corresponding scopes.
Each scope is represented as a list of lists of strings.
"""
def scope_name(action):
if only_read:
return [[read_name]]
if roles:
return [[get_granular_scope(action, table) for table in roles]]
return [[action]]
def get_scope(method, action):
if map_read and method in map_read:
return [[map_read_name]]
return scope_name(action)
return {
method: get_scope(method, action) if method != 'OPTIONS' else [[DEFAULT_READ]]
for method, action in ACTION_MAP.items()
}
# Precalculate the roles mapping
roles = users.ruleset.get_ruleset_models()
precalculated_roles = {}
for role, tables in roles.items():
for table in tables:
if table not in precalculated_roles:
precalculated_roles[table] = []
precalculated_roles[table].append(role)
class OASTokenMixin:
"""Mixin that combines the permissions of normal classes and token classes."""
def has_permission(self, request, view):
"""Check if the user has the required scopes or was authenticated another way."""
return self.check_oauth2_authentication(
request, view
) or super().has_permission(request, view)
def check_oauth2_authentication(self, request, view):
"""Check if the user is authenticated using OAuth2 and has the required scopes."""
return self.is_oauth2ed(
request
) and TokenMatchesOASRequirements().has_permission(request, view)
def is_oauth2ed(self, request):
"""Check if the user is authenticated using OAuth2."""
oauth2authenticated = False
if bool(request.user and request.user.is_authenticated):
oauth2authenticated = isinstance(
request.successful_authenticator, OAuth2Authentication
)
return oauth2authenticated
class InvenTreeRoleScopeMixin(OASTokenMixin):
"""Permission that discovers the required scopes from the OpenAPI schema."""
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
if hasattr(view, 'required_alternate_scopes'):
return view.required_alternate_scopes
try:
# Extract the model name associated with this request
model = get_model_for_view(view)
calc = precalculated_roles.get(
f'{model._meta.app_label}_{model._meta.model_name}', []
)
if model is None or not calc:
return map_scope(only_read=True)
return map_scope(roles=calc)
except AttributeError:
# We will assume that if the serializer class does *not* have a Meta,
# then we don't need a permission
return map_scope(only_read=True)
except Exception:
return map_scope(only_read=True)
class InvenTreeTokenMatchesOASRequirements(InvenTreeRoleScopeMixin):
"""Combines InvenTree role-based scope handling with OpenAPI schema token requirements.
Usesd as default permission class.
"""
def has_permission(self, request, view):
"""Check if the user has the required scopes or was authenticated another way."""
if self.is_oauth2ed(request):
# Check if the user is authenticated using OAuth2 and has the required scopes
return super().has_permission(request, view)
# If the user is authenticated using another method, check if they have the required permissions
return bool(request.user and request.user.is_authenticated)
def has_object_permission(self, request, view, obj):
"""Return `True` if permission is granted, `False` otherwise."""
return True
class RolePermission(InvenTreeRoleScopeMixin, permissions.BasePermission):
"""Role mixin for API endpoints, allowing us to specify the user "role" which is required for certain operations. """Role mixin for API endpoints, allowing us to specify the user "role" which is required for certain operations.
Each endpoint can have one or more of the following actions: Each endpoint can have one or more of the following actions:
@@ -52,14 +190,7 @@ class RolePermission(permissions.BasePermission):
return True return True
# Map the request method to a permission type # Map the request method to a permission type
rolemap = { rolemap = {**ACTION_MAP, 'OPTIONS': 'view'}
'GET': 'view',
'OPTIONS': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
}
# let the view define a custom rolemap # let the view define a custom rolemap
if hasattr(view, 'rolemap'): if hasattr(view, 'rolemap'):
@@ -73,7 +204,7 @@ class RolePermission(permissions.BasePermission):
if '.' in role: if '.' in role:
role, permission = role.split('.') role, permission = role.split('.')
return users.models.check_user_role(user, role, permission) return users.permissions.check_user_role(user, role, permission)
try: try:
# Extract the model name associated with this request # Extract the model name associated with this request
@@ -82,27 +213,66 @@ class RolePermission(permissions.BasePermission):
if model is None: if model is None:
return True return True
app_label = model._meta.app_label
model_name = model._meta.model_name
table = f'{app_label}_{model_name}'
except AttributeError: except AttributeError:
# We will assume that if the serializer class does *not* have a Meta, # We will assume that if the serializer class does *not* have a Meta,
# then we don't need a permission # then we don't need a permission
return True return True
return users.models.RuleSet.check_table_permission(user, table, permission) return users.permissions.check_user_permission(user, model, permission)
class IsSuperuser(permissions.IsAdminUser): class RolePermissionOrReadOnly(RolePermission):
"""RolePermission which also allows read access for any authenticated user."""
REQUIRE_STAFF = False
def has_permission(self, request, view):
"""Determine if the current user has the specified permissions.
- If the user does have the required role, then allow the request
- If the user does not have the required role, but is authenticated, then allow read-only access
"""
user = getattr(request, 'user', None)
if not user or not user.is_active or not user.is_authenticated:
return False
if user.is_superuser:
return True
if not self.REQUIRE_STAFF or user.is_staff:
if super().has_permission(request, view):
return True
return request.method in permissions.SAFE_METHODS
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
scopes = map_scope(
only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS
)
return scopes
class StaffRolePermissionOrReadOnly(RolePermissionOrReadOnly):
"""RolePermission which requires staff AND role access, or read-only."""
REQUIRE_STAFF = True
class IsSuperuserOrSuperScope(OASTokenMixin, permissions.IsAdminUser):
"""Allows access only to superuser users.""" """Allows access only to superuser users."""
def has_permission(self, request, view): def has_permission(self, request, view):
"""Check if the user is a superuser.""" """Check if the user is a superuser."""
return bool(request.user and request.user.is_superuser) return bool(request.user and request.user.is_superuser)
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True, read_name=DEFAULT_SUPERUSER)
class IsSuperuserOrReadOnly(permissions.IsAdminUser):
class IsSuperuserOrReadOnlyOrScope(OASTokenMixin, permissions.IsAdminUser):
"""Allow read-only access to any user, but write access is restricted to superuser users.""" """Allow read-only access to any user, but write access is restricted to superuser users."""
def has_permission(self, request, view): def has_permission(self, request, view):
@@ -112,17 +282,59 @@ class IsSuperuserOrReadOnly(permissions.IsAdminUser):
or request.method in permissions.SAFE_METHODS or request.method in permissions.SAFE_METHODS
) )
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(
only_read=True,
read_name=DEFAULT_SUPERUSER,
map_read=permissions.SAFE_METHODS,
)
class IsStaffOrReadOnly(permissions.IsAdminUser):
"""Allows read-only access to any user, but write access is restricted to staff users.""" class IsAuthenticatedOrReadScope(OASTokenMixin, permissions.IsAuthenticated):
"""Allows access only to authenticated users or read scope tokens."""
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True)
class IsStaffOrReadOnlyScope(OASTokenMixin, permissions.IsAuthenticated):
"""Allows read-only access to any authenticated user, but write access is restricted to staff users."""
def has_permission(self, request, view): def has_permission(self, request, view):
"""Check if the user is a superuser.""" """Check if the user is a staff."""
return bool( return bool(permissions.IsAuthenticated().has_permission(request, view)) and (
(request.user and request.user.is_staff) (request.user and request.user.is_staff)
or request.method in permissions.SAFE_METHODS or request.method in permissions.SAFE_METHODS
) )
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(
only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS
)
class IsAdminOrAdminScope(OASTokenMixin, permissions.IsAdminUser):
"""Allows access only to admin users or admin scope tokens."""
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True, read_name=DEFAULT_STAFF)
class AllowAnyOrReadScope(OASTokenMixin, permissions.AllowAny):
"""Allows access to any user or read scope tokens."""
def has_permission(self, request, view):
"""Anyone is allowed."""
return True
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True)
def auth_exempt(view_func): def auth_exempt(view_func):
"""Mark a view function as being exempt from auth requirements.""" """Mark a view function as being exempt from auth requirements."""
@@ -132,3 +344,43 @@ def auth_exempt(view_func):
wrapped_view.auth_exempt = True wrapped_view.auth_exempt = True
return wraps(view_func)(wrapped_view) return wraps(view_func)(wrapped_view)
class UserSettingsPermissionsOrScope(OASTokenMixin, permissions.BasePermission):
"""Special permission class to determine if the user can view / edit a particular setting."""
def has_object_permission(self, request, view, obj):
"""Check if the user that requested is also the object owner."""
try:
user = request.user
except AttributeError: # pragma: no cover
return False
return user == obj.user
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(only_read=True)
class GlobalSettingsPermissions(OASTokenMixin, permissions.BasePermission):
"""Special permission class to determine if the user is "staff"."""
def has_permission(self, request, view):
"""Check that the requesting user is 'admin'."""
try:
user = request.user
if request.method in permissions.SAFE_METHODS:
return True
# Any other methods require staff access permissions
return user.is_staff
except AttributeError: # pragma: no cover
return False
def get_required_alternate_scopes(self, request, view):
"""Return the required scopes for the current request."""
return map_scope(
only_read=True, read_name=DEFAULT_STAFF, map_read=permissions.SAFE_METHODS
)
+85 -6
View File
@@ -1,25 +1,55 @@
"""Schema processing functions for cleaning up generated schema.""" """Schema processing functions for cleaning up generated schema."""
from itertools import chain
from typing import Optional from typing import Optional
from drf_spectacular.contrib.django_oauth_toolkit import DjangoOAuthToolkitScheme
from drf_spectacular.drainage import warn
from drf_spectacular.openapi import AutoSchema from drf_spectacular.openapi import AutoSchema
from drf_spectacular.plumbing import ComponentRegistry from drf_spectacular.plumbing import ComponentRegistry
from drf_spectacular.utils import _SchemaType from drf_spectacular.utils import _SchemaType
from InvenTree.permissions import OASTokenMixin
from users.oauth2_scopes import oauth2_scopes
class ExtendedOAuth2Scheme(DjangoOAuthToolkitScheme):
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
target_class = 'users.authentication.ExtendedOAuth2Authentication'
def get_security_requirement(self, auto_schema):
"""Get the security requirement for the current view."""
ret = super().get_security_requirement(auto_schema)
if ret:
return ret
# If no security requirement is found, try if the view uses our OASTokenMixin
for permission in auto_schema.view.get_permissions():
if isinstance(permission, OASTokenMixin):
alt_scopes = permission.get_required_alternate_scopes(
auto_schema.view.request, auto_schema.view
)
alt_scopes = alt_scopes.get(auto_schema.method, [])
return [{self.name: group} for group in alt_scopes]
class ExtendedAutoSchema(AutoSchema): class ExtendedAutoSchema(AutoSchema):
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior.""" """Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
def is_bulk_delete(self) -> bool: def is_bulk_action(self, ref: str) -> bool:
"""Check the class of the current view for the BulkDeleteMixin.""" """Check the class of the current view for the bulk mixins."""
return 'BulkDeleteMixin' in [c.__name__ for c in type(self.view).__mro__] return ref in [c.__name__ for c in type(self.view).__mro__]
def get_operation_id(self) -> str: def get_operation_id(self) -> str:
"""Custom path handling overrides, falling back to default behavior.""" """Custom path handling overrides, falling back to default behavior."""
result_id = super().get_operation_id() result_id = super().get_operation_id()
# rename bulk deletes to deconflict with single delete operation_id # rename bulk actions to deconflict with single action operation_id
if self.method == 'DELETE' and self.is_bulk_delete(): if (self.method == 'DELETE' and self.is_bulk_action('BulkDeleteMixin')) or (
(self.method == 'PUT' or self.method == 'PATCH')
and self.is_bulk_action('BulkUpdateMixin')
):
action = self.method_mapping[self.method.lower()] action = self.method_mapping[self.method.lower()]
result_id = result_id.replace(action, 'bulk_' + action) result_id = result_id.replace(action, 'bulk_' + action)
@@ -42,7 +72,7 @@ class ExtendedAutoSchema(AutoSchema):
# drf-spectacular doesn't support a body on DELETE endpoints because the semantics are not well-defined and # drf-spectacular doesn't support a body on DELETE endpoints because the semantics are not well-defined and
# OpenAPI recommends against it. This allows us to generate a schema that follows existing behavior. # OpenAPI recommends against it. This allows us to generate a schema that follows existing behavior.
if self.method == 'DELETE' and self.is_bulk_delete(): if self.method == 'DELETE' and self.is_bulk_action('BulkDeleteMixin'):
original_method = self.method original_method = self.method
self.method = 'PUT' self.method = 'PUT'
request_body = self._get_request_body() request_body = self._get_request_body()
@@ -75,3 +105,52 @@ def postprocess_required_nullable(result, generator, request, public):
schema.pop('required') schema.pop('required')
return result return result
def postprocess_print_stats(result, generator, request, public):
"""Prints statistics against schema."""
rlt_dict = {}
for path in result['paths']:
for method in result['paths'][path]:
sec = result['paths'][path][method].get('security', [])
scopes = list(filter(None, (item.get('oauth2') for item in sec)))
rlt_dict[f'{path}:{method}'] = {
'method': method,
'oauth': list(chain(*scopes)),
'sec': sec is None,
}
# Get paths without oauth2
no_oauth2 = [
path for path, details in rlt_dict.items() if not any(details['oauth'])
]
no_oauth2_wa = [path for path in no_oauth2 if not path.startswith('/api/auth/v1/')]
# Get paths without security
no_security = [path for path, details in rlt_dict.items() if details['sec']]
# Get path counts per scope
scopes = {}
for path, details in rlt_dict.items():
if details['oauth']:
for scope in details['oauth']:
if scope not in scopes:
scopes[scope] = []
scopes[scope].append(path)
# Sort scopes by keys
scopes = dict(sorted(scopes.items()))
# Print statistics
print('\nSchema statistics:')
print(f'Paths without oauth2: {len(no_oauth2)}')
print(f'Paths without oauth2 (without allauth): {len(no_oauth2_wa)}')
print(f'Paths without security: {len(no_security)}\n')
print('Scope stats:')
for scope, paths in scopes.items():
print(f' {scope}: {len(paths)}')
print()
# Check for unknown scopes
for scope, paths in scopes.items():
if scope not in oauth2_scopes:
warn(f'unknown scope `{scope}` in {len(paths)} paths')
return result
@@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
from djmoney.contrib.django_rest_framework.fields import MoneyField from djmoney.contrib.django_rest_framework.fields import MoneyField
from djmoney.money import Money from djmoney.money import Money
from djmoney.utils import MONEY_CLASSES, get_currency_field_name from djmoney.utils import MONEY_CLASSES, get_currency_field_name
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty from rest_framework.fields import empty
@@ -86,6 +87,7 @@ class InvenTreeMoneySerializer(MoneyField):
return amount return amount
@extend_schema_field(serializers.CharField())
class InvenTreeCurrencySerializer(serializers.ChoiceField): class InvenTreeCurrencySerializer(serializers.ChoiceField):
"""Custom serializers for selecting currency option.""" """Custom serializers for selecting currency option."""
@@ -111,6 +113,14 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
if 'help_text' not in kwargs: if 'help_text' not in kwargs:
kwargs['help_text'] = _('Select currency from available options') kwargs['help_text'] = _('Select currency from available options')
if InvenTree.ready.isGeneratingSchema():
kwargs['help_text'] = (
kwargs['help_text']
+ '\n\n'
+ '\n'.join(f'* `{value}` - {label}' for value, label in choices)
+ "\n\nOther valid currencies may be found in the 'CURRENCY_CODES' global setting."
)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
+43 -1
View File
@@ -21,13 +21,20 @@ from django.core.validators import URLValidator
from django.http import Http404, HttpResponseGone from django.http import Http404, HttpResponseGone
import structlog import structlog
from corsheaders.defaults import default_headers as default_cors_headers
from dotenv import load_dotenv from dotenv import load_dotenv
from InvenTree.cache import get_cache_config, is_global_cache_enabled from InvenTree.cache import get_cache_config, is_global_cache_enabled
from InvenTree.config import get_boolean_setting, get_custom_file, get_setting from InvenTree.config import (
get_boolean_setting,
get_custom_file,
get_oidc_private_key,
get_setting,
)
from InvenTree.ready import isInMainThread from InvenTree.ready import isInMainThread
from InvenTree.sentry import default_sentry_dsn, init_sentry from InvenTree.sentry import default_sentry_dsn, init_sentry
from InvenTree.version import checkMinPythonVersion, inventreeApiVersion from InvenTree.version import checkMinPythonVersion, inventreeApiVersion
from users.oauth2_scopes import oauth2_scopes
from . import config, locales from . import config, locales
@@ -298,6 +305,7 @@ INSTALLED_APPS = [
'django_otp', # OTP is needed for MFA - base package 'django_otp', # OTP is needed for MFA - base package
'django_otp.plugins.otp_totp', # Time based OTP 'django_otp.plugins.otp_totp', # Time based OTP
'django_otp.plugins.otp_static', # Backup codes 'django_otp.plugins.otp_static', # Backup codes
'oauth2_provider', # OAuth2 provider and API access
'drf_spectacular', # API documentation 'drf_spectacular', # API documentation
'django_ical', # For exporting calendars 'django_ical', # For exporting calendars
] ]
@@ -321,6 +329,7 @@ MIDDLEWARE = CONFIG.get(
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'InvenTree.middleware.AuthRequiredMiddleware', 'InvenTree.middleware.AuthRequiredMiddleware',
'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA 'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
'oauth2_provider.middleware.OAuth2TokenMiddleware', # oauth2_provider
'maintenance_mode.middleware.MaintenanceModeMiddleware', 'maintenance_mode.middleware.MaintenanceModeMiddleware',
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting 'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching 'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
@@ -353,6 +362,7 @@ QUERYCOUNT = {
AUTHENTICATION_BACKENDS = CONFIG.get( AUTHENTICATION_BACKENDS = CONFIG.get(
'authentication_backends', 'authentication_backends',
[ [
'oauth2_provider.backends.OAuth2Backend', # OAuth2 provider
'django.contrib.auth.backends.RemoteUserBackend', # proxy login 'django.contrib.auth.backends.RemoteUserBackend', # proxy login
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers 'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers
@@ -522,6 +532,15 @@ TEMPLATES = [
} }
] ]
OAUTH2_PROVIDER = {
# default scopes
'SCOPES': oauth2_scopes,
# OIDC
'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY': get_oidc_private_key(),
'PKCE_REQUIRED': False,
}
REST_FRAMEWORK = { REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler', 'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
'DATETIME_FORMAT': '%Y-%m-%d %H:%M', 'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
@@ -529,12 +548,14 @@ REST_FRAMEWORK = {
'users.authentication.ApiTokenAuthentication', 'users.authentication.ApiTokenAuthentication',
'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'users.authentication.ExtendedOAuth2Authentication',
], ],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions', 'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.RolePermission', 'InvenTree.permissions.RolePermission',
'InvenTree.permissions.InvenTreeTokenMatchesOASRequirements',
], ],
'DEFAULT_SCHEMA_CLASS': 'InvenTree.schema.ExtendedAutoSchema', 'DEFAULT_SCHEMA_CLASS': 'InvenTree.schema.ExtendedAutoSchema',
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata', 'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
@@ -1203,6 +1224,11 @@ CORS_ALLOWED_ORIGIN_REGEXES = get_setting(
typecast=list, typecast=list,
) )
# Allow extra CORS headers in DEBUG mode
# Required for serving /static/ and /media/ files
if DEBUG:
CORS_ALLOW_HEADERS = (*default_cors_headers, 'cache-control', 'pragma', 'expires')
# In debug mode allow CORS requests from localhost # In debug mode allow CORS requests from localhost
# This allows connection from the frontend development server # This allows connection from the frontend development server
if DEBUG: if DEBUG:
@@ -1395,6 +1421,7 @@ FLAGS = {
'NEXT_GEN': [ 'NEXT_GEN': [
{'condition': 'parameter', 'value': 'ngen='} {'condition': 'parameter', 'value': 'ngen='}
], # Should next-gen features be turned on? ], # Should next-gen features be turned on?
'OIDC': [{'condition': 'parameter', 'value': 'oidc='}],
} }
# Get custom flags from environment/yaml # Get custom flags from environment/yaml
@@ -1428,7 +1455,22 @@ SPECTACULAR_SETTINGS = {
'POSTPROCESSING_HOOKS': [ 'POSTPROCESSING_HOOKS': [
'drf_spectacular.hooks.postprocess_schema_enums', 'drf_spectacular.hooks.postprocess_schema_enums',
'InvenTree.schema.postprocess_required_nullable', 'InvenTree.schema.postprocess_required_nullable',
'InvenTree.schema.postprocess_print_stats',
], ],
'ENUM_NAME_OVERRIDES': {
'UserTypeEnum': 'users.models.UserProfile.UserType',
'TemplateModelTypeEnum': 'report.models.ReportTemplateBase.ModelChoices',
'AttachmentModelTypeEnum': 'common.models.Attachment.ModelChoices',
'DataImportSessionModelTypeEnum': 'importer.models.DataImportSession.ModelChoices',
# Allauth
'UnauthorizedStatus': [[401, 401]],
'IsTrueEnum': [[True, True]],
},
# oAuth2
'OAUTH2_FLOWS': ['authorizationCode', 'clientCredentials'],
'OAUTH2_AUTHORIZATION_URL': '/o/authorize/',
'OAUTH2_TOKEN_URL': '/o/token/',
'OAUTH2_REFRESH_URL': '/o/revoke_token/',
} }
if SITE_URL and not TESTING: if SITE_URL and not TESTING:
+1 -1
View File
@@ -67,7 +67,7 @@ def check_system_health(**kwargs):
if not InvenTree.helpers_email.is_email_configured(): # pragma: no cover if not InvenTree.helpers_email.is_email_configured(): # pragma: no cover
result = False result = False
if not settings.DEBUG: if not settings.DEBUG:
logger.warning('Email backend not configured') logger.warning('INVE-W7: Email backend not configured')
if not result: # pragma: no cover if not result: # pragma: no cover
if not settings.DEBUG: if not settings.DEBUG:
+14 -1
View File
@@ -705,4 +705,17 @@ def email_user(user_id: int, subject: str, message: str) -> None:
logger.warning('User <%s> not found - cannot send welcome message', user_id) logger.warning('User <%s> not found - cannot send welcome message', user_id)
return return
user.email_user(subject=subject, message=message) from InvenTree.helpers_email import get_email_for_user, send_email
if email := get_email_for_user(user):
send_email(subject, message, [email])
@scheduled_task(ScheduledTask.DAILY)
def run_oauth_maintenance():
"""Run the OAuth maintenance task(s)."""
from oauth2_provider.models import clear_expired
logger.info('Starting OAuth maintenance task')
clear_expired()
logger.info('Completed OAuth maintenance task')
+102 -5
View File
@@ -12,7 +12,8 @@ from InvenTree.api import read_license_file
from InvenTree.api_version import INVENTREE_API_VERSION from InvenTree.api_version import INVENTREE_API_VERSION
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
from InvenTree.version import inventreeApiText, parse_version_text from InvenTree.version import inventreeApiText, parse_version_text
from users.models import RuleSet, update_group_roles from users.ruleset import RULESET_NAMES
from users.tasks import update_group_roles
class HTMLAPITests(InvenTreeTestCase): class HTMLAPITests(InvenTreeTestCase):
@@ -142,7 +143,7 @@ class ApiAccessTests(InvenTreeAPITestCase):
role_names = roles.keys() role_names = roles.keys()
# By default, no permissions are provided # By default, no permissions are provided
for rule in RuleSet.RULESET_NAMES: for rule in RULESET_NAMES:
self.assertIn(rule, role_names) self.assertIn(rule, role_names)
if roles[rule] is None: if roles[rule] is None:
@@ -167,7 +168,7 @@ class ApiAccessTests(InvenTreeAPITestCase):
roles = response.data['roles'] roles = response.data['roles']
for rule in RuleSet.RULESET_NAMES: for rule in RULESET_NAMES:
self.assertIn(rule, roles.keys()) self.assertIn(rule, roles.keys())
for perm in ['view', 'add', 'change', 'delete']: for perm in ['view', 'add', 'change', 'delete']:
@@ -297,6 +298,7 @@ class SearchTests(InvenTreeAPITestCase):
'stock', 'stock',
'order', 'order',
'sales_order', 'sales_order',
'build',
] ]
roles = ['build.view', 'part.view'] roles = ['build.view', 'part.view']
@@ -353,6 +355,82 @@ class SearchTests(InvenTreeAPITestCase):
self.assertNotIn('stockitem', response.data) self.assertNotIn('stockitem', response.data)
self.assertNotIn('build', response.data) self.assertNotIn('build', response.data)
def test_search_filters(self):
"""Test that the regex, whole word, and notes filters are handled correctly."""
SEARCH_TERM = 'some note'
RE_SEARCH_TERM = 'some (.*) note'
response = self.post(
reverse('api-search'),
{'search': SEARCH_TERM, 'limit': 10, 'part': {}, 'build': {}},
expected_code=200,
)
# No build or part results
self.assertEqual(response.data['build']['count'], 0)
self.assertEqual(response.data['part']['count'], 0)
# add the search_notes param
response = self.post(
reverse('api-search'),
{
'search': SEARCH_TERM,
'limit': 10,
'search_notes': True,
'part': {},
'build': {},
},
expected_code=200,
)
# now should have some build results
self.assertEqual(response.data['build']['count'], 4)
# use the regex term
response = self.post(
reverse('api-search'),
{
'search': RE_SEARCH_TERM,
'limit': 10,
'search_notes': True,
'part': {},
'build': {},
},
expected_code=200,
)
# No results again
self.assertEqual(response.data['build']['count'], 0)
# add the regex_search param
response = self.post(
reverse('api-search'),
{
'search': RE_SEARCH_TERM,
'limit': 10,
'search_notes': True,
'search_regex': True,
'part': {},
'build': {},
},
expected_code=200,
)
# we get our results back!
self.assertEqual(response.data['build']['count'], 4)
# add the search_whole param
response = self.post(
reverse('api-search'),
{
'search': RE_SEARCH_TERM,
'limit': 10,
'search_notes': True,
'search_whole': True,
'part': {},
'build': {},
},
expected_code=200,
)
# No results again
self.assertEqual(response.data['build']['count'], 0)
def test_permissions(self): def test_permissions(self):
"""Test that users with insufficient permissions are handled correctly.""" """Test that users with insufficient permissions are handled correctly."""
# First, remove all roles # First, remove all roles
@@ -437,24 +515,43 @@ class GeneralApiTests(InvenTreeAPITestCase):
def test_inventree_api_text_fnc(self): def test_inventree_api_text_fnc(self):
"""Test that the inventreeApiText function works expected.""" """Test that the inventreeApiText function works expected."""
latest_version = f'v{INVENTREE_API_VERSION}'
# Normal run # Normal run
resp = inventreeApiText() resp = inventreeApiText()
self.assertEqual(len(resp), 10) self.assertEqual(len(resp), 10)
self.assertIn(latest_version, resp)
# More responses # More responses
resp = inventreeApiText(20) resp = inventreeApiText(20)
self.assertEqual(len(resp), 20) self.assertEqual(len(resp), 20)
self.assertIn(latest_version, resp)
# Specific version # Specific version
resp = inventreeApiText(start_version=5) resp = inventreeApiText(start_version=5)
self.assertEqual(list(resp)[0], 'v5') self.assertEqual(list(resp)[0], 'v5')
self.assertEqual(list(resp)[-1], 'v14')
def test_parse_version_text_fnc(self): def test_parse_version_text_fnc(self):
"""Test that api version text is correctly parsed.""" """Test that api version text is correctly parsed."""
resp = parse_version_text() resp = parse_version_text()
# Check that all texts are parsed latest_version = INVENTREE_API_VERSION
self.assertEqual(len(resp), INVENTREE_API_VERSION - 1) self.assertTrue(resp[f'v{latest_version}']['latest'])
# All fields except github link should exist for every version
latest_count = 0
for k, v in resp.items():
self.assertEqual('v', k[0], f'Version should start with v: {k}')
self.assertEqual(k, v['version'])
self.assertGreater(len(v['date']), 0, f'Date is missing from {v}')
self.assertGreater(len(v['text']), 0, f'Text is missing from {v}')
self.assertIsNotNone(v['latest'])
latest_count = latest_count + (1 if v['latest'] else 0)
self.assertEqual(1, latest_count, 'Should have a single version marked latest')
# Check that all texts are parsed: v1 and v2 are missing
self.assertEqual(len(resp), INVENTREE_API_VERSION - 2)
def test_api_license(self): def test_api_license(self):
"""Test that the license endpoint is working.""" """Test that the license endpoint is working."""
@@ -0,0 +1,13 @@
"""Tests for custom InvenTree management commands."""
from django.core.management import call_command
from django.test import TestCase
class CommandTestCase(TestCase):
"""Test case for custom management commands."""
def test_schema(self):
"""Test the schema generation command."""
output = call_command('schema', file='schema.yml', verbosity=0)
self.assertEqual(output, 'done')
+28 -13
View File
@@ -87,7 +87,7 @@ class InvenTreeTaskTests(TestCase):
): ):
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexist') InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexist')
def test_task_hearbeat(self): def test_task_heartbeat(self):
"""Test the task heartbeat.""" """Test the task heartbeat."""
InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat) InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat)
@@ -190,26 +190,41 @@ class InvenTreeTaskTests(TestCase):
from common.models import NotificationEntry, NotificationMessage from common.models import NotificationEntry, NotificationMessage
# Create a staff user (to ensure notifications are sent) # Create a staff user (to ensure notifications are sent)
User.objects.create_user(username='staff', password='staffpass', is_staff=True) user = User.objects.create_user(
username='staff', password='staffpass', is_staff=False
)
n_tasks = Task.objects.count() n_tasks = Task.objects.count()
n_entries = NotificationEntry.objects.count() n_entries = NotificationEntry.objects.count()
n_messages = NotificationMessage.objects.count() n_messages = NotificationMessage.objects.count()
test_data = {
'name': 'failed_task',
'func': 'InvenTree.tasks.failed_task',
'group': 'test',
'success': False,
'started': timezone.now(),
'stopped': timezone.now(),
'attempt_count': 10,
}
# Create a 'failed' task in the database # Create a 'failed' task in the database
# Note: The 'attempt count' is set to 10 to ensure that the task is properly marked as 'failed' # Note: The 'attempt count' is set to 10 to ensure that the task is properly marked as 'failed'
Task.objects.create( Task.objects.create(id=n_tasks + 1, **test_data)
id=n_tasks + 1,
name='failed_task',
func='InvenTree.tasks.failed_task',
group='test',
success=False,
started=timezone.now(),
stopped=timezone.now(),
attempt_count=10,
)
# A new notification entry should be created # A new notification entry should NOT be created (yet) - due to lack of permission for the user
self.assertEqual(NotificationEntry.objects.count(), n_entries + 0)
self.assertEqual(NotificationMessage.objects.count(), n_messages + 0)
# Give them all the required staff level permissions
user.is_staff = True
user.save()
# Create a 'failed' task in the database
# Note: The 'attempt count' is set to 10 to ensure that the task is properly marked as 'failed'
Task.objects.create(id=n_tasks + 2, **test_data)
# A new notification entry should be created (as the user now has permission to see it)
self.assertEqual(NotificationEntry.objects.count(), n_entries + 1) self.assertEqual(NotificationEntry.objects.count(), n_entries + 1)
self.assertEqual(NotificationMessage.objects.count(), n_messages + 1) self.assertEqual(NotificationMessage.objects.count(), n_messages + 1)
@@ -39,6 +39,9 @@ def setup_tracing(
headers: The headers to send with the traces. headers: The headers to send with the traces.
resources_input: The resources to send with the traces. resources_input: The resources to send with the traces.
console: Whether to output the traces to the console. console: Whether to output the traces to the console.
auth: Dict with auth information
is_http: Whether to use HTTP or gRPC for the exporter.
append_http: Whether to append '/v1/traces' to the endpoint.
""" """
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations(): if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
return return
+16 -9
View File
@@ -11,7 +11,7 @@ from pathlib import Path
from unittest import mock from unittest import mock
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission, User
from django.db import connections, models from django.db import connections, models
from django.http.response import StreamingHttpResponse from django.http.response import StreamingHttpResponse
from django.test import TestCase from django.test import TestCase
@@ -25,16 +25,23 @@ from plugin import registry
from plugin.models import PluginConfig from plugin.models import PluginConfig
def addUserPermission(user, permission): def addUserPermission(user: User, app_name: str, model_name: str, perm: str) -> None:
"""Shortcut function for adding a certain permission to a user.""" """Add a specific permission for the provided user.
perm = Permission.objects.get(codename=permission)
user.user_permissions.add(perm)
Arguments:
user: The user to add the permission to
app_name: The name of the app (e.g. 'part')
model_name: The name of the model (e.g. 'location')
perm: The permission to add (e.g. 'add', 'change', 'delete', 'view')
"""
# Get the permission object
permission = Permission.objects.get(
content_type__model=model_name, codename=f'{perm}_{model_name}'
)
def addUserPermissions(user, permissions): # Add the permission to the user
"""Shortcut function for adding multiple permissions to a user.""" user.user_permissions.add(permission)
for permission in permissions: user.save()
addUserPermission(user, permission)
def getMigrationFileNames(app): def getMigrationFileNames(app):
+4
View File
@@ -12,6 +12,8 @@ from django.views.generic.base import RedirectView
from allauth.headless.urls import Client, build_urlpatterns from allauth.headless.urls import Client, build_urlpatterns
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
from flags.urls import flagged_path
from oauth2_provider import urls as oauth2_urls
from sesame.views import LoginView from sesame.views import LoginView
import build.api import build.api
@@ -125,6 +127,8 @@ backendpatterns = [
), # Used for (DRF) browsable API auth ), # Used for (DRF) browsable API auth
path('auth/', auth_request), # Used for proxies to check if user is authenticated path('auth/', auth_request), # Used for proxies to check if user is authenticated
path('accounts/', include('allauth.urls')), path('accounts/', include('allauth.urls')),
# OAuth2
flagged_path('OIDC', 'o/', include(oauth2_urls)),
path( path(
'accounts/login/', 'accounts/login/',
RedirectView.as_view(url=f'/{settings.FRONTEND_URL_BASE}', permanent=False), RedirectView.as_view(url=f'/{settings.FRONTEND_URL_BASE}', permanent=False),
+20 -12
View File
@@ -26,13 +26,21 @@ logger = logging.getLogger('inventree')
# Discover git # Discover git
try: try:
from dulwich.errors import NotGitRepository
from dulwich.porcelain import active_branch from dulwich.porcelain import active_branch
from dulwich.repo import Repo from dulwich.repo import Repo
main_repo = Repo(pathlib.Path(__file__).parent.parent.parent.parent.parent)
main_commit = main_repo[main_repo.head()]
try: try:
main_branch = active_branch(main_repo) main_repo = Repo(pathlib.Path(__file__).parent.parent.parent.parent.parent)
main_commit = main_repo[main_repo.head()]
except NotGitRepository:
# If we are running in a docker container, the repo may not be available
logger.warning('INVE-W3: Could not detect git information.')
main_repo = None
main_commit = None
try:
main_branch = active_branch(main_repo) if main_repo else None
except (KeyError, IndexError): except (KeyError, IndexError):
logger.warning('INVE-W1: Current branch could not be detected.') logger.warning('INVE-W1: Current branch could not be detected.')
main_branch = None main_branch = None
@@ -129,11 +137,6 @@ def inventreeAppUrl():
return 'https://docs.inventree.org/app/' return 'https://docs.inventree.org/app/'
def inventreeCreditsUrl():
"""Return URL for InvenTree credits site."""
return 'https://docs.inventree.org/en/latest/credits/'
def inventreeGithubUrl(): def inventreeGithubUrl():
"""Return URL for InvenTree github site.""" """Return URL for InvenTree github site."""
return 'https://github.com/InvenTree/InvenTree/' return 'https://github.com/InvenTree/InvenTree/'
@@ -172,22 +175,27 @@ def parse_version_text():
# Remove first newline on latest version # Remove first newline on latest version
patched_data[0] = patched_data[0].replace('\n', '', 1) patched_data[0] = patched_data[0].replace('\n', '', 1)
latest_version = f'v{INVENTREE_API_VERSION}'
version_data = {} version_data = {}
for version in patched_data: for version in patched_data:
data = version.split('\n') data = version.split('\n')
version_split = data[0].split(' -> ') version_split = data[0].split(' -> ')
version_string = version_split[0].strip()
if version_string == '':
continue
version_detail = ( version_detail = (
version_split[1].split(':', 1) if len(version_split) > 1 else [''] version_split[1].split(':', 1) if len(version_split) > 1 else ['']
) )
new_data = { new_data = {
'version': version_split[0].strip(), 'version': version_string,
'date': version_detail[0].strip(), 'date': version_detail[0].strip(),
'gh': version_detail[1].strip() if len(version_detail) > 1 else None, 'gh': version_detail[1].strip() if len(version_detail) > 1 else None,
'text': data[1:], 'text': data[1:],
'latest': False, 'latest': latest_version == version_string,
} }
version_data[new_data['version']] = new_data version_data[version_string] = new_data
return version_data return version_data
@@ -206,7 +214,7 @@ def inventreeApiText(versions: int = 10, start_version: int = 0):
# Define the range of versions to return # Define the range of versions to return
if start_version == 0: if start_version == 0:
start_version = INVENTREE_API_VERSION - versions start_version = INVENTREE_API_VERSION - versions + 1
return { return {
f'v{a}': version_data.get(f'v{a}', None) f'v{a}': version_data.get(f'v{a}', None)
+2 -4
View File
@@ -920,8 +920,7 @@ build_api_urls = [
include([ include([
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(model=BuildItem),
{'model': BuildItem},
name='api-build-item-metadata', name='api-build-item-metadata',
), ),
path('', BuildItemDetail.as_view(), name='api-build-item-detail'), path('', BuildItemDetail.as_view(), name='api-build-item-detail'),
@@ -967,8 +966,7 @@ build_api_urls = [
path('unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'), path('unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(model=Build),
{'model': Build},
name='api-build-metadata', name='api-build-metadata',
), ),
path('', BuildDetail.as_view(), name='api-build-detail'), path('', BuildDetail.as_view(), name='api-build-detail'),
+15 -3
View File
@@ -628,8 +628,13 @@ class Build(
self.allocated_stock.delete() self.allocated_stock.delete()
@transaction.atomic @transaction.atomic
def complete_build(self, user, trim_allocated_stock=False): def complete_build(self, user: User, trim_allocated_stock: bool = False):
"""Mark this build as complete.""" """Mark this build as complete.
Arguments:
user: The user who is completing the build
trim_allocated_stock: If True, trim any allocated stock
"""
return self.handle_transition( return self.handle_transition(
self.status, self.status,
BuildStatus.COMPLETE.value, BuildStatus.COMPLETE.value,
@@ -681,6 +686,9 @@ class Build(
# Notify users that this build has been completed # Notify users that this build has been completed
targets = [self.issued_by, self.responsible] targets = [self.issued_by, self.responsible]
# Also inform anyone subscribed to the assembly part
targets.extend(self.part.get_subscribers())
# Notify those users interested in the parent build # Notify those users interested in the parent build
if self.parent: if self.parent:
targets.append(self.parent.issued_by) targets.append(self.parent.issued_by)
@@ -817,6 +825,7 @@ class Build(
Build, Build,
exclude=self.issued_by, exclude=self.issued_by,
content=InvenTreeNotificationBodies.OrderCanceled, content=InvenTreeNotificationBodies.OrderCanceled,
extra_users=self.part.get_subscribers(),
) )
trigger_event(BuildEvents.CANCELLED, id=self.pk) trigger_event(BuildEvents.CANCELLED, id=self.pk)
@@ -1470,7 +1479,10 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
# Notify the responsible users that the build order has been created # Notify the responsible users that the build order has been created
InvenTree.helpers_model.notify_responsible( InvenTree.helpers_model.notify_responsible(
instance, sender, exclude=instance.issued_by instance,
sender,
exclude=instance.issued_by,
extra_users=instance.part.get_subscribers(),
) )
else: else:
+6 -6
View File
@@ -40,7 +40,11 @@ from InvenTree.serializers import (
) )
from stock.generators import generate_batch_code from stock.generators import generate_batch_code
from stock.models import StockItem, StockLocation from stock.models import StockItem, StockLocation
from stock.serializers import LocationBriefSerializer, StockItemSerializerBrief from stock.serializers import (
LocationBriefSerializer,
StockItemSerializerBrief,
StockStatusCustomSerializer,
)
from stock.status_codes import StockStatus from stock.status_codes import StockStatus
from users.serializers import OwnerSerializer, UserSerializer from users.serializers import OwnerSerializer, UserSerializer
@@ -581,11 +585,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
help_text=_('Location for completed build outputs'), help_text=_('Location for completed build outputs'),
) )
status_custom_key = serializers.ChoiceField( status_custom_key = StockStatusCustomSerializer(default=StockStatus.OK.value)
choices=StockStatus.items(custom=True),
default=StockStatus.OK.value,
label=_('Status'),
)
accept_incomplete_allocation = serializers.BooleanField( accept_incomplete_allocation = serializers.BooleanField(
default=False, default=False,
+2
View File
@@ -262,6 +262,8 @@ def notify_overdue_build_order(bo: build_models.Build):
if bo.responsible: if bo.responsible:
targets.append(bo.responsible) targets.append(bo.responsible)
targets.extend(bo.part.get_subscribers())
name = _('Overdue Build Order') name = _('Overdue Build Order')
context = { context = {
+63 -79
View File
@@ -20,9 +20,9 @@ from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from error_report.models import Error from error_report.models import Error
from pint._typing import UnitLike from pint._typing import UnitLike
from rest_framework import permissions, serializers from rest_framework import serializers
from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@@ -44,7 +44,15 @@ from InvenTree.mixins import (
RetrieveUpdateAPI, RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI, RetrieveUpdateDestroyAPI,
) )
from InvenTree.permissions import IsStaffOrReadOnly, IsSuperuser from InvenTree.permissions import (
AllowAnyOrReadScope,
GlobalSettingsPermissions,
IsAdminOrAdminScope,
IsAuthenticatedOrReadScope,
IsStaffOrReadOnlyScope,
IsSuperuserOrSuperScope,
UserSettingsPermissionsOrScope,
)
from plugin.models import NotificationUserSetting from plugin.models import NotificationUserSetting
from plugin.serializers import NotificationUserSettingSerializer from plugin.serializers import NotificationUserSettingSerializer
@@ -134,7 +142,7 @@ class WebhookView(CsrfExemptMixin, APIView):
class CurrencyExchangeView(APIView): class CurrencyExchangeView(APIView):
"""API endpoint for displaying currency information.""" """API endpoint for displaying currency information."""
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
serializer_class = None serializer_class = None
@extend_schema(responses={200: common.serializers.CurrencyExchangeSerializer}) @extend_schema(responses={200: common.serializers.CurrencyExchangeSerializer})
@@ -178,7 +186,7 @@ class CurrencyRefreshView(APIView):
User must be a 'staff' user to access this endpoint User must be a 'staff' user to access this endpoint
""" """
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser] permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
serializer_class = None serializer_class = None
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -208,6 +216,7 @@ class GlobalSettingsList(SettingsList):
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_') queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer serializer_class = common.serializers.GlobalSettingsSerializer
permission_classes = [IsAuthenticated, GlobalSettingsPermissions]
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
"""Ensure all global settings are created.""" """Ensure all global settings are created."""
@@ -215,23 +224,6 @@ class GlobalSettingsList(SettingsList):
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)
class GlobalSettingsPermissions(permissions.BasePermission):
"""Special permission class to determine if the user is "staff"."""
def has_permission(self, request, view):
"""Check that the requesting user is 'admin'."""
try:
user = request.user
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
# Any other methods require staff access permissions
return user.is_staff
except AttributeError: # pragma: no cover
return False
class GlobalSettingsDetail(RetrieveUpdateAPI): class GlobalSettingsDetail(RetrieveUpdateAPI):
"""Detail view for an individual "global setting" object. """Detail view for an individual "global setting" object.
@@ -241,6 +233,7 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
lookup_field = 'key' lookup_field = 'key'
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_') queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
serializer_class = common.serializers.GlobalSettingsSerializer serializer_class = common.serializers.GlobalSettingsSerializer
permission_classes = [IsAuthenticated, GlobalSettingsPermissions]
def get_object(self): def get_object(self):
"""Attempt to find a global setting object with the provided key.""" """Attempt to find a global setting object with the provided key."""
@@ -253,14 +246,13 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
key, cache=False, create=True key, cache=False, create=True
) )
permission_classes = [permissions.IsAuthenticated, GlobalSettingsPermissions]
class UserSettingsList(SettingsList): class UserSettingsList(SettingsList):
"""API endpoint for accessing a list of user settings objects.""" """API endpoint for accessing a list of user settings objects."""
queryset = common.models.InvenTreeUserSetting.objects.all() queryset = common.models.InvenTreeUserSetting.objects.all()
serializer_class = common.serializers.UserSettingsSerializer serializer_class = common.serializers.UserSettingsSerializer
permission_classes = [UserSettingsPermissionsOrScope]
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
"""Ensure all user settings are created.""" """Ensure all user settings are created."""
@@ -288,19 +280,6 @@ class UserSettingsList(SettingsList):
return queryset return queryset
class UserSettingsPermissions(permissions.BasePermission):
"""Special permission class to determine if the user can view / edit a particular setting."""
def has_object_permission(self, request, view, obj):
"""Check if the user that requested is also the object owner."""
try:
user = request.user
except AttributeError: # pragma: no cover
return False
return user == obj.user
class UserSettingsDetail(RetrieveUpdateAPI): class UserSettingsDetail(RetrieveUpdateAPI):
"""Detail view for an individual "user setting" object. """Detail view for an individual "user setting" object.
@@ -310,6 +289,7 @@ class UserSettingsDetail(RetrieveUpdateAPI):
lookup_field = 'key' lookup_field = 'key'
queryset = common.models.InvenTreeUserSetting.objects.all() queryset = common.models.InvenTreeUserSetting.objects.all()
serializer_class = common.serializers.UserSettingsSerializer serializer_class = common.serializers.UserSettingsSerializer
permission_classes = [UserSettingsPermissionsOrScope]
def get_object(self): def get_object(self):
"""Attempt to find a user setting object with the provided key.""" """Attempt to find a user setting object with the provided key."""
@@ -325,14 +305,13 @@ class UserSettingsDetail(RetrieveUpdateAPI):
key, user=self.request.user, cache=False, create=True key, user=self.request.user, cache=False, create=True
) )
permission_classes = [UserSettingsPermissions]
class NotificationUserSettingsList(SettingsList): class NotificationUserSettingsList(SettingsList):
"""API endpoint for accessing a list of notification user settings objects.""" """API endpoint for accessing a list of notification user settings objects."""
queryset = NotificationUserSetting.objects.all() queryset = NotificationUserSetting.objects.all()
serializer_class = NotificationUserSettingSerializer serializer_class = NotificationUserSettingSerializer
permission_classes = [UserSettingsPermissionsOrScope]
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
"""Only list settings which apply to the current user.""" """Only list settings which apply to the current user."""
@@ -354,7 +333,7 @@ class NotificationUserSettingsDetail(RetrieveUpdateAPI):
queryset = NotificationUserSetting.objects.all() queryset = NotificationUserSetting.objects.all()
serializer_class = NotificationUserSettingSerializer serializer_class = NotificationUserSettingSerializer
permission_classes = [UserSettingsPermissions] permission_classes = [UserSettingsPermissionsOrScope]
class NotificationMessageMixin: class NotificationMessageMixin:
@@ -362,7 +341,7 @@ class NotificationMessageMixin:
queryset = common.models.NotificationMessage.objects.all() queryset = common.models.NotificationMessage.objects.all()
serializer_class = common.serializers.NotificationMessageSerializer serializer_class = common.serializers.NotificationMessageSerializer
permission_classes = [UserSettingsPermissions] permission_classes = [UserSettingsPermissionsOrScope]
def get_queryset(self): def get_queryset(self):
"""Return prefetched queryset.""" """Return prefetched queryset."""
@@ -384,7 +363,7 @@ class NotificationMessageMixin:
class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI): class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
"""List view for all notifications of the current user.""" """List view for all notifications of the current user."""
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
@@ -437,7 +416,7 @@ class NewsFeedMixin:
queryset = common.models.NewsFeedEntry.objects.all() queryset = common.models.NewsFeedEntry.objects.all()
serializer_class = common.serializers.NewsFeedEntrySerializer serializer_class = common.serializers.NewsFeedEntrySerializer
permission_classes = [IsAdminUser] permission_classes = [IsAdminOrAdminScope]
class NewsFeedEntryList(NewsFeedMixin, BulkDeleteMixin, ListAPI): class NewsFeedEntryList(NewsFeedMixin, BulkDeleteMixin, ListAPI):
@@ -461,7 +440,7 @@ class ConfigList(ListAPI):
queryset = CONFIG_LOOKUPS queryset = CONFIG_LOOKUPS
serializer_class = common.serializers.ConfigSerializer serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser] permission_classes = [IsSuperuserOrSuperScope]
# Specifically disable pagination for this view # Specifically disable pagination for this view
pagination_class = None pagination_class = None
@@ -471,7 +450,7 @@ class ConfigDetail(RetrieveAPI):
"""Detail view for an individual configuration.""" """Detail view for an individual configuration."""
serializer_class = common.serializers.ConfigSerializer serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser] permission_classes = [IsSuperuserOrSuperScope]
def get_object(self): def get_object(self):
"""Attempt to find a config object with the provided key.""" """Attempt to find a config object with the provided key."""
@@ -487,7 +466,7 @@ class NotesImageList(ListCreateAPI):
queryset = common.models.NotesImage.objects.all() queryset = common.models.NotesImage.objects.all()
serializer_class = common.serializers.NotesImageSerializer serializer_class = common.serializers.NotesImageSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
@@ -505,7 +484,7 @@ class ProjectCodeList(DataExportViewMixin, ListCreateAPI):
queryset = common.models.ProjectCode.objects.all() queryset = common.models.ProjectCode.objects.all()
serializer_class = common.serializers.ProjectCodeSerializer serializer_class = common.serializers.ProjectCodeSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] permission_classes = [IsStaffOrReadOnlyScope]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['code'] ordering_fields = ['code']
@@ -518,7 +497,7 @@ class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.ProjectCode.objects.all() queryset = common.models.ProjectCode.objects.all()
serializer_class = common.serializers.ProjectCodeSerializer serializer_class = common.serializers.ProjectCodeSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] permission_classes = [IsStaffOrReadOnlyScope]
class CustomUnitList(DataExportViewMixin, ListCreateAPI): class CustomUnitList(DataExportViewMixin, ListCreateAPI):
@@ -526,7 +505,7 @@ class CustomUnitList(DataExportViewMixin, ListCreateAPI):
queryset = common.models.CustomUnit.objects.all() queryset = common.models.CustomUnit.objects.all()
serializer_class = common.serializers.CustomUnitSerializer serializer_class = common.serializers.CustomUnitSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] permission_classes = [IsStaffOrReadOnlyScope]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
@@ -535,14 +514,14 @@ class CustomUnitDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.CustomUnit.objects.all() queryset = common.models.CustomUnit.objects.all()
serializer_class = common.serializers.CustomUnitSerializer serializer_class = common.serializers.CustomUnitSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] permission_classes = [IsStaffOrReadOnlyScope]
class AllUnitList(ListAPI): class AllUnitList(RetrieveAPI):
"""List of all defined units.""" """List of all defined units."""
serializer_class = common.serializers.AllUnitListResponseSerializer serializer_class = common.serializers.AllUnitListResponseSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] permission_classes = [IsStaffOrReadOnlyScope]
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Return a list of all available units.""" """Return a list of all available units."""
@@ -573,7 +552,7 @@ class ErrorMessageList(BulkDeleteMixin, ListAPI):
queryset = Error.objects.all() queryset = Error.objects.all()
serializer_class = common.serializers.ErrorMessageSerializer serializer_class = common.serializers.ErrorMessageSerializer
permission_classes = [permissions.IsAuthenticated, IsAdminUser] permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
@@ -589,13 +568,13 @@ class ErrorMessageDetail(RetrieveUpdateDestroyAPI):
queryset = Error.objects.all() queryset = Error.objects.all()
serializer_class = common.serializers.ErrorMessageSerializer serializer_class = common.serializers.ErrorMessageSerializer
permission_classes = [permissions.IsAuthenticated, IsAdminUser] permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
class BackgroundTaskOverview(APIView): class BackgroundTaskOverview(APIView):
"""Provides an overview of the background task queue status.""" """Provides an overview of the background task queue status."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser] permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
serializer_class = None serializer_class = None
def get(self, request, fmt=None): def get(self, request, fmt=None):
@@ -617,7 +596,7 @@ class BackgroundTaskOverview(APIView):
class PendingTaskList(BulkDeleteMixin, ListAPI): class PendingTaskList(BulkDeleteMixin, ListAPI):
"""Provides a read-only list of currently pending tasks.""" """Provides a read-only list of currently pending tasks."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser] permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
queryset = django_q.models.OrmQ.objects.all() queryset = django_q.models.OrmQ.objects.all()
serializer_class = common.serializers.PendingTaskSerializer serializer_class = common.serializers.PendingTaskSerializer
@@ -626,7 +605,7 @@ class PendingTaskList(BulkDeleteMixin, ListAPI):
class ScheduledTaskList(ListAPI): class ScheduledTaskList(ListAPI):
"""Provides a read-only list of currently scheduled tasks.""" """Provides a read-only list of currently scheduled tasks."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser] permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
queryset = django_q.models.Schedule.objects.all() queryset = django_q.models.Schedule.objects.all()
serializer_class = common.serializers.ScheduledTaskSerializer serializer_class = common.serializers.ScheduledTaskSerializer
@@ -646,7 +625,7 @@ class ScheduledTaskList(ListAPI):
class FailedTaskList(BulkDeleteMixin, ListAPI): class FailedTaskList(BulkDeleteMixin, ListAPI):
"""Provides a read-only list of currently failed tasks.""" """Provides a read-only list of currently failed tasks."""
permission_classes = [permissions.IsAuthenticated, IsAdminUser] permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
queryset = django_q.models.Failure.objects.all() queryset = django_q.models.Failure.objects.all()
serializer_class = common.serializers.FailedTaskSerializer serializer_class = common.serializers.FailedTaskSerializer
@@ -663,14 +642,14 @@ class FlagList(ListAPI):
queryset = settings.FLAGS queryset = settings.FLAGS
serializer_class = common.serializers.FlagSerializer serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny] permission_classes = [AllowAnyOrReadScope]
class FlagDetail(RetrieveAPI): class FlagDetail(RetrieveAPI):
"""Detail view for an individual feature flag.""" """Detail view for an individual feature flag."""
serializer_class = common.serializers.FlagSerializer serializer_class = common.serializers.FlagSerializer
permission_classes = [permissions.AllowAny] permission_classes = [AllowAnyOrReadScope]
def get_object(self): def get_object(self):
"""Attempt to find a config object with the provided key.""" """Attempt to find a config object with the provided key."""
@@ -686,7 +665,7 @@ class ContentTypeList(ListAPI):
queryset = ContentType.objects.all() queryset = ContentType.objects.all()
serializer_class = common.serializers.ContentTypeSerializer serializer_class = common.serializers.ContentTypeSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
search_fields = ['app_label', 'model'] search_fields = ['app_label', 'model']
@@ -696,10 +675,9 @@ class ContentTypeDetail(RetrieveAPI):
queryset = ContentType.objects.all() queryset = ContentType.objects.all()
serializer_class = common.serializers.ContentTypeSerializer serializer_class = common.serializers.ContentTypeSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
@extend_schema(operation_id='contenttype_retrieve_model')
class ContentTypeModelDetail(ContentTypeDetail): class ContentTypeModelDetail(ContentTypeDetail):
"""Detail view for a ContentType model.""" """Detail view for a ContentType model."""
@@ -714,6 +692,11 @@ class ContentTypeModelDetail(ContentTypeDetail):
raise NotFound() raise NotFound()
raise NotFound() raise NotFound()
@extend_schema(operation_id='contenttype_retrieve_model')
def get(self, request, *args, **kwargs):
"""Detail view for a ContentType model."""
return super().get(request, *args, **kwargs)
class AttachmentFilter(rest_filters.FilterSet): class AttachmentFilter(rest_filters.FilterSet):
"""Filterset for the AttachmentList API endpoint.""" """Filterset for the AttachmentList API endpoint."""
@@ -746,7 +729,7 @@ class AttachmentList(BulkDeleteMixin, ListCreateAPI):
queryset = common.models.Attachment.objects.all() queryset = common.models.Attachment.objects.all()
serializer_class = common.serializers.AttachmentSerializer serializer_class = common.serializers.AttachmentSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
filterset_class = AttachmentFilter filterset_class = AttachmentFilter
@@ -767,7 +750,7 @@ class AttachmentList(BulkDeleteMixin, ListCreateAPI):
- Ensure that the user has correct 'delete' permissions for each model - Ensure that the user has correct 'delete' permissions for each model
""" """
from common.validators import attachment_model_class_from_label from common.validators import attachment_model_class_from_label
from users.models import check_user_permission from users.permissions import check_user_permission
model_types = queryset.values_list('model_type', flat=True).distinct() model_types = queryset.values_list('model_type', flat=True).distinct()
@@ -784,7 +767,7 @@ class AttachmentDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.Attachment.objects.all() queryset = common.models.Attachment.objects.all()
serializer_class = common.serializers.AttachmentSerializer serializer_class = common.serializers.AttachmentSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
"""Check user permissions before deleting an attachment.""" """Check user permissions before deleting an attachment."""
@@ -803,11 +786,11 @@ class IconList(ListAPI):
"""List view for available icon packages.""" """List view for available icon packages."""
serializer_class = common.serializers.IconPackageSerializer serializer_class = common.serializers.IconPackageSerializer
permission_classes = [permissions.AllowAny] permission_classes = [AllowAnyOrReadScope]
def get_queryset(self): def get_queryset(self):
"""Return a list of all available icon packages.""" """Return a list of all available icon packages."""
return get_icon_packs().values() return list(get_icon_packs().values())
class SelectionListList(ListCreateAPI): class SelectionListList(ListCreateAPI):
@@ -815,7 +798,7 @@ class SelectionListList(ListCreateAPI):
queryset = common.models.SelectionList.objects.all() queryset = common.models.SelectionList.objects.all()
serializer_class = common.serializers.SelectionListSerializer serializer_class = common.serializers.SelectionListSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
def get_queryset(self): def get_queryset(self):
"""Override the queryset method to include entry count.""" """Override the queryset method to include entry count."""
@@ -827,7 +810,7 @@ class SelectionListDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.SelectionList.objects.all() queryset = common.models.SelectionList.objects.all()
serializer_class = common.serializers.SelectionListSerializer serializer_class = common.serializers.SelectionListSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
class EntryMixin: class EntryMixin:
@@ -835,7 +818,7 @@ class EntryMixin:
queryset = common.models.SelectionListEntry.objects.all() queryset = common.models.SelectionListEntry.objects.all()
serializer_class = common.serializers.SelectionEntrySerializer serializer_class = common.serializers.SelectionEntrySerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
lookup_url_kwarg = 'entrypk' lookup_url_kwarg = 'entrypk'
def get_queryset(self): def get_queryset(self):
@@ -854,22 +837,22 @@ class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI):
"""Detail view for a SelectionEntry object.""" """Detail view for a SelectionEntry object."""
class DataOutputEndpoint: class DataOutputEndpointMixin:
"""Mixin class for DataOutput endpoints.""" """Mixin class for DataOutput endpoints."""
queryset = common.models.DataOutput.objects.all() queryset = common.models.DataOutput.objects.all()
serializer_class = common.serializers.DataOutputSerializer serializer_class = common.serializers.DataOutputSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [IsAuthenticatedOrReadScope]
class DataOutputList(DataOutputEndpoint, BulkDeleteMixin, ListAPI): class DataOutputList(DataOutputEndpointMixin, BulkDeleteMixin, ListAPI):
"""List view for DataOutput objects.""" """List view for DataOutput objects."""
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['pk', 'user', 'plugin', 'output_type', 'created'] ordering_fields = ['pk', 'user', 'plugin', 'output_type', 'created']
class DataOutputDetail(DataOutputEndpoint, RetrieveAPI): class DataOutputDetail(DataOutputEndpointMixin, RetrieveAPI):
"""Detail view for a DataOutput object.""" """Detail view for a DataOutput object."""
@@ -982,8 +965,7 @@ common_api_urls = [
include([ include([
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(model=common.models.Attachment),
{'model': common.models.Attachment},
name='api-attachment-metadata', name='api-attachment-metadata',
), ),
path('', AttachmentDetail.as_view(), name='api-attachment-detail'), path('', AttachmentDetail.as_view(), name='api-attachment-detail'),
@@ -1008,8 +990,10 @@ common_api_urls = [
include([ include([
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(
{'model': common.models.ProjectCode}, model=common.models.ProjectCode,
permission_classes=[IsStaffOrReadOnlyScope],
),
name='api-project-code-metadata', name='api-project-code-metadata',
), ),
path( path(
@@ -0,0 +1,24 @@
# Generated by Django 4.2.20 on 2025-04-07 20:53
import common.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("common", "0037_dataoutput"),
]
operations = [
migrations.AlterField(
model_name="attachment",
name="model_type",
field=models.CharField(
help_text="Target model type for image",
max_length=100,
validators=[common.validators.validate_attachment_model_type],
verbose_name="Model type",
),
),
]
+30 -10
View File
@@ -40,15 +40,11 @@ from djmoney.contrib.exchange.models import convert_money
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
import common.currency
import common.validators import common.validators
import InvenTree.exceptions
import InvenTree.fields import InvenTree.fields
import InvenTree.helpers import InvenTree.helpers
import InvenTree.models import InvenTree.models
import InvenTree.ready import InvenTree.ready
import InvenTree.tasks
import InvenTree.validators
import users.models import users.models
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
from generic.states import ColorEnum from generic.states import ColorEnum
@@ -59,6 +55,24 @@ from InvenTree.sanitizer import sanitize_svg
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
class RenderMeta(models.enums.ChoicesMeta):
"""Metaclass for rendering choices."""
choice_fnc = None
@property
def choices(self):
"""Return a list of choices for the enum class."""
fnc = getattr(self, 'choice_fnc', None)
if fnc:
return fnc()
return []
class RenderChoices(models.TextChoices, metaclass=RenderMeta):
"""Class for creating enumerated string choices for schema rendering."""
class MetaMixin(models.Model): class MetaMixin(models.Model):
"""A base class for InvenTree models to include shared meta fields. """A base class for InvenTree models to include shared meta fields.
@@ -1766,15 +1780,15 @@ def after_custom_unit_updated(sender, instance, **kwargs):
reload_unit_registry() reload_unit_registry()
def rename_attachment(instance, filename): def rename_attachment(instance, filename: str):
"""Callback function to rename an uploaded attachment file. """Callback function to rename an uploaded attachment file.
Arguments: Args:
- instance: The Attachment instance instance (Attachment): The Attachment instance for which the file is being renamed.
- filename: The original filename of the uploaded file filename (str): The original filename of the uploaded file.
Returns: Returns:
- The new filename for the uploaded file, e.g. 'attachments/<model_type>/<model_id>/<filename>' str: The new filename for the uploaded file, e.g. 'attachments/<model_type>/<model_id>/<filename>'.
""" """
# Remove any illegal characters from the filename # Remove any illegal characters from the filename
illegal_chars = '\'"\\`~#|!@#$%^&*()[]{}<>?;:+=,' illegal_chars = '\'"\\`~#|!@#$%^&*()[]{}<>?;:+=,'
@@ -1811,6 +1825,11 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
verbose_name = _('Attachment') verbose_name = _('Attachment')
class ModelChoices(RenderChoices):
"""Model choices for attachments."""
choice_fnc = common.validators.attachment_model_options
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Custom 'save' method for the Attachment model. """Custom 'save' method for the Attachment model.
@@ -1859,7 +1878,8 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
model_type = models.CharField( model_type = models.CharField(
max_length=100, max_length=100,
validators=[common.validators.validate_attachment_model_type], validators=[common.validators.validate_attachment_model_type],
help_text=_('Target model type for this image'), verbose_name=_('Model type'),
help_text=_('Target model type for image'),
) )
model_id = models.PositiveIntegerField() model_id = models.PositiveIntegerField()
+58 -27
View File
@@ -6,6 +6,7 @@ from typing import Optional
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.db.models import Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import structlog import structlog
@@ -16,6 +17,7 @@ from InvenTree.ready import isImportingData, isRebuildingData
from plugin import registry from plugin import registry
from plugin.models import NotificationUserSetting, PluginConfig from plugin.models import NotificationUserSetting, PluginConfig
from users.models import Owner from users.models import Owner
from users.permissions import check_user_permission
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
@@ -31,7 +33,7 @@ class NotificationMethod:
GLOBAL_SETTING = None GLOBAL_SETTING = None
USER_SETTING = None USER_SETTING = None
def __init__(self, obj, category, targets, context) -> None: def __init__(self, obj: Model, category: str, targets: list, context) -> None:
"""Check that the method is read. """Check that the method is read.
This checks that: This checks that:
@@ -357,8 +359,21 @@ class InvenTreeNotificationBodies:
) )
def trigger_notification(obj, category=None, obj_ref='pk', **kwargs): def trigger_notification(
"""Send out a notification.""" obj: Model, category: Optional[str] = None, obj_ref: str = 'pk', **kwargs
):
"""Send out a notification.
Args:
obj: The object (model instance) that is triggering the notification
category: The category (label) for the notification
obj_ref: The reference to the object that should be used for the notification
kwargs: Additional arguments to pass to the notification method
"""
# Check if data is importing currently
if isImportingData() or isRebuildingData():
return
targets = kwargs.get('targets') targets = kwargs.get('targets')
target_fnc = kwargs.get('target_fnc') target_fnc = kwargs.get('target_fnc')
target_args = kwargs.get('target_args', []) target_args = kwargs.get('target_args', [])
@@ -368,10 +383,6 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
delivery_methods = kwargs.get('delivery_methods') delivery_methods = kwargs.get('delivery_methods')
check_recent = kwargs.get('check_recent', True) check_recent = kwargs.get('check_recent', True)
# Check if data is importing currently
if isImportingData() or isRebuildingData():
return
# Resolve object reference # Resolve object reference
refs = [obj_ref, 'pk', 'id', 'uid'] refs = [obj_ref, 'pk', 'id', 'uid']
@@ -441,26 +452,40 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
) )
if target_users: if target_users:
logger.info("Sending notification '%s' for '%s'", category, str(obj)) # Filter out any users who are inactive, or do not have the required model permissions
valid_users = list(
filter(
lambda u: u and u.is_active and check_user_permission(u, obj, 'view'),
list(target_users),
)
)
# Collect possible methods if len(valid_users) > 0:
if delivery_methods is None: logger.info(
delivery_methods = storage.methods or [] "Sending notification '%s' for '%s' to %s users",
else: category,
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS str(obj),
len(valid_users),
)
for method in delivery_methods: # Collect possible methods
logger.info("Triggering notification method '%s'", method.METHOD_NAME) if delivery_methods is None:
try: delivery_methods = storage.methods or []
deliver_notification(method, obj, category, target_users, context) else:
except NotImplementedError as error: delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
# Allow any single notification method to fail, without failing the others
logger.error(error)
except Exception as error:
logger.error(error)
# Set delivery flag for method in delivery_methods:
common.models.NotificationEntry.notify(category, obj_ref_value) logger.info("Triggering notification method '%s'", method.METHOD_NAME)
try:
deliver_notification(method, obj, category, valid_users, context)
except NotImplementedError as error:
# Allow any single notification method to fail, without failing the others
logger.error(error)
except Exception as error:
logger.error(error)
# Set delivery flag
common.models.NotificationEntry.notify(category, obj_ref_value)
else: else:
logger.info("No possible users for notification '%s'", category) logger.info("No possible users for notification '%s'", category)
@@ -484,12 +509,18 @@ def trigger_superuser_notification(plugin: PluginConfig, msg: str):
def deliver_notification( def deliver_notification(
cls: NotificationMethod, obj, category: str, targets, context: dict cls: NotificationMethod, obj: Model, category: str, targets: list, context: dict
): ):
"""Send notification with the provided class. """Send notification with the provided class.
This: Arguments:
- Intis the method cls: The class that should be used to send the notification
obj: The object (model instance) that triggered the notification
category: The category (label) for the notification
targets: List of users that should receive the notification
context: Context dictionary with additional information for the notification
- Initializes the method
- Checks that there are valid targets - Checks that there are valid targets
- Runs the delivery setup - Runs the delivery setup
- Sends notifications either via `send_bulk` or send` - Sends notifications either via `send_bulk` or send`
+2 -2
View File
@@ -428,7 +428,7 @@ class AllUnitListResponseSerializer(serializers.Serializer):
default_system = serializers.CharField() default_system = serializers.CharField()
available_systems = serializers.ListField(child=serializers.CharField()) available_systems = serializers.ListField(child=serializers.CharField())
available_units = Unit(many=True) available_units = serializers.DictField(child=Unit())
class ErrorMessageSerializer(InvenTreeModelSerializer): class ErrorMessageSerializer(InvenTreeModelSerializer):
@@ -619,7 +619,7 @@ class AttachmentSerializer(InvenTreeModelSerializer):
def save(self, **kwargs): def save(self, **kwargs):
"""Override the save method to handle the model_type field.""" """Override the save method to handle the model_type field."""
from InvenTree.models import InvenTreeAttachmentMixin from InvenTree.models import InvenTreeAttachmentMixin
from users.models import check_user_permission from users.permissions import check_user_permission
model_type = self.validated_data.get('model_type', None) model_type = self.validated_data.get('model_type', None)
@@ -165,6 +165,14 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'SEARCH_NOTES': {
'name': _('Search Notes'),
'description': _(
"Search queries return results for matches from the item's notes"
),
'default': False,
'validator': bool,
},
'PART_SHOW_QUANTITY_IN_FORMS': { 'PART_SHOW_QUANTITY_IN_FORMS': {
'name': _('Show Quantity in Forms'), 'name': _('Show Quantity in Forms'),
'description': _('Display available part quantity in some forms'), 'description': _('Display available part quantity in some forms'),
+7 -3
View File
@@ -28,6 +28,7 @@ from InvenTree.unit_test import (
InvenTreeAPITestCase, InvenTreeAPITestCase,
InvenTreeTestCase, InvenTreeTestCase,
PluginMixin, PluginMixin,
addUserPermission,
) )
from part.models import Part, PartParameterTemplate from part.models import Part, PartParameterTemplate
from plugin import registry from plugin import registry
@@ -1077,6 +1078,9 @@ class NotificationTest(InvenTreeAPITestCase):
"""Tests for bulk deletion of user notifications.""" """Tests for bulk deletion of user notifications."""
from error_report.models import Error from error_report.models import Error
# Ensure *this* user has permission to view error reports
addUserPermission(self.user, 'error_report', 'error', 'view')
# Create some notification messages by throwing errors # Create some notification messages by throwing errors
for _ii in range(10): for _ii in range(10):
Error.objects.create() Error.objects.create()
@@ -1088,7 +1092,7 @@ class NotificationTest(InvenTreeAPITestCase):
# However, one user is marked as inactive # However, one user is marked as inactive
self.assertEqual(messages.count(), 20) self.assertEqual(messages.count(), 20)
# Only 10 messages related to *this* user # Only messages related to *this* user
my_notifications = messages.filter(user=self.user) my_notifications = messages.filter(user=self.user)
self.assertEqual(my_notifications.count(), 10) self.assertEqual(my_notifications.count(), 10)
@@ -1169,7 +1173,7 @@ class CommonTest(InvenTreeAPITestCase):
"""Test flag URLs.""" """Test flag URLs."""
# Not superuser # Not superuser
response = self.get(reverse('api-flag-list'), expected_code=200) response = self.get(reverse('api-flag-list'), expected_code=200)
self.assertEqual(len(response.data), 2) self.assertEqual(len(response.data), 3)
self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL') self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL')
# Turn into superuser # Turn into superuser
@@ -1178,7 +1182,7 @@ class CommonTest(InvenTreeAPITestCase):
# Successful checks # Successful checks
response = self.get(reverse('api-flag-list'), expected_code=200) response = self.get(reverse('api-flag-list'), expected_code=200)
self.assertEqual(len(response.data), 2) self.assertEqual(len(response.data), 3)
self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL') self.assertEqual(response.data[0]['key'], 'EXPERIMENTAL')
self.assertTrue(response.data[0]['conditions']) self.assertTrue(response.data[0]['conditions'])
+4 -8
View File
@@ -516,8 +516,7 @@ manufacturer_part_api_urls = [
include([ include([
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(model=ManufacturerPart),
{'model': ManufacturerPart},
name='api-manufacturer-part-metadata', name='api-manufacturer-part-metadata',
), ),
path( path(
@@ -538,8 +537,7 @@ supplier_part_api_urls = [
include([ include([
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(model=SupplierPart),
{'model': SupplierPart},
name='api-supplier-part-metadata', name='api-supplier-part-metadata',
), ),
path('', SupplierPartDetail.as_view(), name='api-supplier-part-detail'), path('', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
@@ -574,8 +572,7 @@ company_api_urls = [
include([ include([
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(model=Company),
{'model': Company},
name='api-company-metadata', name='api-company-metadata',
), ),
path('', CompanyDetail.as_view(), name='api-company-detail'), path('', CompanyDetail.as_view(), name='api-company-detail'),
@@ -589,8 +586,7 @@ company_api_urls = [
include([ include([
path( path(
'metadata/', 'metadata/',
MetadataView.as_view(), MetadataView.as_view(model=Contact),
{'model': Contact},
name='api-contact-metadata', name='api-contact-metadata',
), ),
path('', ContactDetail.as_view(), name='api-contact-detail'), path('', ContactDetail.as_view(), name='api-contact-detail'),
+7 -3
View File
@@ -2,10 +2,10 @@
from django.urls import reverse from django.urls import reverse
from company.models import Address, Company, Contact, ManufacturerPart, SupplierPart
from InvenTree.unit_test import InvenTreeAPITestCase from InvenTree.unit_test import InvenTreeAPITestCase
from part.models import Part from part.models import Part
from users.permissions import check_user_permission
from .models import Address, Company, Contact, ManufacturerPart, SupplierPart
class CompanyTest(InvenTreeAPITestCase): class CompanyTest(InvenTreeAPITestCase):
@@ -384,7 +384,7 @@ class AddressTest(InvenTreeAPITestCase):
self.assertIn(key, response.data) self.assertIn(key, response.data)
def test_edit(self): def test_edit(self):
"""Test editing an object.""" """Test editing an Address object."""
addr = Address.objects.first() addr = Address.objects.first()
url = reverse('api-address-detail', kwargs={'pk': addr.pk}) url = reverse('api-address-detail', kwargs={'pk': addr.pk})
@@ -392,6 +392,7 @@ class AddressTest(InvenTreeAPITestCase):
self.patch(url, {'title': 'Hello'}, expected_code=403) self.patch(url, {'title': 'Hello'}, expected_code=403)
self.assignRole('purchase_order.change') self.assignRole('purchase_order.change')
self.assertTrue(check_user_permission(self.user, Address, 'change'))
self.patch(url, {'title': 'World'}, expected_code=200) self.patch(url, {'title': 'World'}, expected_code=200)
@@ -407,7 +408,10 @@ class AddressTest(InvenTreeAPITestCase):
self.delete(url, expected_code=403) self.delete(url, expected_code=403)
# Assign role, check permission
self.assertFalse(check_user_permission(self.user, Address, 'delete'))
self.assignRole('purchase_order.delete') self.assignRole('purchase_order.delete')
self.assertTrue(check_user_permission(self.user, Address, 'delete'))
self.delete(url, expected_code=204) self.delete(url, expected_code=204)
@@ -5,6 +5,9 @@
# Secret key for backend # Secret key for backend
# Use the environment variable INVENTREE_SECRET_KEY_FILE # Use the environment variable INVENTREE_SECRET_KEY_FILE
#secret_key_file: '/etc/inventree/secret_key.txt' #secret_key_file: '/etc/inventree/secret_key.txt'
# Use the environment variable INVENTREE_OIDC_PRIVATE_KEY or INVENTREE_OIDC_PRIVATE_KEY_FILE
#oidc_private_key: '-----BEGIN PRIVATE KEY-----aaa'
#oidc_private_key_file: '/etc/inventree/oidc.key'
# Database backend selection - Configure backend database settings # Database backend selection - Configure backend database settings
# Documentation: https://docs.inventree.org/en/latest/start/config/ # Documentation: https://docs.inventree.org/en/latest/start/config/
+7 -6
View File
@@ -5,16 +5,16 @@ import inspect
from django.urls import include, path from django.urls import include, path
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import permissions, serializers from rest_framework import serializers
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
from rest_framework.response import Response from rest_framework.response import Response
import common.models import common.models
import common.serializers import common.serializers
import InvenTree.permissions
from data_exporter.mixins import DataExportViewMixin from data_exporter.mixins import DataExportViewMixin
from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
from InvenTree.permissions import IsStaffOrReadOnly
from InvenTree.serializers import EmptySerializer from InvenTree.serializers import EmptySerializer
from .serializers import GenericStateClassSerializer from .serializers import GenericStateClassSerializer
@@ -36,7 +36,7 @@ class StatusView(GenericAPIView):
all available 'StockStatus' codes all available 'StockStatus' codes
""" """
permission_classes = [permissions.IsAuthenticated] permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = GenericStateClassSerializer serializer_class = GenericStateClassSerializer
# Override status_class for implementing subclass # Override status_class for implementing subclass
@@ -96,9 +96,10 @@ class StatusView(GenericAPIView):
class AllStatusViews(StatusView): class AllStatusViews(StatusView):
"""Endpoint for listing all defined status models.""" """Endpoint for listing all defined status models."""
permission_classes = [permissions.IsAuthenticated] permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = EmptySerializer serializer_class = EmptySerializer
@extend_schema(operation_id='generic_status_retrieve_all')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Perform a GET request to learn information about status codes.""" """Perform a GET request to learn information about status codes."""
from InvenTree.helpers import inheritors from InvenTree.helpers import inheritors
@@ -135,7 +136,7 @@ class CustomStateList(DataExportViewMixin, ListCreateAPI):
queryset = common.models.InvenTreeCustomUserStateModel.objects.all() queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
serializer_class = common.serializers.CustomStateSerializer serializer_class = common.serializers.CustomStateSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
filter_backends = SEARCH_ORDER_FILTER filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['key'] ordering_fields = ['key']
search_fields = ['key', 'name', 'label', 'reference_status'] search_fields = ['key', 'name', 'label', 'reference_status']
@@ -147,7 +148,7 @@ class CustomStateDetail(RetrieveUpdateDestroyAPI):
queryset = common.models.InvenTreeCustomUserStateModel.objects.all() queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
serializer_class = common.serializers.CustomStateSerializer serializer_class = common.serializers.CustomStateSerializer
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
urlpattern = [ urlpattern = [
+19 -1
View File
@@ -8,9 +8,13 @@ from django.db import models
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import ChoiceField from rest_framework.fields import ChoiceField
import InvenTree.ready
from .custom import get_logical_value from .custom import get_logical_value
@@ -73,6 +77,7 @@ class CustomChoiceField(serializers.ChoiceField):
return field_info return field_info
@extend_schema_field(OpenApiTypes.INT)
class ExtraCustomChoiceField(CustomChoiceField): class ExtraCustomChoiceField(CustomChoiceField):
"""Custom Choice Field that returns value of status if empty. """Custom Choice Field that returns value of status if empty.
@@ -138,14 +143,27 @@ class InvenTreeCustomStatusModelField(models.PositiveIntegerField):
if self.status_class: if self.status_class:
validators.append(CustomStatusCodeValidator(status_class=self.status_class)) validators.append(CustomStatusCodeValidator(status_class=self.status_class))
help_text = _('Additional status information for this item')
if InvenTree.ready.isGeneratingSchema():
help_text = (
help_text
+ '\n\n'
+ '\n'.join(
f'* `{value}` - {label}'
for value, label in self.status_class.items(custom=True)
)
+ "\n\nAdditional custom status keys may be retrieved from the corresponding 'status_retrieve' call."
)
custom_key_field = ExtraInvenTreeCustomStatusModelField( custom_key_field = ExtraInvenTreeCustomStatusModelField(
default=None, default=None,
verbose_name=_('Custom status key'), verbose_name=_('Custom status key'),
help_text=_('Additional status information for this item'), help_text=help_text,
validators=validators, validators=validators,
blank=True, blank=True,
null=True, null=True,
) )
cls.add_to_class(f'{name}_custom_key', custom_key_field) cls.add_to_class(f'{name}_custom_key', custom_key_field)
self._custom_key_field = custom_key_field self._custom_key_field = custom_key_field
+20 -6
View File
@@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
from django.urls import include, path from django.urls import include, path
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework import permissions from rest_framework import permissions, serializers
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@@ -12,6 +12,7 @@ from rest_framework.views import APIView
import importer.models import importer.models
import importer.registry import importer.registry
import importer.serializers import importer.serializers
import InvenTree.permissions
from InvenTree.api import BulkDeleteMixin from InvenTree.api import BulkDeleteMixin
from InvenTree.filters import SEARCH_ORDER_FILTER from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.mixins import ( from InvenTree.mixins import (
@@ -21,14 +22,14 @@ from InvenTree.mixins import (
RetrieveUpdateAPI, RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI, RetrieveUpdateDestroyAPI,
) )
from users.models import check_user_permission from users.permissions import check_user_permission
class DataImporterPermission(permissions.BasePermission): class DataImporterPermission(permissions.BasePermission):
"""Mixin class for determining if the user has correct permissions.""" """Mixin class for determining if the user has correct permissions."""
def has_permission(self, request, view): def has_permission(self, request, view):
"""Class level permission checks are handled via permissions.IsAuthenticated.""" """Class level permission checks are handled via InvenTree.permissions.IsAuthenticatedOrReadScope."""
return True return True
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
@@ -53,13 +54,25 @@ class DataImporterPermissionMixin:
"""Mixin class for checking permissions on DataImporter objects.""" """Mixin class for checking permissions on DataImporter objects."""
# Default permissions: User must be authenticated # Default permissions: User must be authenticated
permission_classes = [permissions.IsAuthenticated, DataImporterPermission] permission_classes = [
InvenTree.permissions.IsAuthenticatedOrReadScope,
DataImporterPermission,
]
class DataImporterModelSerializer(serializers.Serializer):
"""Model references to map info that might get imported."""
serializer = serializers.CharField(read_only=True)
model_type = serializers.CharField(read_only=True)
api_url = serializers.URLField(read_only=True)
class DataImporterModelList(APIView): class DataImporterModelList(APIView):
"""API endpoint for displaying a list of models available for import.""" """API endpoint for displaying a list of models available for import."""
permission_classes = [permissions.IsAuthenticated] permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = DataImporterModelSerializer(many=True)
def get(self, request): def get(self, request):
"""Return a list of models available for import.""" """Return a list of models available for import."""
@@ -101,7 +114,8 @@ class DataImportSessionDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
class DataImportSessionAcceptFields(APIView): class DataImportSessionAcceptFields(APIView):
"""API endpoint to accept the field mapping for a DataImportSession.""" """API endpoint to accept the field mapping for a DataImportSession."""
permission_classes = [permissions.IsAuthenticated] permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
serializer_class = None
@extend_schema( @extend_schema(
responses={200: importer.serializers.DataImportSessionSerializer(many=False)} responses={200: importer.serializers.DataImportSessionSerializer(many=False)}
@@ -0,0 +1,24 @@
# Generated by Django 4.2.20 on 2025-04-07 20:53
from django.db import migrations, models
import importer.validators
class Migration(migrations.Migration):
dependencies = [
("importer", "0003_dataimportsession_field_filters"),
]
operations = [
migrations.AlterField(
model_name="dataimportsession",
name="model_type",
field=models.CharField(
help_text="Target model type for this import session",
max_length=100,
validators=[importer.validators.validate_importer_model_type],
verbose_name="Model Type",
),
),
]
+8
View File
@@ -18,6 +18,7 @@ import importer.registry
import importer.tasks import importer.tasks
import importer.validators import importer.validators
import InvenTree.helpers import InvenTree.helpers
from common.models import RenderChoices
from importer.status_codes import DataImportStatusCode from importer.status_codes import DataImportStatusCode
logger = structlog.get_logger('inventree') logger = structlog.get_logger('inventree')
@@ -38,6 +39,11 @@ class DataImportSession(models.Model):
field_filters: JSONField for field filter values - optional field API filters field_filters: JSONField for field filter values - optional field API filters
""" """
class ModelChoices(RenderChoices):
"""Model choices for data import sessions."""
choice_fnc = importer.registry.supported_models
@staticmethod @staticmethod
def get_api_url(): def get_api_url():
"""Return the API URL associated with the DataImportSession model.""" """Return the API URL associated with the DataImportSession model."""
@@ -77,6 +83,8 @@ class DataImportSession(models.Model):
blank=False, blank=False,
max_length=100, max_length=100,
validators=[importer.validators.validate_importer_model_type], validators=[importer.validators.validate_importer_model_type],
verbose_name=_('Model Type'),
help_text=_('Target model type for this import session'),
) )
status = models.PositiveIntegerField( status = models.PositiveIntegerField(
+1 -2
View File
@@ -72,9 +72,8 @@ def extract_column_names(data_file) -> list:
headers = [] headers = []
for idx, header in enumerate(data.headers): for idx, header in enumerate(data.headers):
header = header.strip()
if header: if header:
header = str(header).strip()
headers.append(header) headers.append(header)
else: else:
# If the header is empty, generate a default header # If the header is empty, generate a default header
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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