Merge remote-tracking branch 'origin/master' into report_merge
This commit is contained in:
commit
bd57809f13
|
|
@ -34,7 +34,7 @@ RUN apt install -y \
|
|||
yarn nodejs npm
|
||||
|
||||
# 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
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ python3 -m pip install --upgrade pip
|
|||
pip3 install --ignore-installed --upgrade invoke Pillow
|
||||
|
||||
# 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
|
||||
invoke update -s
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@ runs:
|
|||
- name: Install Specific Python Dependencies
|
||||
if: ${{ inputs.pip-dependency }}
|
||||
shell: bash
|
||||
run: uv pip install ${{ inputs.pip-dependency }}
|
||||
run: uv pip install ${PIP_DEPS}
|
||||
env:
|
||||
PIP_DEPS: ${{ inputs.pip-dependency }}
|
||||
|
||||
# NPM installs
|
||||
- name: Install node.js ${{ env.node_version }}
|
||||
|
|
@ -78,8 +80,10 @@ runs:
|
|||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install ${{ inputs.apt-dependency }}
|
||||
sudo apt-get install ${{ inputs.apt-dependency }}
|
||||
sudo apt-get install ${APT_DEPS}
|
||||
sudo apt-get install ${APT_DEPS}
|
||||
env:
|
||||
APT_DEPS: ${{ inputs.apt-dependency }}
|
||||
|
||||
# Invoke commands
|
||||
- name: Install dev requirements
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ updates:
|
|||
- package-ecosystem: npm
|
||||
directories:
|
||||
- /src/frontend
|
||||
- /src/backend
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ jobs:
|
|||
test -f data/config.yaml
|
||||
test -f data/plugins.txt
|
||||
test -f data/secret_key.txt
|
||||
test -f data/oidc.pem
|
||||
- name: Run Unit Tests
|
||||
run: |
|
||||
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> contrib/container/docker.dev.env
|
||||
|
|
|
|||
|
|
@ -193,7 +193,6 @@ jobs:
|
|||
diff -u src/backend/InvenTree/schema.yml api.yaml && echo "no difference in API schema " || exit 2
|
||||
- name: Check schema - including warnings
|
||||
run: invoke dev.schema
|
||||
continue-on-error: true
|
||||
- name: Extract version for publishing
|
||||
id: version
|
||||
if: github.ref == 'refs/heads/master' && needs.paths-filter.outputs.api == 'true'
|
||||
|
|
@ -344,7 +343,7 @@ jobs:
|
|||
- name: Coverage Tests
|
||||
run: invoke dev.test --coverage --translations
|
||||
- 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()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -487,7 +486,7 @@ jobs:
|
|||
- name: Run Tests
|
||||
run: invoke dev.test --migrations --report --coverage --translations
|
||||
- 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()
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -619,7 +618,7 @@ jobs:
|
|||
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
|
||||
- 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'
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
@ -682,7 +681,7 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # pin@v3
|
||||
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # pin@v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
|
|
|||
|
|
@ -67,6 +67,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
|
||||
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ __pycache__/
|
|||
.Python
|
||||
env/
|
||||
inventree-env/
|
||||
.venv/
|
||||
./build/
|
||||
.cache/
|
||||
develop-eggs/
|
||||
|
|
@ -18,7 +19,6 @@ share/
|
|||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
|
@ -57,6 +57,7 @@ data.json
|
|||
|
||||
# Key file
|
||||
secret_key.txt
|
||||
oidc.pem
|
||||
|
||||
# IDE / development files
|
||||
.idea/
|
||||
|
|
|
|||
|
|
@ -40,6 +40,29 @@
|
|||
"django": true,
|
||||
"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",
|
||||
"type": "chrome",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ ENV INVENTREE_BACKEND_DIR="${INVENTREE_HOME}/src/backend"
|
|||
# InvenTree configuration files
|
||||
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
||||
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"
|
||||
|
||||
# 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
|
||||
|
||||
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 yarn config set network-timeout 600000 -g
|
||||
COPY src ${INVENTREE_HOME}/src
|
||||
|
|
@ -152,7 +153,7 @@ RUN pip install --require-hashes -r base_requirements.txt --no-cache
|
|||
# Install nodejs / npm / yarn
|
||||
|
||||
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 yarn config set network-timeout 600000 -g
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ function detect_envs() {
|
|||
export INVENTREE_PLUGINS_ENABLED=true
|
||||
export INVENTREE_PLUGIN_FILE=${CONF_DIR}/plugins.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_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}
|
||||
# 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}
|
||||
# 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
|
||||
sed -i s=debug:\ True=debug:\ False=g ${INVENTREE_CONFIG_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.
|
||||
|
||||
!!! 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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
!!! 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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -95,6 +95,46 @@ 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
|
||||
|
||||
### User Roles
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Pricing Support
|
|||
|
||||
## 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
### 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))?
|
||||
- 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.
|
||||
|
||||
### 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
|
||||
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):
|
||||
"""
|
||||
Use docstrings for everything... pls
|
||||
"""
|
||||
"""Use docstrings for everything."""
|
||||
|
||||
NAME = "SampleActionPlugin"
|
||||
ACTION_NAME = "sample"
|
||||
|
|
|
|||
|
|
@ -4,40 +4,8 @@ title: 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.
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Plugins
|
|||
|
||||
## 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:
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
!!! warning "Namechange"
|
||||
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`. While the old name is still available till `0.8.0` we strongly suggest upgrading your plugins. Deprecation warnings are raised if the old name is used.
|
||||
!!! warning "Name Change"
|
||||
The name of the base class was changed with `0.7.0` from `IntegrationPluginBase` to `InvenTreePlugin`.
|
||||
|
||||
### 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
|
||||
|
||||
!!! 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.
|
||||
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 |
|
||||
| [ScheduleMixin](./plugins/schedule.md) | Schedule periodic tasks |
|
||||
| [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 |
|
||||
| [ValidationMixin](./plugins/validation.md) | Provide custom validation of database models |
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
!!! 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.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: Navigation Mixin
|
|||
## NavigationMixin
|
||||
|
||||
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
|
||||
class MyNavigationPlugin(NavigationMixin, InvenTreePlugin):
|
||||
|
|
|
|||
|
|
@ -217,3 +217,47 @@ We are working to develop and distribute a library of custom InvenTree component
|
|||
### 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.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ from plugin.mixins import ValidationMixin
|
|||
import part.models
|
||||
|
||||
|
||||
class MyValidationMixin(Validationixin, InvenTreePlugin):
|
||||
class MyValidationMixin(ValidationMixin, InvenTreePlugin):
|
||||
"""Custom validation plugin."""
|
||||
|
||||
def validate_model_instance(self, instance, deltas=None):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
#### 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)
|
||||
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)
|
||||
|
||||
#### 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)
|
||||
Information — These are not errors but information messages. They might point out potential issues or just provide information.
|
||||
|
|
|
|||
|
|
@ -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). |
|
||||
|
|
@ -38,6 +38,7 @@ Customize settings for search results:
|
|||
| ---- | ----------- | ------- | ----- |
|
||||
{{ usersetting("SEARCH_WHOLE") }}
|
||||
{{ usersetting("SEARCH_REGEX") }}
|
||||
{{ usersetting("SEARCH_NOTES") }}
|
||||
{{ usersetting("SEARCH_PREVIEW_RESULTS") }}
|
||||
{{ usersetting("SEARCH_PREVIEW_SHOW_PARTS") }}
|
||||
{{ usersetting("SEARCH_HIDE_INACTIVE_PARTS") }}
|
||||
|
|
|
|||
|
|
@ -218,31 +218,35 @@ You can either specify the password directly using `INVENTREE_ADMIN_PASSWORD`, o
|
|||
!!! info "Administrator Account"
|
||||
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"
|
||||
If none of the following options are specified, InvenTree will automatically generate a secret key file (stored in `secret_key.txt`) on first run.
|
||||
!!! 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`) 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 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 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 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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_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
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
### 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"
|
||||
Refer to the [proxy server documentation](./processes.md#proxy-server) for more details
|
||||
|
|
|
|||
27
docs/main.py
27
docs/main.py
|
|
@ -126,17 +126,18 @@ def define_env(env):
|
|||
"""Define custom environment variables for the documentation build process."""
|
||||
|
||||
@env.macro
|
||||
def sourcedir(dirname, branch=None):
|
||||
def sourcedir(dirname: str, branch=None):
|
||||
"""Return a link to a directory within the source code repository.
|
||||
|
||||
Arguments:
|
||||
- dirname: The name of the directory to link to (relative to the top-level directory)
|
||||
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:
|
||||
- A fully qualified URL to the source code directory on GitHub
|
||||
A fully qualified URL to the source code directory on GitHub
|
||||
|
||||
Raises:
|
||||
- FileNotFoundError: If the directory does not exist, or the generated URL is invalid
|
||||
FileNotFoundError: If the directory does not exist, or the generated URL is invalid
|
||||
"""
|
||||
if branch == None:
|
||||
branch = get_build_environment()
|
||||
|
|
@ -169,13 +170,15 @@ def define_env(env):
|
|||
"""Return a link to a file within the source code repository.
|
||||
|
||||
Arguments:
|
||||
- filename: The name of the file to link to (relative to the top-level directory)
|
||||
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:
|
||||
- A fully qualified URL to the source code file on GitHub
|
||||
A fully qualified URL to the source code file on GitHub
|
||||
|
||||
Raises:
|
||||
- FileNotFoundError: If the file does not exist, or the generated URL is invalid
|
||||
FileNotFoundError: If the file does not exist, or the generated URL is invalid
|
||||
"""
|
||||
if branch == None:
|
||||
branch = get_build_environment()
|
||||
|
|
@ -247,9 +250,9 @@ def define_env(env):
|
|||
"""Include a file in the documentation, in a 'collapse' block.
|
||||
|
||||
Arguments:
|
||||
- filename: The name of the file to include (relative to the top-level directory)
|
||||
- title:
|
||||
- fmt:
|
||||
filename: The name of the file to include (relative to the top-level directory)
|
||||
title: The title of the collapse block in the documentation
|
||||
fmt: The format of the included file (e.g., 'python', 'html', etc.)
|
||||
"""
|
||||
here = os.path.dirname(__file__)
|
||||
path = os.path.join(here, '..', filename)
|
||||
|
|
@ -299,7 +302,7 @@ def define_env(env):
|
|||
"""Extract information on a particular global setting.
|
||||
|
||||
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
|
||||
setting = GLOBAL_SETTINGS[key]
|
||||
|
|
@ -311,7 +314,7 @@ def define_env(env):
|
|||
"""Extract information on a particular user setting.
|
||||
|
||||
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
|
||||
setting = USER_SETTINGS[key]
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ nav:
|
|||
- Project:
|
||||
- Governance: project/governance.md
|
||||
- Project Security: project/security.md
|
||||
- Resources: project/resources.md
|
||||
- Security: security.md
|
||||
- Install:
|
||||
- Introduction: start/intro.md
|
||||
|
|
@ -160,6 +161,7 @@ nav:
|
|||
- Single Sign on: settings/SSO.md
|
||||
- Multi Factor Authentication: settings/MFA.md
|
||||
- Email: settings/email.md
|
||||
- Experimental Features: settings/experimental.md
|
||||
- Export Data: settings/export.md
|
||||
- Import Data: settings/import.md
|
||||
- Operations:
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ mkdocs-git-revision-date-localized-plugin>=1.1,<2.0
|
|||
mkdocs-simple-hooks>=0.1,<1.0
|
||||
mkdocs-include-markdown-plugin
|
||||
neoteroi-mkdocs
|
||||
mkdocstrings[python]>=0.25.0,<=0.29.0
|
||||
mkdocstrings[python]>=0.25.0,<=0.29.1
|
||||
|
|
|
|||
|
|
@ -331,9 +331,9 @@ mkdocs-macros-plugin==1.3.7 \
|
|||
--hash=sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59 \
|
||||
--hash=sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material==9.6.9 \
|
||||
--hash=sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1 \
|
||||
--hash=sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c
|
||||
mkdocs-material==9.6.11 \
|
||||
--hash=sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff \
|
||||
--hash=sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263
|
||||
# via -r docs/requirements.in
|
||||
mkdocs-material-extensions==1.3.1 \
|
||||
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
|
||||
|
|
@ -343,9 +343,9 @@ mkdocs-simple-hooks==0.1.5 \
|
|||
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
|
||||
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
|
||||
# via -r docs/requirements.in
|
||||
mkdocstrings[python]==0.29.0 \
|
||||
--hash=sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e \
|
||||
--hash=sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d
|
||||
mkdocstrings[python]==0.29.1 \
|
||||
--hash=sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6 \
|
||||
--hash=sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42
|
||||
# via
|
||||
# -r docs/requirements.in
|
||||
# mkdocstrings-python
|
||||
|
|
@ -497,7 +497,6 @@ typing-extensions==4.12.2 \
|
|||
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
|
||||
# via
|
||||
# anyio
|
||||
# mkdocstrings
|
||||
# mkdocstrings-python
|
||||
# rich
|
||||
urllib3==2.3.0 \
|
||||
|
|
|
|||
|
|
@ -18,11 +18,16 @@ src = ["src/backend/InvenTree"]
|
|||
"__init__.py" = ["D104"]
|
||||
|
||||
[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:
|
||||
# - LOG
|
||||
# - DJ # for Django stuff
|
||||
# - S # for security stuff (bandit)
|
||||
# - D401 - Imperative docstrings
|
||||
|
||||
ignore = [
|
||||
"PLE1205",
|
||||
|
|
@ -51,8 +56,6 @@ ignore = [
|
|||
"N806",
|
||||
# - N812 - lowercase imported as non-lowercase
|
||||
"N812",
|
||||
# - D417 Missing argument descriptions in the docstring
|
||||
"D417",
|
||||
# - RUF032 - decimal-from-float-literal
|
||||
"RUF032",
|
||||
"RUF045",
|
||||
|
|
@ -69,6 +72,7 @@ ignore = [
|
|||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
ignore-var-parameters = true
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
split-on-trailing-comma = false
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"""Main JSON interface views."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -11,24 +10,22 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
import structlog
|
||||
from django_q.models import OrmQ
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework import permissions, serializers
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework import serializers
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
|
||||
import common.models
|
||||
import InvenTree.version
|
||||
import users.models
|
||||
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.mixins import ListCreateAPI
|
||||
from InvenTree.sso import sso_registration_enabled
|
||||
from part.models import Part
|
||||
from plugin.serializers import MetadataSerializer
|
||||
from users.models import ApiToken
|
||||
from users.permissions import check_user_permission
|
||||
|
||||
from .helpers import plugins_info
|
||||
from .helpers_email import is_email_configured
|
||||
|
|
@ -81,8 +78,8 @@ def read_license_file(path: Path) -> list:
|
|||
class LicenseViewSerializer(serializers.Serializer):
|
||||
"""Serializer for license information."""
|
||||
|
||||
backend = serializers.CharField(help_text='Backend licenses texts', read_only=True)
|
||||
frontend = serializers.CharField(
|
||||
backend = serializers.ListField(help_text='Backend licenses texts', read_only=True)
|
||||
frontend = serializers.ListField(
|
||||
help_text='Frontend licenses texts', read_only=True
|
||||
)
|
||||
|
||||
|
|
@ -90,7 +87,7 @@ class LicenseViewSerializer(serializers.Serializer):
|
|||
class LicenseView(APIView):
|
||||
"""Simple JSON endpoint for InvenTree license information."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
|
||||
|
||||
@extend_schema(responses={200: OpenApiResponse(response=LicenseViewSerializer)})
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
|
@ -115,7 +112,7 @@ class VersionViewSerializer(serializers.Serializer):
|
|||
api = serializers.IntegerField()
|
||||
commit_hash = serializers.CharField()
|
||||
commit_date = serializers.CharField()
|
||||
commit_branch = serializers.CharField()
|
||||
commit_branch = serializers.CharField(allow_null=True)
|
||||
python = serializers.CharField()
|
||||
django = serializers.CharField()
|
||||
|
||||
|
|
@ -124,7 +121,6 @@ class VersionViewSerializer(serializers.Serializer):
|
|||
|
||||
doc = serializers.URLField()
|
||||
code = serializers.URLField()
|
||||
credit = serializers.URLField()
|
||||
app = serializers.URLField()
|
||||
bug = serializers.URLField()
|
||||
|
||||
|
|
@ -137,7 +133,7 @@ class VersionViewSerializer(serializers.Serializer):
|
|||
class VersionView(APIView):
|
||||
"""Simple JSON endpoint for InvenTree version information."""
|
||||
|
||||
permission_classes = [permissions.IsAdminUser]
|
||||
permission_classes = [InvenTree.permissions.IsAdminOrAdminScope]
|
||||
|
||||
@extend_schema(responses={200: OpenApiResponse(response=VersionViewSerializer)})
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
|
@ -157,7 +153,6 @@ class VersionView(APIView):
|
|||
'links': {
|
||||
'doc': InvenTree.version.inventreeDocUrl(),
|
||||
'code': InvenTree.version.inventreeGithubUrl(),
|
||||
'credit': InvenTree.version.inventreeCreditsUrl(),
|
||||
'app': InvenTree.version.inventreeAppUrl(),
|
||||
'bug': f'{InvenTree.version.inventreeGithubUrl()}issues',
|
||||
},
|
||||
|
|
@ -168,9 +163,9 @@ class VersionInformationSerializer(serializers.Serializer):
|
|||
"""Serializer for a single version."""
|
||||
|
||||
version = serializers.CharField()
|
||||
date = serializers.CharField()
|
||||
gh = serializers.CharField()
|
||||
text = serializers.CharField()
|
||||
date = serializers.DateField()
|
||||
gh = serializers.CharField(allow_null=True)
|
||||
text = serializers.ListField(child=serializers.CharField())
|
||||
latest = serializers.BooleanField()
|
||||
|
||||
class Meta:
|
||||
|
|
@ -179,23 +174,44 @@ class VersionInformationSerializer(serializers.Serializer):
|
|||
fields = '__all__'
|
||||
|
||||
|
||||
class VersionApiSerializer(serializers.Serializer):
|
||||
"""Serializer for the version api endpoint."""
|
||||
|
||||
VersionInformationSerializer(many=True)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
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):
|
||||
"""Simple JSON endpoint for InvenTree version text."""
|
||||
|
||||
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):
|
||||
"""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):
|
||||
|
|
@ -249,7 +265,7 @@ class InfoView(APIView):
|
|||
Use to confirm that the server is running, etc.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
permission_classes = [InvenTree.permissions.AllowAnyOrReadScope]
|
||||
|
||||
def worker_pending_tasks(self):
|
||||
"""Return the current number of outstanding background tasks."""
|
||||
|
|
@ -332,7 +348,7 @@ class InfoView(APIView):
|
|||
class NotFoundView(APIView):
|
||||
"""Simple JSON view when accessing an invalid API view."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
permission_classes = [InvenTree.permissions.AllowAnyOrReadScope]
|
||||
|
||||
def not_found(self, request):
|
||||
"""Return a 404 error."""
|
||||
|
|
@ -582,6 +598,7 @@ class APISearchViewSerializer(serializers.Serializer):
|
|||
search = serializers.CharField()
|
||||
search_regex = 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)
|
||||
offset = serializers.IntegerField(default=0, required=False)
|
||||
|
||||
|
|
@ -595,7 +612,7 @@ class APISearchView(GenericAPIView):
|
|||
Is much more efficient and simplifies code!
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
|
||||
serializer_class = APISearchViewSerializer
|
||||
|
||||
def get_result_types(self):
|
||||
|
|
@ -643,6 +660,7 @@ class APISearchView(GenericAPIView):
|
|||
'search': '',
|
||||
'search_regex': False,
|
||||
'search_whole': False,
|
||||
'search_notes': False,
|
||||
'limit': 1,
|
||||
'offset': 0,
|
||||
}
|
||||
|
|
@ -681,14 +699,9 @@ class APISearchView(GenericAPIView):
|
|||
|
||||
# Check permissions and update results dict with particular query
|
||||
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:
|
||||
if users.models.RuleSet.check_table_permission(
|
||||
request.user, table, 'view'
|
||||
):
|
||||
if check_user_permission(request.user, model, 'view'):
|
||||
results[key] = view.list(request, *args, **kwargs).data
|
||||
else:
|
||||
results[key] = {
|
||||
|
|
@ -705,37 +718,32 @@ class APISearchView(GenericAPIView):
|
|||
class MetadataView(RetrieveUpdateAPI):
|
||||
"""Generic API endpoint for reading and editing metadata for a model."""
|
||||
|
||||
MODEL_REF = 'model'
|
||||
|
||||
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')
|
||||
model = None # Placeholder for the model class
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, model, lookup_field=None, **initkwargs):
|
||||
"""Override to ensure model specific rendering."""
|
||||
if model is None:
|
||||
raise ValidationError(
|
||||
f"MetadataView called without '{self.MODEL_REF}' parameter"
|
||||
"MetadataView defined without 'model' arg"
|
||||
) # 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):
|
||||
"""Return the 'permission' model associated with this view."""
|
||||
return self.get_model_type()
|
||||
return self.model
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset for this endpoint."""
|
||||
return self.get_model_type().objects.all()
|
||||
return self.model.objects.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""Return MetadataSerializer instance."""
|
||||
# Detect if we are currently generating the OpenAPI schema
|
||||
if 'spectacular' in sys.argv:
|
||||
return MetadataSerializer(Part, *args, **kwargs) # pragma: no cover
|
||||
return MetadataSerializer(self.get_model_type(), *args, **kwargs)
|
||||
return MetadataSerializer(self.model, *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,581 +1,607 @@
|
|||
"""InvenTree API version information."""
|
||||
|
||||
# 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."""
|
||||
|
||||
|
||||
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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
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 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
|
||||
- Allow customization of data exporting via plugins
|
||||
- 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
|
||||
- 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 PartCategory 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
|
||||
- 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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
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 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
|
||||
|
||||
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
|
||||
- Add allow_null to various fields in Build, Settings, Order, Part, and Stock serializers
|
||||
- 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 '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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
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 SalesOrder API
|
||||
- Adds "start_date" to ReturnOrder API
|
||||
- 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
|
||||
- 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
|
||||
|
||||
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 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 '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 SalesOrder 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 BuildOrder API
|
||||
- Adds new date filters to the SalesOrder API
|
||||
- Adds new date filters to the PurchaseOrder 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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
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
|
||||
- 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" 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
|
||||
- Add "shipment count" information to the SalesOrder API endpoints
|
||||
- Allow null value for SalesOrderAllocation.shipment field
|
||||
- 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")
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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 SalesOrder API endpoint
|
||||
- Adds "include_variants" filter to the PurchaseOrderLineItem API endpoint
|
||||
- Adds "include_variants" filter to the ReturnOrder API endpoint
|
||||
|
||||
268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
|
||||
v268 -> 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
|
||||
- Adds "in_stock" attribute to the StockItem serializer
|
||||
|
||||
267 - 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
|
||||
v267 -> 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
|
||||
- Remove "allocations" field from the SalesOrderShipment API endpoint(s)
|
||||
- Add "allocated_items" field to the SalesOrderShipment API endpoint(s)
|
||||
|
||||
266 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
|
||||
v266 -> 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
|
||||
- Tweak SalesOrderShipment API for more efficient data retrieval
|
||||
|
||||
265 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228
|
||||
v265 -> 2024-10-07 : https://github.com/inventree/InvenTree/pull/8228
|
||||
- Adds API endpoint for providing custom admin integration details for plugins
|
||||
|
||||
264 - 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231
|
||||
v264 -> 2024-10-03 : https://github.com/inventree/InvenTree/pull/8231
|
||||
- Adds Sales Order Shipment attachment model type
|
||||
|
||||
263 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194
|
||||
v263 -> 2024-09-30 : https://github.com/inventree/InvenTree/pull/8194
|
||||
- Adds Sales Order Shipment report
|
||||
|
||||
262 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
|
||||
v262 -> 2024-09-30 : https://github.com/inventree/InvenTree/pull/8220
|
||||
- Tweak permission requirements for uninstalling plugins via API
|
||||
|
||||
261 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
|
||||
v261 -> 2024-09-26 : https://github.com/inventree/InvenTree/pull/8184
|
||||
- Fixes for BuildOrder API serializers
|
||||
|
||||
v260 - 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
|
||||
v260 -> 2024-09-26 : https://github.com/inventree/InvenTree/pull/8190
|
||||
- Adds facility for server-side context data to be passed to client-side plugins
|
||||
|
||||
v259 - 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
|
||||
v259 -> 2024-09-20 : https://github.com/inventree/InvenTree/pull/8137
|
||||
- Implements new API endpoint for enabling custom UI features via plugins
|
||||
|
||||
v258 - 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
|
||||
v258 -> 2024-09-24 : https://github.com/inventree/InvenTree/pull/8163
|
||||
- Enhances the existing PartScheduling API endpoint
|
||||
- Adds a formal DRF serializer to the endpoint
|
||||
|
||||
v257 - 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
|
||||
v257 -> 2024-09-22 : https://github.com/inventree/InvenTree/pull/8150
|
||||
- Adds API endpoint for reporting barcode scan history
|
||||
|
||||
v256 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
|
||||
v256 -> 2024-09-19 : https://github.com/inventree/InvenTree/pull/7704
|
||||
- Adjustments for "stocktake" (stock history) API endpoints
|
||||
|
||||
v255 - 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
|
||||
v255 -> 2024-09-19 : https://github.com/inventree/InvenTree/pull/8145
|
||||
- Enables copying line items when duplicating an order
|
||||
|
||||
v254 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470
|
||||
v254 -> 2024-09-14 : https://github.com/inventree/InvenTree/pull/7470
|
||||
- Implements new API endpoints for enabling custom UI functionality via plugins
|
||||
|
||||
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
|
||||
v253 -> 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
|
||||
- Adjustments for user API endpoints
|
||||
|
||||
v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
|
||||
v252 -> 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
|
||||
- Add endpoint for listing all known units
|
||||
|
||||
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
||||
v251 -> 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- 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 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)
|
||||
|
||||
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
|
||||
- Write-only field to create child builds from the API
|
||||
- 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
|
||||
|
||||
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
|
||||
- Allow ordering of BuildOrder API by "level" attribute
|
||||
- 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
|
||||
|
||||
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 "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 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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
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 "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
|
||||
|
||||
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 purchase orders
|
||||
- Adds new endpoints for managing state of sales 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'
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 PartBriefSerializer
|
||||
- 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 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 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 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)
|
||||
|
||||
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
|
||||
- All "Attachment" operations are now performed at /api/attachment/
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- 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 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 "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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
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
|
||||
- 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 BomItem list endpoint to be sorted by pricing_min and pricing_max values
|
||||
- Allow InternalPrice and SalePrice endpoints to be sorted by quantity
|
||||
- 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
|
||||
|
||||
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
|
||||
- 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)
|
||||
|
||||
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 "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 "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 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 "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
|
||||
|
||||
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 "sublocations" count to StockLocation serializer
|
||||
- Adds "image" field to PartBrief 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 BomItem 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
|
||||
|
||||
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 "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
|
||||
|
||||
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
|
||||
|
||||
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 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 "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" filter to the PartTestTemplate 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
|
||||
|
||||
v170 -> 2024-02-19 : https://github.com/inventree/InvenTree/pull/6514
|
||||
|
|
|
|||
|
|
@ -365,6 +365,52 @@ def get_secret_key():
|
|||
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(
|
||||
env_ref: str, conf_ref: str, log_ref: str, lookup_media: bool = False
|
||||
):
|
||||
|
|
|
|||
|
|
@ -32,14 +32,23 @@ class InvenTreeSearchFilter(filters.SearchFilter):
|
|||
"""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)
|
||||
- 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
|
||||
"""
|
||||
regex = InvenTree.helpers.str2bool(
|
||||
request.query_params.get('search_regex', False)
|
||||
search_notes = InvenTree.helpers.str2bool(
|
||||
request.query_params.get('search_notes', False)
|
||||
)
|
||||
|
||||
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 = []
|
||||
|
||||
if search_fields:
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ def format_money(
|
|||
money (Money): The money object to format
|
||||
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)
|
||||
include_symbol (bool): Whether to include the currency symbol in the formatted output
|
||||
|
||||
Returns:
|
||||
str: The formatted string
|
||||
|
|
|
|||
|
|
@ -32,18 +32,67 @@ from .settings import MEDIA_URL, STATIC_URL
|
|||
|
||||
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
|
||||
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()
|
||||
|
||||
# Ignore empty string
|
||||
if len(reference) == 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"?
|
||||
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
|
||||
# Note: This will result in large values being "clipped"
|
||||
if clip is not None:
|
||||
if ref_int > clip:
|
||||
ref_int = clip
|
||||
elif ref_int < -clip:
|
||||
ref_int = -clip
|
||||
ref_int = do_clip(ref_int, clip, allow_negative)
|
||||
|
||||
if not allow_negative and ref_int < 0:
|
||||
ref_int = abs(ref_int)
|
||||
|
|
@ -437,6 +482,7 @@ def increment_serial_number(serial, part=None):
|
|||
|
||||
Arguments:
|
||||
serial: The serial number which should be incremented
|
||||
part: Optional part object to provide additional context for incrementing the serial number
|
||||
|
||||
Returns:
|
||||
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)
|
||||
expected_quantity: The number of (unique) serial numbers we expect
|
||||
starting_value: Provide a starting value for the sequence (or None)
|
||||
part: Part that should be used as context
|
||||
"""
|
||||
if starting_value is None:
|
||||
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
|
||||
|
||||
import structlog
|
||||
from allauth.account.models import EmailAddress
|
||||
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
|
|
@ -30,15 +31,15 @@ def is_email_configured():
|
|||
|
||||
# Display warning unless in test mode
|
||||
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
|
||||
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
|
||||
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
|
||||
if not settings.DEFAULT_FROM_EMAIL:
|
||||
|
|
@ -56,7 +57,6 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
|
|||
recipients = [recipients]
|
||||
|
||||
import InvenTree.ready
|
||||
import InvenTree.status
|
||||
|
||||
if InvenTree.ready.isImportingData():
|
||||
# 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:
|
||||
# Email is not configured / enabled
|
||||
logger.info('INVE-W7: Email will not be send, no mail server configured')
|
||||
return
|
||||
|
||||
# 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,
|
||||
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:
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ def notify_responsible(
|
|||
sender,
|
||||
content: NotificationBody = InvenTreeNotificationBodies.NewOrder,
|
||||
exclude=None,
|
||||
extra_users: Optional[list] = None,
|
||||
):
|
||||
"""Notify all responsible parties of a change in an instance.
|
||||
|
||||
|
|
@ -276,15 +277,19 @@ def notify_responsible(
|
|||
sender: Sender model reference
|
||||
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
||||
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
|
||||
|
||||
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
|
||||
return
|
||||
|
||||
notify_users(
|
||||
[instance.responsible], instance, sender, content=content, exclude=exclude
|
||||
)
|
||||
users = [instance.responsible]
|
||||
|
||||
if extra_users:
|
||||
users.extend(extra_users)
|
||||
|
||||
notify_users(users, instance, sender, content=content, exclude=exclude)
|
||||
|
||||
|
||||
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 typing import TypeVar
|
||||
|
|
@ -108,3 +108,5 @@ class Command(spectacular.Command):
|
|||
settings.SPECTACULAR_SETTINGS['APPEND_COMPONENTS'] = components
|
||||
|
||||
super().handle(*args, **kwargs)
|
||||
|
||||
return 'done'
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ from rest_framework.utils import model_meta
|
|||
|
||||
import common.models
|
||||
import InvenTree.permissions
|
||||
import users.models
|
||||
from InvenTree.helpers import str2bool
|
||||
from InvenTree.serializers import DependentField
|
||||
from users.permissions import check_user_permission
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
|
@ -107,28 +107,16 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||
self.model = InvenTree.permissions.get_model_for_view(view)
|
||||
|
||||
# Construct the 'table name' from the model
|
||||
app_label = self.model._meta.app_label
|
||||
tbl_label = self.model._meta.model_name
|
||||
|
||||
metadata['model'] = tbl_label
|
||||
|
||||
table = f'{app_label}_{tbl_label}'
|
||||
|
||||
actions = metadata.get('actions', None)
|
||||
|
||||
if actions is None:
|
||||
actions = {}
|
||||
|
||||
check = users.models.RuleSet.check_table_permission
|
||||
|
||||
# Map the request method to a permission type
|
||||
rolemap = {
|
||||
'GET': 'view',
|
||||
'POST': 'add',
|
||||
'PUT': 'change',
|
||||
'PATCH': 'change',
|
||||
'DELETE': 'delete',
|
||||
}
|
||||
rolemap = {**InvenTree.permissions.ACTION_MAP, 'OPTIONS': 'view'}
|
||||
|
||||
# let the view define a custom rolemap
|
||||
if hasattr(view, 'rolemap'):
|
||||
|
|
@ -136,13 +124,15 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||
|
||||
# Remove any HTTP methods that the user does not have permission for
|
||||
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:
|
||||
del actions[method]
|
||||
|
||||
# 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'] = {}
|
||||
|
||||
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 not a string, and field_value is a string (this indicates that the model value is translated)
|
||||
|
||||
Arguments:
|
||||
- field_name: The name of the field
|
||||
- field_key: The property key to override
|
||||
- field_value: The value of the field (if available)
|
||||
- model_value: The equivalent value of the model (if available)
|
||||
Args:
|
||||
field_name (str): The name of the field.
|
||||
field_key (str): The property key to override.
|
||||
field_value: The value of the field (if available).
|
||||
model_value: The equivalent value of the model (if available).
|
||||
"""
|
||||
if field_value is None and model_value is not None:
|
||||
return model_value
|
||||
|
|
|
|||
|
|
@ -86,6 +86,11 @@ class AuthRequiredMiddleware:
|
|||
response = self.get_response(request)
|
||||
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?
|
||||
path_func = resolve(request.path).func
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
"""Permission set for InvenTree."""
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
|
|
@ -21,7 +40,126 @@ def get_model_for_view(view):
|
|||
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.
|
||||
|
||||
Each endpoint can have one or more of the following actions:
|
||||
|
|
@ -52,14 +190,7 @@ class RolePermission(permissions.BasePermission):
|
|||
return True
|
||||
|
||||
# Map the request method to a permission type
|
||||
rolemap = {
|
||||
'GET': 'view',
|
||||
'OPTIONS': 'view',
|
||||
'POST': 'add',
|
||||
'PUT': 'change',
|
||||
'PATCH': 'change',
|
||||
'DELETE': 'delete',
|
||||
}
|
||||
rolemap = {**ACTION_MAP, 'OPTIONS': 'view'}
|
||||
|
||||
# let the view define a custom rolemap
|
||||
if hasattr(view, 'rolemap'):
|
||||
|
|
@ -73,7 +204,7 @@ class RolePermission(permissions.BasePermission):
|
|||
if '.' in role:
|
||||
role, permission = role.split('.')
|
||||
|
||||
return users.models.check_user_role(user, role, permission)
|
||||
return users.permissions.check_user_role(user, role, permission)
|
||||
|
||||
try:
|
||||
# Extract the model name associated with this request
|
||||
|
|
@ -82,27 +213,66 @@ class RolePermission(permissions.BasePermission):
|
|||
if model is None:
|
||||
return True
|
||||
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
table = f'{app_label}_{model_name}'
|
||||
except AttributeError:
|
||||
# We will assume that if the serializer class does *not* have a Meta,
|
||||
# then we don't need a permission
|
||||
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."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check if the user is a 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."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
|
|
@ -112,17 +282,59 @@ class IsSuperuserOrReadOnly(permissions.IsAdminUser):
|
|||
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):
|
||||
"""Check if the user is a superuser."""
|
||||
return bool(
|
||||
"""Check if the user is a staff."""
|
||||
return bool(permissions.IsAuthenticated().has_permission(request, view)) and (
|
||||
(request.user and request.user.is_staff)
|
||||
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):
|
||||
"""Mark a view function as being exempt from auth requirements."""
|
||||
|
|
@ -132,3 +344,43 @@ def auth_exempt(view_func):
|
|||
|
||||
wrapped_view.auth_exempt = True
|
||||
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,55 @@
|
|||
"""Schema processing functions for cleaning up generated schema."""
|
||||
|
||||
from itertools import chain
|
||||
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.plumbing import ComponentRegistry
|
||||
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):
|
||||
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
|
||||
|
||||
def is_bulk_delete(self) -> bool:
|
||||
"""Check the class of the current view for the BulkDeleteMixin."""
|
||||
return 'BulkDeleteMixin' in [c.__name__ for c in type(self.view).__mro__]
|
||||
def is_bulk_action(self, ref: str) -> bool:
|
||||
"""Check the class of the current view for the bulk mixins."""
|
||||
return ref in [c.__name__ for c in type(self.view).__mro__]
|
||||
|
||||
def get_operation_id(self) -> str:
|
||||
"""Custom path handling overrides, falling back to default behavior."""
|
||||
result_id = super().get_operation_id()
|
||||
|
||||
# rename bulk deletes to deconflict with single delete operation_id
|
||||
if self.method == 'DELETE' and self.is_bulk_delete():
|
||||
# rename bulk actions to deconflict with single action operation_id
|
||||
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()]
|
||||
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
|
||||
# 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
|
||||
self.method = 'PUT'
|
||||
request_body = self._get_request_body()
|
||||
|
|
@ -75,3 +105,52 @@ def postprocess_required_nullable(result, generator, request, public):
|
|||
schema.pop('required')
|
||||
|
||||
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.money import Money
|
||||
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.exceptions import ValidationError
|
||||
from rest_framework.fields import empty
|
||||
|
|
@ -86,6 +87,7 @@ class InvenTreeMoneySerializer(MoneyField):
|
|||
return amount
|
||||
|
||||
|
||||
@extend_schema_field(serializers.CharField())
|
||||
class InvenTreeCurrencySerializer(serializers.ChoiceField):
|
||||
"""Custom serializers for selecting currency option."""
|
||||
|
||||
|
|
@ -111,6 +113,14 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
|
|||
if 'help_text' not in kwargs:
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,13 +21,20 @@ from django.core.validators import URLValidator
|
|||
from django.http import Http404, HttpResponseGone
|
||||
|
||||
import structlog
|
||||
from corsheaders.defaults import default_headers as default_cors_headers
|
||||
from dotenv import load_dotenv
|
||||
|
||||
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.sentry import default_sentry_dsn, init_sentry
|
||||
from InvenTree.version import checkMinPythonVersion, inventreeApiVersion
|
||||
from users.oauth2_scopes import oauth2_scopes
|
||||
|
||||
from . import config, locales
|
||||
|
||||
|
|
@ -298,6 +305,7 @@ INSTALLED_APPS = [
|
|||
'django_otp', # OTP is needed for MFA - base package
|
||||
'django_otp.plugins.otp_totp', # Time based OTP
|
||||
'django_otp.plugins.otp_static', # Backup codes
|
||||
'oauth2_provider', # OAuth2 provider and API access
|
||||
'drf_spectacular', # API documentation
|
||||
'django_ical', # For exporting calendars
|
||||
]
|
||||
|
|
@ -321,6 +329,7 @@ MIDDLEWARE = CONFIG.get(
|
|||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'InvenTree.middleware.AuthRequiredMiddleware',
|
||||
'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
|
||||
'oauth2_provider.middleware.OAuth2TokenMiddleware', # oauth2_provider
|
||||
'maintenance_mode.middleware.MaintenanceModeMiddleware',
|
||||
'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
|
||||
'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
|
||||
|
|
@ -353,6 +362,7 @@ QUERYCOUNT = {
|
|||
AUTHENTICATION_BACKENDS = CONFIG.get(
|
||||
'authentication_backends',
|
||||
[
|
||||
'oauth2_provider.backends.OAuth2Backend', # OAuth2 provider
|
||||
'django.contrib.auth.backends.RemoteUserBackend', # proxy login
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'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 = {
|
||||
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
|
||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
|
||||
|
|
@ -529,12 +548,14 @@ REST_FRAMEWORK = {
|
|||
'users.authentication.ApiTokenAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'users.authentication.ExtendedOAuth2Authentication',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
'rest_framework.permissions.DjangoModelPermissions',
|
||||
'InvenTree.permissions.RolePermission',
|
||||
'InvenTree.permissions.InvenTreeTokenMatchesOASRequirements',
|
||||
],
|
||||
'DEFAULT_SCHEMA_CLASS': 'InvenTree.schema.ExtendedAutoSchema',
|
||||
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
|
||||
|
|
@ -1203,6 +1224,11 @@ CORS_ALLOWED_ORIGIN_REGEXES = get_setting(
|
|||
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
|
||||
# This allows connection from the frontend development server
|
||||
if DEBUG:
|
||||
|
|
@ -1395,6 +1421,7 @@ FLAGS = {
|
|||
'NEXT_GEN': [
|
||||
{'condition': 'parameter', 'value': 'ngen='}
|
||||
], # Should next-gen features be turned on?
|
||||
'OIDC': [{'condition': 'parameter', 'value': 'oidc='}],
|
||||
}
|
||||
|
||||
# Get custom flags from environment/yaml
|
||||
|
|
@ -1428,7 +1455,22 @@ SPECTACULAR_SETTINGS = {
|
|||
'POSTPROCESSING_HOOKS': [
|
||||
'drf_spectacular.hooks.postprocess_schema_enums',
|
||||
'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:
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ def check_system_health(**kwargs):
|
|||
if not InvenTree.helpers_email.is_email_configured(): # pragma: no cover
|
||||
result = False
|
||||
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 settings.DEBUG:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from InvenTree.api import read_license_file
|
|||
from InvenTree.api_version import INVENTREE_API_VERSION
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase, InvenTreeTestCase
|
||||
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):
|
||||
|
|
@ -142,7 +143,7 @@ class ApiAccessTests(InvenTreeAPITestCase):
|
|||
role_names = roles.keys()
|
||||
|
||||
# By default, no permissions are provided
|
||||
for rule in RuleSet.RULESET_NAMES:
|
||||
for rule in RULESET_NAMES:
|
||||
self.assertIn(rule, role_names)
|
||||
|
||||
if roles[rule] is None:
|
||||
|
|
@ -167,7 +168,7 @@ class ApiAccessTests(InvenTreeAPITestCase):
|
|||
|
||||
roles = response.data['roles']
|
||||
|
||||
for rule in RuleSet.RULESET_NAMES:
|
||||
for rule in RULESET_NAMES:
|
||||
self.assertIn(rule, roles.keys())
|
||||
|
||||
for perm in ['view', 'add', 'change', 'delete']:
|
||||
|
|
@ -297,6 +298,7 @@ class SearchTests(InvenTreeAPITestCase):
|
|||
'stock',
|
||||
'order',
|
||||
'sales_order',
|
||||
'build',
|
||||
]
|
||||
roles = ['build.view', 'part.view']
|
||||
|
||||
|
|
@ -353,6 +355,82 @@ class SearchTests(InvenTreeAPITestCase):
|
|||
self.assertNotIn('stockitem', 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):
|
||||
"""Test that users with insufficient permissions are handled correctly."""
|
||||
# First, remove all roles
|
||||
|
|
@ -437,24 +515,43 @@ class GeneralApiTests(InvenTreeAPITestCase):
|
|||
|
||||
def test_inventree_api_text_fnc(self):
|
||||
"""Test that the inventreeApiText function works expected."""
|
||||
latest_version = f'v{INVENTREE_API_VERSION}'
|
||||
|
||||
# Normal run
|
||||
resp = inventreeApiText()
|
||||
self.assertEqual(len(resp), 10)
|
||||
self.assertIn(latest_version, resp)
|
||||
|
||||
# More responses
|
||||
resp = inventreeApiText(20)
|
||||
self.assertEqual(len(resp), 20)
|
||||
self.assertIn(latest_version, resp)
|
||||
|
||||
# Specific version
|
||||
resp = inventreeApiText(start_version=5)
|
||||
self.assertEqual(list(resp)[0], 'v5')
|
||||
self.assertEqual(list(resp)[-1], 'v14')
|
||||
|
||||
def test_parse_version_text_fnc(self):
|
||||
"""Test that api version text is correctly parsed."""
|
||||
resp = parse_version_text()
|
||||
|
||||
# Check that all texts are parsed
|
||||
self.assertEqual(len(resp), INVENTREE_API_VERSION - 1)
|
||||
latest_version = INVENTREE_API_VERSION
|
||||
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):
|
||||
"""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')
|
||||
|
|
@ -87,7 +87,7 @@ class InvenTreeTaskTests(TestCase):
|
|||
):
|
||||
InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexist')
|
||||
|
||||
def test_task_hearbeat(self):
|
||||
def test_task_heartbeat(self):
|
||||
"""Test the task heartbeat."""
|
||||
InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat)
|
||||
|
||||
|
|
@ -190,26 +190,41 @@ class InvenTreeTaskTests(TestCase):
|
|||
from common.models import NotificationEntry, NotificationMessage
|
||||
|
||||
# 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_entries = NotificationEntry.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
|
||||
# Note: The 'attempt count' is set to 10 to ensure that the task is properly marked as 'failed'
|
||||
Task.objects.create(
|
||||
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,
|
||||
)
|
||||
Task.objects.create(id=n_tasks + 1, **test_data)
|
||||
|
||||
# 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(NotificationMessage.objects.count(), n_messages + 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ def setup_tracing(
|
|||
headers: The headers to send with the traces.
|
||||
resources_input: The resources to send with the traces.
|
||||
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():
|
||||
return
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from pathlib import Path
|
|||
from unittest import mock
|
||||
|
||||
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.http.response import StreamingHttpResponse
|
||||
from django.test import TestCase
|
||||
|
|
@ -25,16 +25,23 @@ from plugin import registry
|
|||
from plugin.models import PluginConfig
|
||||
|
||||
|
||||
def addUserPermission(user, permission):
|
||||
"""Shortcut function for adding a certain permission to a user."""
|
||||
perm = Permission.objects.get(codename=permission)
|
||||
user.user_permissions.add(perm)
|
||||
def addUserPermission(user: User, app_name: str, model_name: str, perm: str) -> None:
|
||||
"""Add a specific permission for the provided user.
|
||||
|
||||
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):
|
||||
"""Shortcut function for adding multiple permissions to a user."""
|
||||
for permission in permissions:
|
||||
addUserPermission(user, permission)
|
||||
# Add the permission to the user
|
||||
user.user_permissions.add(permission)
|
||||
user.save()
|
||||
|
||||
|
||||
def getMigrationFileNames(app):
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from django.views.generic.base import RedirectView
|
|||
|
||||
from allauth.headless.urls import Client, build_urlpatterns
|
||||
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
|
||||
|
||||
import build.api
|
||||
|
|
@ -125,6 +127,8 @@ backendpatterns = [
|
|||
), # Used for (DRF) browsable API auth
|
||||
path('auth/', auth_request), # Used for proxies to check if user is authenticated
|
||||
path('accounts/', include('allauth.urls')),
|
||||
# OAuth2
|
||||
flagged_path('OIDC', 'o/', include(oauth2_urls)),
|
||||
path(
|
||||
'accounts/login/',
|
||||
RedirectView.as_view(url=f'/{settings.FRONTEND_URL_BASE}', permanent=False),
|
||||
|
|
|
|||
|
|
@ -26,13 +26,21 @@ logger = logging.getLogger('inventree')
|
|||
|
||||
# Discover git
|
||||
try:
|
||||
from dulwich.errors import NotGitRepository
|
||||
from dulwich.porcelain import active_branch
|
||||
from dulwich.repo import Repo
|
||||
|
||||
main_repo = Repo(pathlib.Path(__file__).parent.parent.parent.parent.parent)
|
||||
main_commit = main_repo[main_repo.head()]
|
||||
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):
|
||||
logger.warning('INVE-W1: Current branch could not be detected.')
|
||||
main_branch = None
|
||||
|
|
@ -129,11 +137,6 @@ def inventreeAppUrl():
|
|||
return 'https://docs.inventree.org/app/'
|
||||
|
||||
|
||||
def inventreeCreditsUrl():
|
||||
"""Return URL for InvenTree credits site."""
|
||||
return 'https://docs.inventree.org/en/latest/credits/'
|
||||
|
||||
|
||||
def inventreeGithubUrl():
|
||||
"""Return URL for InvenTree github site."""
|
||||
return 'https://github.com/InvenTree/InvenTree/'
|
||||
|
|
@ -172,22 +175,27 @@ def parse_version_text():
|
|||
# Remove first newline on latest version
|
||||
patched_data[0] = patched_data[0].replace('\n', '', 1)
|
||||
|
||||
latest_version = f'v{INVENTREE_API_VERSION}'
|
||||
version_data = {}
|
||||
for version in patched_data:
|
||||
data = version.split('\n')
|
||||
|
||||
version_split = data[0].split(' -> ')
|
||||
version_string = version_split[0].strip()
|
||||
if version_string == '':
|
||||
continue
|
||||
|
||||
version_detail = (
|
||||
version_split[1].split(':', 1) if len(version_split) > 1 else ['']
|
||||
)
|
||||
new_data = {
|
||||
'version': version_split[0].strip(),
|
||||
'version': version_string,
|
||||
'date': version_detail[0].strip(),
|
||||
'gh': version_detail[1].strip() if len(version_detail) > 1 else None,
|
||||
'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
|
||||
|
||||
|
||||
|
|
@ -206,7 +214,7 @@ def inventreeApiText(versions: int = 10, start_version: int = 0):
|
|||
|
||||
# Define the range of versions to return
|
||||
if start_version == 0:
|
||||
start_version = INVENTREE_API_VERSION - versions
|
||||
start_version = INVENTREE_API_VERSION - versions + 1
|
||||
|
||||
return {
|
||||
f'v{a}': version_data.get(f'v{a}', None)
|
||||
|
|
|
|||
|
|
@ -920,8 +920,7 @@ build_api_urls = [
|
|||
include([
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': BuildItem},
|
||||
MetadataView.as_view(model=BuildItem),
|
||||
name='api-build-item-metadata',
|
||||
),
|
||||
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(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': Build},
|
||||
MetadataView.as_view(model=Build),
|
||||
name='api-build-metadata',
|
||||
),
|
||||
path('', BuildDetail.as_view(), name='api-build-detail'),
|
||||
|
|
|
|||
|
|
@ -628,8 +628,13 @@ class Build(
|
|||
self.allocated_stock.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def complete_build(self, user, trim_allocated_stock=False):
|
||||
"""Mark this build as complete."""
|
||||
def complete_build(self, user: User, trim_allocated_stock: bool = False):
|
||||
"""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(
|
||||
self.status,
|
||||
BuildStatus.COMPLETE.value,
|
||||
|
|
@ -681,6 +686,9 @@ class Build(
|
|||
# Notify users that this build has been completed
|
||||
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
|
||||
if self.parent:
|
||||
targets.append(self.parent.issued_by)
|
||||
|
|
@ -817,6 +825,7 @@ class Build(
|
|||
Build,
|
||||
exclude=self.issued_by,
|
||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||
extra_users=self.part.get_subscribers(),
|
||||
)
|
||||
|
||||
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
|
||||
InvenTree.helpers_model.notify_responsible(
|
||||
instance, sender, exclude=instance.issued_by
|
||||
instance,
|
||||
sender,
|
||||
exclude=instance.issued_by,
|
||||
extra_users=instance.part.get_subscribers(),
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@ from InvenTree.serializers import (
|
|||
)
|
||||
from stock.generators import generate_batch_code
|
||||
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 users.serializers import OwnerSerializer, UserSerializer
|
||||
|
||||
|
|
@ -581,11 +585,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||
help_text=_('Location for completed build outputs'),
|
||||
)
|
||||
|
||||
status_custom_key = serializers.ChoiceField(
|
||||
choices=StockStatus.items(custom=True),
|
||||
default=StockStatus.OK.value,
|
||||
label=_('Status'),
|
||||
)
|
||||
status_custom_key = StockStatusCustomSerializer(default=StockStatus.OK.value)
|
||||
|
||||
accept_incomplete_allocation = serializers.BooleanField(
|
||||
default=False,
|
||||
|
|
|
|||
|
|
@ -262,6 +262,8 @@ def notify_overdue_build_order(bo: build_models.Build):
|
|||
if bo.responsible:
|
||||
targets.append(bo.responsible)
|
||||
|
||||
targets.extend(bo.part.get_subscribers())
|
||||
|
||||
name = _('Overdue Build Order')
|
||||
|
||||
context = {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
|||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from error_report.models import Error
|
||||
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.permissions import IsAdminUser
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
|
@ -44,7 +44,15 @@ from InvenTree.mixins import (
|
|||
RetrieveUpdateAPI,
|
||||
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.serializers import NotificationUserSettingSerializer
|
||||
|
||||
|
|
@ -134,7 +142,7 @@ class WebhookView(CsrfExemptMixin, APIView):
|
|||
class CurrencyExchangeView(APIView):
|
||||
"""API endpoint for displaying currency information."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
serializer_class = None
|
||||
|
||||
@extend_schema(responses={200: common.serializers.CurrencyExchangeSerializer})
|
||||
|
|
@ -178,7 +186,7 @@ class CurrencyRefreshView(APIView):
|
|||
User must be a 'staff' user to access this endpoint
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
|
||||
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
|
||||
serializer_class = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
|
@ -208,6 +216,7 @@ class GlobalSettingsList(SettingsList):
|
|||
|
||||
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
|
||||
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||
permission_classes = [IsAuthenticated, GlobalSettingsPermissions]
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Ensure all global settings are created."""
|
||||
|
|
@ -215,23 +224,6 @@ class GlobalSettingsList(SettingsList):
|
|||
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):
|
||||
"""Detail view for an individual "global setting" object.
|
||||
|
||||
|
|
@ -241,6 +233,7 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
|
|||
lookup_field = 'key'
|
||||
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
|
||||
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||
permission_classes = [IsAuthenticated, GlobalSettingsPermissions]
|
||||
|
||||
def get_object(self):
|
||||
"""Attempt to find a global setting object with the provided key."""
|
||||
|
|
@ -253,14 +246,13 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
|
|||
key, cache=False, create=True
|
||||
)
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated, GlobalSettingsPermissions]
|
||||
|
||||
|
||||
class UserSettingsList(SettingsList):
|
||||
"""API endpoint for accessing a list of user settings objects."""
|
||||
|
||||
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||
serializer_class = common.serializers.UserSettingsSerializer
|
||||
permission_classes = [UserSettingsPermissionsOrScope]
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""Ensure all user settings are created."""
|
||||
|
|
@ -288,19 +280,6 @@ class UserSettingsList(SettingsList):
|
|||
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):
|
||||
"""Detail view for an individual "user setting" object.
|
||||
|
||||
|
|
@ -310,6 +289,7 @@ class UserSettingsDetail(RetrieveUpdateAPI):
|
|||
lookup_field = 'key'
|
||||
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||
serializer_class = common.serializers.UserSettingsSerializer
|
||||
permission_classes = [UserSettingsPermissionsOrScope]
|
||||
|
||||
def get_object(self):
|
||||
"""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
|
||||
)
|
||||
|
||||
permission_classes = [UserSettingsPermissions]
|
||||
|
||||
|
||||
class NotificationUserSettingsList(SettingsList):
|
||||
"""API endpoint for accessing a list of notification user settings objects."""
|
||||
|
||||
queryset = NotificationUserSetting.objects.all()
|
||||
serializer_class = NotificationUserSettingSerializer
|
||||
permission_classes = [UserSettingsPermissionsOrScope]
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""Only list settings which apply to the current user."""
|
||||
|
|
@ -354,7 +333,7 @@ class NotificationUserSettingsDetail(RetrieveUpdateAPI):
|
|||
|
||||
queryset = NotificationUserSetting.objects.all()
|
||||
serializer_class = NotificationUserSettingSerializer
|
||||
permission_classes = [UserSettingsPermissions]
|
||||
permission_classes = [UserSettingsPermissionsOrScope]
|
||||
|
||||
|
||||
class NotificationMessageMixin:
|
||||
|
|
@ -362,7 +341,7 @@ class NotificationMessageMixin:
|
|||
|
||||
queryset = common.models.NotificationMessage.objects.all()
|
||||
serializer_class = common.serializers.NotificationMessageSerializer
|
||||
permission_classes = [UserSettingsPermissions]
|
||||
permission_classes = [UserSettingsPermissionsOrScope]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return prefetched queryset."""
|
||||
|
|
@ -384,7 +363,7 @@ class NotificationMessageMixin:
|
|||
class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
|
||||
"""List view for all notifications of the current user."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
|
|
@ -437,7 +416,7 @@ class NewsFeedMixin:
|
|||
|
||||
queryset = common.models.NewsFeedEntry.objects.all()
|
||||
serializer_class = common.serializers.NewsFeedEntrySerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
permission_classes = [IsAdminOrAdminScope]
|
||||
|
||||
|
||||
class NewsFeedEntryList(NewsFeedMixin, BulkDeleteMixin, ListAPI):
|
||||
|
|
@ -461,7 +440,7 @@ class ConfigList(ListAPI):
|
|||
|
||||
queryset = CONFIG_LOOKUPS
|
||||
serializer_class = common.serializers.ConfigSerializer
|
||||
permission_classes = [IsSuperuser]
|
||||
permission_classes = [IsSuperuserOrSuperScope]
|
||||
|
||||
# Specifically disable pagination for this view
|
||||
pagination_class = None
|
||||
|
|
@ -471,7 +450,7 @@ class ConfigDetail(RetrieveAPI):
|
|||
"""Detail view for an individual configuration."""
|
||||
|
||||
serializer_class = common.serializers.ConfigSerializer
|
||||
permission_classes = [IsSuperuser]
|
||||
permission_classes = [IsSuperuserOrSuperScope]
|
||||
|
||||
def get_object(self):
|
||||
"""Attempt to find a config object with the provided key."""
|
||||
|
|
@ -487,7 +466,7 @@ class NotesImageList(ListCreateAPI):
|
|||
|
||||
queryset = common.models.NotesImage.objects.all()
|
||||
serializer_class = common.serializers.NotesImageSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
|
|
@ -505,7 +484,7 @@ class ProjectCodeList(DataExportViewMixin, ListCreateAPI):
|
|||
|
||||
queryset = common.models.ProjectCode.objects.all()
|
||||
serializer_class = common.serializers.ProjectCodeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
permission_classes = [IsStaffOrReadOnlyScope]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
ordering_fields = ['code']
|
||||
|
|
@ -518,7 +497,7 @@ class ProjectCodeDetail(RetrieveUpdateDestroyAPI):
|
|||
|
||||
queryset = common.models.ProjectCode.objects.all()
|
||||
serializer_class = common.serializers.ProjectCodeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
permission_classes = [IsStaffOrReadOnlyScope]
|
||||
|
||||
|
||||
class CustomUnitList(DataExportViewMixin, ListCreateAPI):
|
||||
|
|
@ -526,7 +505,7 @@ class CustomUnitList(DataExportViewMixin, ListCreateAPI):
|
|||
|
||||
queryset = common.models.CustomUnit.objects.all()
|
||||
serializer_class = common.serializers.CustomUnitSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
permission_classes = [IsStaffOrReadOnlyScope]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
|
||||
|
|
@ -535,14 +514,14 @@ class CustomUnitDetail(RetrieveUpdateDestroyAPI):
|
|||
|
||||
queryset = common.models.CustomUnit.objects.all()
|
||||
serializer_class = common.serializers.CustomUnitSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
permission_classes = [IsStaffOrReadOnlyScope]
|
||||
|
||||
|
||||
class AllUnitList(ListAPI):
|
||||
class AllUnitList(RetrieveAPI):
|
||||
"""List of all defined units."""
|
||||
|
||||
serializer_class = common.serializers.AllUnitListResponseSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
permission_classes = [IsStaffOrReadOnlyScope]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return a list of all available units."""
|
||||
|
|
@ -573,7 +552,7 @@ class ErrorMessageList(BulkDeleteMixin, ListAPI):
|
|||
|
||||
queryset = Error.objects.all()
|
||||
serializer_class = common.serializers.ErrorMessageSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
|
||||
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
|
|
@ -589,13 +568,13 @@ class ErrorMessageDetail(RetrieveUpdateDestroyAPI):
|
|||
|
||||
queryset = Error.objects.all()
|
||||
serializer_class = common.serializers.ErrorMessageSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
|
||||
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
|
||||
|
||||
|
||||
class BackgroundTaskOverview(APIView):
|
||||
"""Provides an overview of the background task queue status."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated, IsAdminUser]
|
||||
permission_classes = [IsAuthenticatedOrReadScope, IsAdminUser]
|
||||
serializer_class = None
|
||||
|
||||
def get(self, request, fmt=None):
|
||||
|
|
@ -617,7 +596,7 @@ class BackgroundTaskOverview(APIView):
|
|||
class PendingTaskList(BulkDeleteMixin, ListAPI):
|
||||
"""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()
|
||||
serializer_class = common.serializers.PendingTaskSerializer
|
||||
|
|
@ -626,7 +605,7 @@ class PendingTaskList(BulkDeleteMixin, ListAPI):
|
|||
class ScheduledTaskList(ListAPI):
|
||||
"""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()
|
||||
serializer_class = common.serializers.ScheduledTaskSerializer
|
||||
|
|
@ -646,7 +625,7 @@ class ScheduledTaskList(ListAPI):
|
|||
class FailedTaskList(BulkDeleteMixin, ListAPI):
|
||||
"""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()
|
||||
serializer_class = common.serializers.FailedTaskSerializer
|
||||
|
|
@ -663,14 +642,14 @@ class FlagList(ListAPI):
|
|||
|
||||
queryset = settings.FLAGS
|
||||
serializer_class = common.serializers.FlagSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
permission_classes = [AllowAnyOrReadScope]
|
||||
|
||||
|
||||
class FlagDetail(RetrieveAPI):
|
||||
"""Detail view for an individual feature flag."""
|
||||
|
||||
serializer_class = common.serializers.FlagSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
permission_classes = [AllowAnyOrReadScope]
|
||||
|
||||
def get_object(self):
|
||||
"""Attempt to find a config object with the provided key."""
|
||||
|
|
@ -686,7 +665,7 @@ class ContentTypeList(ListAPI):
|
|||
|
||||
queryset = ContentType.objects.all()
|
||||
serializer_class = common.serializers.ContentTypeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
search_fields = ['app_label', 'model']
|
||||
|
||||
|
|
@ -696,10 +675,9 @@ class ContentTypeDetail(RetrieveAPI):
|
|||
|
||||
queryset = ContentType.objects.all()
|
||||
serializer_class = common.serializers.ContentTypeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
|
||||
@extend_schema(operation_id='contenttype_retrieve_model')
|
||||
class ContentTypeModelDetail(ContentTypeDetail):
|
||||
"""Detail view for a ContentType model."""
|
||||
|
||||
|
|
@ -714,6 +692,11 @@ class ContentTypeModelDetail(ContentTypeDetail):
|
|||
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):
|
||||
"""Filterset for the AttachmentList API endpoint."""
|
||||
|
|
@ -746,7 +729,7 @@ class AttachmentList(BulkDeleteMixin, ListCreateAPI):
|
|||
|
||||
queryset = common.models.Attachment.objects.all()
|
||||
serializer_class = common.serializers.AttachmentSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
filterset_class = AttachmentFilter
|
||||
|
|
@ -767,7 +750,7 @@ class AttachmentList(BulkDeleteMixin, ListCreateAPI):
|
|||
- Ensure that the user has correct 'delete' permissions for each model
|
||||
"""
|
||||
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()
|
||||
|
||||
|
|
@ -784,7 +767,7 @@ class AttachmentDetail(RetrieveUpdateDestroyAPI):
|
|||
|
||||
queryset = common.models.Attachment.objects.all()
|
||||
serializer_class = common.serializers.AttachmentSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""Check user permissions before deleting an attachment."""
|
||||
|
|
@ -803,11 +786,11 @@ class IconList(ListAPI):
|
|||
"""List view for available icon packages."""
|
||||
|
||||
serializer_class = common.serializers.IconPackageSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
permission_classes = [AllowAnyOrReadScope]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return a list of all available icon packages."""
|
||||
return get_icon_packs().values()
|
||||
return list(get_icon_packs().values())
|
||||
|
||||
|
||||
class SelectionListList(ListCreateAPI):
|
||||
|
|
@ -815,7 +798,7 @@ class SelectionListList(ListCreateAPI):
|
|||
|
||||
queryset = common.models.SelectionList.objects.all()
|
||||
serializer_class = common.serializers.SelectionListSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override the queryset method to include entry count."""
|
||||
|
|
@ -827,7 +810,7 @@ class SelectionListDetail(RetrieveUpdateDestroyAPI):
|
|||
|
||||
queryset = common.models.SelectionList.objects.all()
|
||||
serializer_class = common.serializers.SelectionListSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
|
||||
|
||||
class EntryMixin:
|
||||
|
|
@ -835,7 +818,7 @@ class EntryMixin:
|
|||
|
||||
queryset = common.models.SelectionListEntry.objects.all()
|
||||
serializer_class = common.serializers.SelectionEntrySerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticatedOrReadScope]
|
||||
lookup_url_kwarg = 'entrypk'
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
@ -854,22 +837,22 @@ class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI):
|
|||
"""Detail view for a SelectionEntry object."""
|
||||
|
||||
|
||||
class DataOutputEndpoint:
|
||||
class DataOutputEndpointMixin:
|
||||
"""Mixin class for DataOutput endpoints."""
|
||||
|
||||
queryset = common.models.DataOutput.objects.all()
|
||||
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."""
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
ordering_fields = ['pk', 'user', 'plugin', 'output_type', 'created']
|
||||
|
||||
|
||||
class DataOutputDetail(DataOutputEndpoint, RetrieveAPI):
|
||||
class DataOutputDetail(DataOutputEndpointMixin, RetrieveAPI):
|
||||
"""Detail view for a DataOutput object."""
|
||||
|
||||
|
||||
|
|
@ -982,8 +965,7 @@ common_api_urls = [
|
|||
include([
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': common.models.Attachment},
|
||||
MetadataView.as_view(model=common.models.Attachment),
|
||||
name='api-attachment-metadata',
|
||||
),
|
||||
path('', AttachmentDetail.as_view(), name='api-attachment-detail'),
|
||||
|
|
@ -1008,8 +990,10 @@ common_api_urls = [
|
|||
include([
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': common.models.ProjectCode},
|
||||
MetadataView.as_view(
|
||||
model=common.models.ProjectCode,
|
||||
permission_classes=[IsStaffOrReadOnlyScope],
|
||||
),
|
||||
name='api-project-code-metadata',
|
||||
),
|
||||
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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -40,15 +40,11 @@ from djmoney.contrib.exchange.models import convert_money
|
|||
from rest_framework.exceptions import PermissionDenied
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
import common.currency
|
||||
import common.validators
|
||||
import InvenTree.exceptions
|
||||
import InvenTree.fields
|
||||
import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
import InvenTree.ready
|
||||
import InvenTree.tasks
|
||||
import InvenTree.validators
|
||||
import users.models
|
||||
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
|
||||
from generic.states import ColorEnum
|
||||
|
|
@ -59,6 +55,24 @@ from InvenTree.sanitizer import sanitize_svg
|
|||
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):
|
||||
"""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()
|
||||
|
||||
|
||||
def rename_attachment(instance, filename):
|
||||
def rename_attachment(instance, filename: str):
|
||||
"""Callback function to rename an uploaded attachment file.
|
||||
|
||||
Arguments:
|
||||
- instance: The Attachment instance
|
||||
- filename: The original filename of the uploaded file
|
||||
Args:
|
||||
instance (Attachment): The Attachment instance for which the file is being renamed.
|
||||
filename (str): The original filename of the uploaded file.
|
||||
|
||||
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
|
||||
illegal_chars = '\'"\\`~#|!@#$%^&*()[]{}<>?;:+=,'
|
||||
|
|
@ -1811,6 +1825,11 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
|
|||
|
||||
verbose_name = _('Attachment')
|
||||
|
||||
class ModelChoices(RenderChoices):
|
||||
"""Model choices for attachments."""
|
||||
|
||||
choice_fnc = common.validators.attachment_model_options
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom 'save' method for the Attachment model.
|
||||
|
||||
|
|
@ -1859,7 +1878,8 @@ class Attachment(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeModel
|
|||
model_type = models.CharField(
|
||||
max_length=100,
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from typing import Optional
|
|||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db.models import Model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import structlog
|
||||
|
|
@ -16,6 +17,7 @@ from InvenTree.ready import isImportingData, isRebuildingData
|
|||
from plugin import registry
|
||||
from plugin.models import NotificationUserSetting, PluginConfig
|
||||
from users.models import Owner
|
||||
from users.permissions import check_user_permission
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
|
@ -31,7 +33,7 @@ class NotificationMethod:
|
|||
GLOBAL_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.
|
||||
|
||||
This checks that:
|
||||
|
|
@ -357,8 +359,21 @@ class InvenTreeNotificationBodies:
|
|||
)
|
||||
|
||||
|
||||
def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
||||
"""Send out a notification."""
|
||||
def trigger_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')
|
||||
target_fnc = kwargs.get('target_fnc')
|
||||
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')
|
||||
check_recent = kwargs.get('check_recent', True)
|
||||
|
||||
# Check if data is importing currently
|
||||
if isImportingData() or isRebuildingData():
|
||||
return
|
||||
|
||||
# Resolve object reference
|
||||
refs = [obj_ref, 'pk', 'id', 'uid']
|
||||
|
||||
|
|
@ -441,26 +452,40 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
|||
)
|
||||
|
||||
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 delivery_methods is None:
|
||||
delivery_methods = storage.methods or []
|
||||
else:
|
||||
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
|
||||
if len(valid_users) > 0:
|
||||
logger.info(
|
||||
"Sending notification '%s' for '%s' to %s users",
|
||||
category,
|
||||
str(obj),
|
||||
len(valid_users),
|
||||
)
|
||||
|
||||
for method in delivery_methods:
|
||||
logger.info("Triggering notification method '%s'", method.METHOD_NAME)
|
||||
try:
|
||||
deliver_notification(method, obj, category, target_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)
|
||||
# Collect possible methods
|
||||
if delivery_methods is None:
|
||||
delivery_methods = storage.methods or []
|
||||
else:
|
||||
delivery_methods = delivery_methods - IGNORED_NOTIFICATION_CLS
|
||||
|
||||
# Set delivery flag
|
||||
common.models.NotificationEntry.notify(category, obj_ref_value)
|
||||
for method in delivery_methods:
|
||||
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:
|
||||
logger.info("No possible users for notification '%s'", category)
|
||||
|
||||
|
|
@ -484,12 +509,18 @@ def trigger_superuser_notification(plugin: PluginConfig, msg: str):
|
|||
|
||||
|
||||
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.
|
||||
|
||||
This:
|
||||
- Intis the method
|
||||
Arguments:
|
||||
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
|
||||
- Runs the delivery setup
|
||||
- Sends notifications either via `send_bulk` or send`
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ class AllUnitListResponseSerializer(serializers.Serializer):
|
|||
|
||||
default_system = serializers.CharField()
|
||||
available_systems = serializers.ListField(child=serializers.CharField())
|
||||
available_units = Unit(many=True)
|
||||
available_units = serializers.DictField(child=Unit())
|
||||
|
||||
|
||||
class ErrorMessageSerializer(InvenTreeModelSerializer):
|
||||
|
|
@ -619,7 +619,7 @@ class AttachmentSerializer(InvenTreeModelSerializer):
|
|||
def save(self, **kwargs):
|
||||
"""Override the save method to handle the model_type field."""
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -165,6 +165,14 @@ USER_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||
'default': False,
|
||||
'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': {
|
||||
'name': _('Show Quantity in Forms'),
|
||||
'description': _('Display available part quantity in some forms'),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from InvenTree.unit_test import (
|
|||
InvenTreeAPITestCase,
|
||||
InvenTreeTestCase,
|
||||
PluginMixin,
|
||||
addUserPermission,
|
||||
)
|
||||
from part.models import Part, PartParameterTemplate
|
||||
from plugin import registry
|
||||
|
|
@ -1077,6 +1078,9 @@ class NotificationTest(InvenTreeAPITestCase):
|
|||
"""Tests for bulk deletion of user notifications."""
|
||||
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
|
||||
for _ii in range(10):
|
||||
Error.objects.create()
|
||||
|
|
@ -1088,7 +1092,7 @@ class NotificationTest(InvenTreeAPITestCase):
|
|||
# However, one user is marked as inactive
|
||||
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)
|
||||
self.assertEqual(my_notifications.count(), 10)
|
||||
|
||||
|
|
@ -1169,7 +1173,7 @@ class CommonTest(InvenTreeAPITestCase):
|
|||
"""Test flag URLs."""
|
||||
# Not superuser
|
||||
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')
|
||||
|
||||
# Turn into superuser
|
||||
|
|
@ -1178,7 +1182,7 @@ class CommonTest(InvenTreeAPITestCase):
|
|||
|
||||
# Successful checks
|
||||
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.assertTrue(response.data[0]['conditions'])
|
||||
|
||||
|
|
|
|||
|
|
@ -516,8 +516,7 @@ manufacturer_part_api_urls = [
|
|||
include([
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': ManufacturerPart},
|
||||
MetadataView.as_view(model=ManufacturerPart),
|
||||
name='api-manufacturer-part-metadata',
|
||||
),
|
||||
path(
|
||||
|
|
@ -538,8 +537,7 @@ supplier_part_api_urls = [
|
|||
include([
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': SupplierPart},
|
||||
MetadataView.as_view(model=SupplierPart),
|
||||
name='api-supplier-part-metadata',
|
||||
),
|
||||
path('', SupplierPartDetail.as_view(), name='api-supplier-part-detail'),
|
||||
|
|
@ -574,8 +572,7 @@ company_api_urls = [
|
|||
include([
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': Company},
|
||||
MetadataView.as_view(model=Company),
|
||||
name='api-company-metadata',
|
||||
),
|
||||
path('', CompanyDetail.as_view(), name='api-company-detail'),
|
||||
|
|
@ -589,8 +586,7 @@ company_api_urls = [
|
|||
include([
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': Contact},
|
||||
MetadataView.as_view(model=Contact),
|
||||
name='api-contact-metadata',
|
||||
),
|
||||
path('', ContactDetail.as_view(), name='api-contact-detail'),
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
from django.urls import reverse
|
||||
|
||||
from company.models import Address, Company, Contact, ManufacturerPart, SupplierPart
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from part.models import Part
|
||||
|
||||
from .models import Address, Company, Contact, ManufacturerPart, SupplierPart
|
||||
from users.permissions import check_user_permission
|
||||
|
||||
|
||||
class CompanyTest(InvenTreeAPITestCase):
|
||||
|
|
@ -384,7 +384,7 @@ class AddressTest(InvenTreeAPITestCase):
|
|||
self.assertIn(key, response.data)
|
||||
|
||||
def test_edit(self):
|
||||
"""Test editing an object."""
|
||||
"""Test editing an Address object."""
|
||||
addr = Address.objects.first()
|
||||
|
||||
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.assignRole('purchase_order.change')
|
||||
self.assertTrue(check_user_permission(self.user, Address, 'change'))
|
||||
|
||||
self.patch(url, {'title': 'World'}, expected_code=200)
|
||||
|
||||
|
|
@ -407,7 +408,10 @@ class AddressTest(InvenTreeAPITestCase):
|
|||
|
||||
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.assertTrue(check_user_permission(self.user, Address, 'delete'))
|
||||
|
||||
self.delete(url, expected_code=204)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
# Secret key for backend
|
||||
# Use the environment variable INVENTREE_SECRET_KEY_FILE
|
||||
#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
|
||||
# Documentation: https://docs.inventree.org/en/latest/start/config/
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@ import inspect
|
|||
from django.urls import include, path
|
||||
|
||||
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.response import Response
|
||||
|
||||
import common.models
|
||||
import common.serializers
|
||||
import InvenTree.permissions
|
||||
from data_exporter.mixins import DataExportViewMixin
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
|
||||
from InvenTree.permissions import IsStaffOrReadOnly
|
||||
from InvenTree.serializers import EmptySerializer
|
||||
|
||||
from .serializers import GenericStateClassSerializer
|
||||
|
|
@ -36,7 +36,7 @@ class StatusView(GenericAPIView):
|
|||
all available 'StockStatus' codes
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
|
||||
serializer_class = GenericStateClassSerializer
|
||||
|
||||
# Override status_class for implementing subclass
|
||||
|
|
@ -96,9 +96,10 @@ class StatusView(GenericAPIView):
|
|||
class AllStatusViews(StatusView):
|
||||
"""Endpoint for listing all defined status models."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
|
||||
serializer_class = EmptySerializer
|
||||
|
||||
@extend_schema(operation_id='generic_status_retrieve_all')
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Perform a GET request to learn information about status codes."""
|
||||
from InvenTree.helpers import inheritors
|
||||
|
|
@ -135,7 +136,7 @@ class CustomStateList(DataExportViewMixin, ListCreateAPI):
|
|||
|
||||
queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
|
||||
serializer_class = common.serializers.CustomStateSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
ordering_fields = ['key']
|
||||
search_fields = ['key', 'name', 'label', 'reference_status']
|
||||
|
|
@ -147,7 +148,7 @@ class CustomStateDetail(RetrieveUpdateDestroyAPI):
|
|||
|
||||
queryset = common.models.InvenTreeCustomUserStateModel.objects.all()
|
||||
serializer_class = common.serializers.CustomStateSerializer
|
||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
||||
permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
|
||||
|
||||
|
||||
urlpattern = [
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@ from django.db import models
|
|||
from django.utils.encoding import force_str
|
||||
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.fields import ChoiceField
|
||||
|
||||
import InvenTree.ready
|
||||
|
||||
from .custom import get_logical_value
|
||||
|
||||
|
||||
|
|
@ -73,6 +77,7 @@ class CustomChoiceField(serializers.ChoiceField):
|
|||
return field_info
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
class ExtraCustomChoiceField(CustomChoiceField):
|
||||
"""Custom Choice Field that returns value of status if empty.
|
||||
|
||||
|
|
@ -138,14 +143,27 @@ class InvenTreeCustomStatusModelField(models.PositiveIntegerField):
|
|||
if 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(
|
||||
default=None,
|
||||
verbose_name=_('Custom status key'),
|
||||
help_text=_('Additional status information for this item'),
|
||||
help_text=help_text,
|
||||
validators=validators,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
cls.add_to_class(f'{name}_custom_key', custom_key_field)
|
||||
self._custom_key_field = custom_key_field
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404
|
|||
from django.urls import include, path
|
||||
|
||||
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.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
|
@ -12,6 +12,7 @@ from rest_framework.views import APIView
|
|||
import importer.models
|
||||
import importer.registry
|
||||
import importer.serializers
|
||||
import InvenTree.permissions
|
||||
from InvenTree.api import BulkDeleteMixin
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import (
|
||||
|
|
@ -21,14 +22,14 @@ from InvenTree.mixins import (
|
|||
RetrieveUpdateAPI,
|
||||
RetrieveUpdateDestroyAPI,
|
||||
)
|
||||
from users.models import check_user_permission
|
||||
from users.permissions import check_user_permission
|
||||
|
||||
|
||||
class DataImporterPermission(permissions.BasePermission):
|
||||
"""Mixin class for determining if the user has correct permissions."""
|
||||
|
||||
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
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
|
|
@ -53,13 +54,25 @@ class DataImporterPermissionMixin:
|
|||
"""Mixin class for checking permissions on DataImporter objects."""
|
||||
|
||||
# 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):
|
||||
"""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):
|
||||
"""Return a list of models available for import."""
|
||||
|
|
@ -101,7 +114,8 @@ class DataImportSessionDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
|
|||
class DataImportSessionAcceptFields(APIView):
|
||||
"""API endpoint to accept the field mapping for a DataImportSession."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
|
||||
serializer_class = None
|
||||
|
||||
@extend_schema(
|
||||
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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -18,6 +18,7 @@ import importer.registry
|
|||
import importer.tasks
|
||||
import importer.validators
|
||||
import InvenTree.helpers
|
||||
from common.models import RenderChoices
|
||||
from importer.status_codes import DataImportStatusCode
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
|
@ -38,6 +39,11 @@ class DataImportSession(models.Model):
|
|||
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
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the DataImportSession model."""
|
||||
|
|
@ -77,6 +83,8 @@ class DataImportSession(models.Model):
|
|||
blank=False,
|
||||
max_length=100,
|
||||
validators=[importer.validators.validate_importer_model_type],
|
||||
verbose_name=_('Model Type'),
|
||||
help_text=_('Target model type for this import session'),
|
||||
)
|
||||
|
||||
status = models.PositiveIntegerField(
|
||||
|
|
|
|||
|
|
@ -72,9 +72,8 @@ def extract_column_names(data_file) -> list:
|
|||
headers = []
|
||||
|
||||
for idx, header in enumerate(data.headers):
|
||||
header = header.strip()
|
||||
|
||||
if header:
|
||||
header = str(header).strip()
|
||||
headers.append(header)
|
||||
else:
|
||||
# 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
Loading…
Reference in New Issue