Merge branch 'pui-plugins' of github.com:SchrodingersGat/InvenTree into pui-plugins

This commit is contained in:
Oliver Walters 2024-08-13 06:32:53 +00:00
commit 3c7fcee391
90 changed files with 39473 additions and 37843 deletions

View File

@ -130,7 +130,7 @@ jobs:
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # pin@v3.6.1
- name: Set up cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # pin@v3.5.0
uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # pin@v3.6.0
- name: Check if Dockerhub login is required
id: docker_login
run: |
@ -166,7 +166,7 @@ jobs:
- name: Push Docker Images
id: push-docker
if: github.event_name != 'pull_request'
uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # pin@v6.5.0
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # pin@v6.6.1
with:
context: .
file: ./contrib/container/Dockerfile

View File

@ -159,7 +159,7 @@ jobs:
- name: Export API Documentation
run: invoke schema --ignore-warnings --filename src/backend/InvenTree/schema.yml
- name: Upload schema
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # pin@v4.3.5
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # pin@v4.3.6
with:
name: schema.yml
path: src/backend/InvenTree/schema.yml
@ -535,7 +535,7 @@ jobs:
- name: Run Playwright tests
id: tests
run: cd src/frontend && npx nyc playwright test
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # pin@v4
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # pin@v4
if: ${{ !cancelled() && steps.tests.outcome == 'failure' }}
with:
name: playwright-report
@ -573,7 +573,7 @@ jobs:
run: |
cd src/backend/InvenTree/web/static
zip -r frontend-build.zip web/ web/.vite
- uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # pin@v4.3.5
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # pin@v4.3.6
with:
name: frontend-build
path: src/backend/InvenTree/web/static/web

View File

@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
with:
name: SARIF file
path: results.sarif
@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
with:
sarif_file: results.sarif

View File

@ -1,13 +1,17 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 236
INVENTREE_API_VERSION = 237
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
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
- Adds "supplier_name" to the PurchaseOrder API serializer

View File

@ -4,6 +4,7 @@ import json
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.http.response import HttpResponse
from django.urls import include, path, re_path
@ -706,7 +707,7 @@ class AttachmentFilter(rest_filters.FilterSet):
return queryset.filter(Q(attachment=None) | Q(attachment='')).distinct()
class AttachmentList(ListCreateAPI):
class AttachmentList(BulkDeleteMixin, ListCreateAPI):
"""List API endpoint for Attachment objects."""
queryset = common.models.Attachment.objects.all()
@ -725,6 +726,24 @@ class AttachmentList(ListCreateAPI):
attachment.upload_user = self.request.user
attachment.save()
def validate_delete(self, queryset, request) -> None:
"""Ensure that the user has correct permissions for a bulk-delete.
- Extract all model types from the provided queryset
- 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
model_types = queryset.values_list('model_type', flat=True).distinct()
for model_type in model_types:
if model_class := attachment_model_class_from_label(model_type):
if not check_user_permission(request.user, model_class, 'delete'):
raise ValidationError(
_('User does not have permission to delete these attachments')
)
class AttachmentDetail(RetrieveUpdateDestroyAPI):
"""Detail API endpoint for Attachment objects."""

View File

@ -540,12 +540,17 @@ class AttachmentSerializer(InvenTreeModelSerializer):
allow_null=False,
)
def save(self):
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
model_type = self.validated_data.get('model_type', None)
if model_type is None:
if self.instance:
model_type = self.instance.model_type
# Ensure that the user has permission to attach files to the specified model
user = self.context.get('request').user
@ -556,15 +561,18 @@ class AttachmentSerializer(InvenTreeModelSerializer):
if not issubclass(target_model_class, InvenTreeAttachmentMixin):
raise PermissionDenied(_('Invalid model type specified for attachment'))
permission_error_msg = _(
'User does not have permission to create or edit attachments for this model'
)
if not check_user_permission(user, target_model_class, 'change'):
raise PermissionDenied(permission_error_msg)
# Check that the user has the required permissions to attach files to the target model
if not target_model_class.check_attachment_permission('change', user):
raise PermissionDenied(
_(
'User does not have permission to create or edit attachments for this model'
)
)
raise PermissionDenied(_(permission_error_msg))
return super().save()
return super().save(**kwargs)
class IconSerializer(serializers.Serializer):

View File

@ -157,6 +157,29 @@ class AttachmentTest(InvenTreeAPITestCase):
# Upload should now work!
response = self.post(url, data, expected_code=201)
pk = response.data['pk']
# Edit the attachment via API
response = self.patch(
reverse('api-attachment-detail', kwargs={'pk': pk}),
{'comment': 'New comment'},
expected_code=200,
)
self.assertEqual(response.data['comment'], 'New comment')
attachment = Attachment.objects.get(pk=pk)
self.assertEqual(attachment.comment, 'New comment')
# And check that we cannot edit the attachment without the correct permissions
self.clearRoles()
self.patch(
reverse('api-attachment-detail', kwargs={'pk': pk}),
{'comment': 'New comment 2'},
expected_code=403,
)
# Try to delete the attachment via API (should fail)
attachment = part.attachments.first()
url = reverse('api-attachment-detail', kwargs={'pk': attachment.pk})

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

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

View File

@ -682,6 +682,18 @@ def clear_user_role_cache(user: User):
cache.delete(key)
def check_user_permission(user: User, model, permission):
"""Check if the user has a particular permission against a given model type.
Arguments:
user: The user object to check
model: The model class to check (e.g. Part)
permission: The permission to check (e.g. 'view' / 'delete')
"""
permission_name = f'{model._meta.app_label}.{permission}_{model._meta.model_name}'
return user.has_perm(permission_name)
def check_user_role(user: User, role, permission):
"""Check if a user has a particular role:permission combination.

View File

@ -5,7 +5,7 @@
"packages": {
"": {
"dependencies": {
"eslint": "^9.7.0",
"eslint": "^9.9.0",
"eslint-config-google": "^0.14.0"
}
},
@ -86,9 +86,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.7.0.tgz",
"integrity": "sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==",
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz",
"integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
@ -322,15 +322,15 @@
}
},
"node_modules/eslint": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.7.0.tgz",
"integrity": "sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==",
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz",
"integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.17.0",
"@eslint/config-array": "^0.17.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.7.0",
"@eslint/js": "9.9.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
@ -369,6 +369,14 @@
},
"funding": {
"url": "https://eslint.org/donate"
},
"peerDependencies": {
"jiti": "*"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
}
}
},
"node_modules/eslint-config-google": {

View File

@ -1,6 +1,6 @@
{
"dependencies": {
"eslint": "^9.7.0",
"eslint": "^9.9.0",
"eslint-config-google": "^0.14.0"
},
"type": "module"

View File

@ -18,29 +18,29 @@
"@codemirror/search": ">=6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.30.0",
"@codemirror/view": ">=6.32.0",
"@emotion/react": "^11.13.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@lingui/core": "^4.11.2",
"@lingui/react": "^4.11.2",
"@mantine/carousel": "^7.12.0",
"@mantine/charts": "^7.12.0",
"@mantine/core": "^7.12.0",
"@mantine/dates": "^7.12.0",
"@mantine/dropzone": "^7.12.0",
"@mantine/form": "^7.12.0",
"@mantine/hooks": "^7.12.0",
"@mantine/modals": "^7.12.0",
"@mantine/notifications": "^7.12.0",
"@mantine/spotlight": "^7.12.0",
"@mantine/vanilla-extract": "^7.12.0",
"@mdxeditor/editor": "^3.10.1",
"@sentry/react": "^8.23.0",
"@tabler/icons-react": "^3.11.0",
"@tanstack/react-query": "^5.51.21",
"@lingui/core": "^4.11.3",
"@lingui/react": "^4.11.3",
"@mantine/carousel": "^7.12.1",
"@mantine/charts": "^7.12.1",
"@mantine/core": "^7.12.1",
"@mantine/dates": "^7.12.1",
"@mantine/dropzone": "^7.12.1",
"@mantine/form": "^7.12.1",
"@mantine/hooks": "^7.12.1",
"@mantine/modals": "^7.12.1",
"@mantine/notifications": "^7.12.1",
"@mantine/spotlight": "^7.12.1",
"@mantine/vanilla-extract": "^7.12.1",
"@mdxeditor/editor": "^3.11.0",
"@sentry/react": "^8.25.0",
"@tabler/icons-react": "^3.12.0",
"@tanstack/react-query": "^5.51.23",
"@uiw/codemirror-theme-vscode": "^4.23.0",
"@uiw/react-codemirror": "^4.23.0",
"@uiw/react-split": "^5.9.3",
@ -53,7 +53,7 @@
"fuse.js": "^7.0.0",
"html5-qrcode": "^2.3.8",
"mantine-datatable": "^7.11.3",
"qrcode": "^1.5.3",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-grid-layout": "^1.4.4",
@ -70,10 +70,10 @@
"@babel/core": "^7.25.2",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@lingui/cli": "^4.11.2",
"@lingui/macro": "^4.11.2",
"@playwright/test": "^1.45.3",
"@types/node": "^22.1.0",
"@lingui/cli": "^4.11.3",
"@lingui/macro": "^4.11.3",
"@playwright/test": "^1.46.0",
"@types/node": "^22.2.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
@ -86,7 +86,7 @@
"nyc": "^17.0.0",
"rollup-plugin-license": "^3.5.2",
"typescript": "^5.5.4",
"vite": "^5.3.5",
"vite": "^5.4.0",
"vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-istanbul": "^6.0.2"
}

View File

@ -133,7 +133,11 @@ export function SearchDrawer({
return [
{
model: ModelType.part,
parameters: {},
parameters: {
active: userSettings.isSet('SEARCH_HIDE_INACTIVE_PARTS')
? true
: undefined
},
enabled:
user.hasViewRole(UserRoles.part) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_PARTS')
@ -173,7 +177,10 @@ export function SearchDrawer({
model: ModelType.stockitem,
parameters: {
part_detail: true,
location_detail: true
location_detail: true,
in_stock: userSettings.isSet('SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK')
? true
: undefined
},
enabled:
user.hasViewRole(UserRoles.stock) &&
@ -206,7 +213,12 @@ export function SearchDrawer({
{
model: ModelType.purchaseorder,
parameters: {
supplier_detail: true
supplier_detail: true,
outstanding: userSettings.isSet(
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_PURCHASE_ORDERS'
)
? true
: undefined
},
enabled:
user.hasViewRole(UserRoles.purchase_order) &&
@ -215,7 +227,12 @@ export function SearchDrawer({
{
model: ModelType.salesorder,
parameters: {
customer_detail: true
customer_detail: true,
outstanding: userSettings.isSet(
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_SALES_ORDERS'
)
? true
: undefined
},
enabled:
user.hasViewRole(UserRoles.sales_order) &&
@ -224,7 +241,12 @@ export function SearchDrawer({
{
model: ModelType.returnorder,
parameters: {
customer_detail: true
customer_detail: true,
outstanding: userSettings.isSet(
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_RETURN_ORDERS'
)
? true
: undefined
},
enabled:
user.hasViewRole(UserRoles.return_order) &&
@ -250,7 +272,7 @@ export function SearchDrawer({
let params: any = {
offset: 0,
limit: 10, // TODO: Make this configurable (based on settings)
limit: userSettings.getSetting('SEARCH_PREVIEW_RESULTS', '10'),
search: searchText,
search_regex: searchRegex,
search_whole: searchWhole

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

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