mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'origin/master' into pui-plugins
This commit is contained in:
commit
e2afea2344
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
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
|
- Adds "supplier_name" to the PurchaseOrder API serializer
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import json
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.urls import include, path, re_path
|
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()
|
return queryset.filter(Q(attachment=None) | Q(attachment='')).distinct()
|
||||||
|
|
||||||
|
|
||||||
class AttachmentList(ListCreateAPI):
|
class AttachmentList(BulkDeleteMixin, ListCreateAPI):
|
||||||
"""List API endpoint for Attachment objects."""
|
"""List API endpoint for Attachment objects."""
|
||||||
|
|
||||||
queryset = common.models.Attachment.objects.all()
|
queryset = common.models.Attachment.objects.all()
|
||||||
@ -725,6 +726,24 @@ class AttachmentList(ListCreateAPI):
|
|||||||
attachment.upload_user = self.request.user
|
attachment.upload_user = self.request.user
|
||||||
attachment.save()
|
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):
|
class AttachmentDetail(RetrieveUpdateDestroyAPI):
|
||||||
"""Detail API endpoint for Attachment objects."""
|
"""Detail API endpoint for Attachment objects."""
|
||||||
|
@ -540,12 +540,17 @@ class AttachmentSerializer(InvenTreeModelSerializer):
|
|||||||
allow_null=False,
|
allow_null=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self):
|
def save(self, **kwargs):
|
||||||
"""Override the save method to handle the model_type field."""
|
"""Override the save method to handle the model_type field."""
|
||||||
from InvenTree.models import InvenTreeAttachmentMixin
|
from InvenTree.models import InvenTreeAttachmentMixin
|
||||||
|
from users.models import check_user_permission
|
||||||
|
|
||||||
model_type = self.validated_data.get('model_type', None)
|
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
|
# Ensure that the user has permission to attach files to the specified model
|
||||||
user = self.context.get('request').user
|
user = self.context.get('request').user
|
||||||
|
|
||||||
@ -556,15 +561,18 @@ class AttachmentSerializer(InvenTreeModelSerializer):
|
|||||||
if not issubclass(target_model_class, InvenTreeAttachmentMixin):
|
if not issubclass(target_model_class, InvenTreeAttachmentMixin):
|
||||||
raise PermissionDenied(_('Invalid model type specified for attachment'))
|
raise PermissionDenied(_('Invalid model type specified for attachment'))
|
||||||
|
|
||||||
# Check that the user has the required permissions to attach files to the target model
|
permission_error_msg = _(
|
||||||
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'
|
'User does not have permission to create or edit attachments for this model'
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return super().save()
|
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(_(permission_error_msg))
|
||||||
|
|
||||||
|
return super().save(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class IconSerializer(serializers.Serializer):
|
class IconSerializer(serializers.Serializer):
|
||||||
|
@ -157,6 +157,29 @@ class AttachmentTest(InvenTreeAPITestCase):
|
|||||||
# Upload should now work!
|
# Upload should now work!
|
||||||
response = self.post(url, data, expected_code=201)
|
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)
|
# Try to delete the attachment via API (should fail)
|
||||||
attachment = part.attachments.first()
|
attachment = part.attachments.first()
|
||||||
url = reverse('api-attachment-detail', kwargs={'pk': attachment.pk})
|
url = reverse('api-attachment-detail', kwargs={'pk': attachment.pk})
|
||||||
|
@ -682,6 +682,18 @@ def clear_user_role_cache(user: User):
|
|||||||
cache.delete(key)
|
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):
|
def check_user_role(user: User, role, permission):
|
||||||
"""Check if a user has a particular role:permission combination.
|
"""Check if a user has a particular role:permission combination.
|
||||||
|
|
||||||
|
@ -133,7 +133,11 @@ export function SearchDrawer({
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
model: ModelType.part,
|
model: ModelType.part,
|
||||||
parameters: {},
|
parameters: {
|
||||||
|
active: userSettings.isSet('SEARCH_HIDE_INACTIVE_PARTS')
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
enabled:
|
enabled:
|
||||||
user.hasViewRole(UserRoles.part) &&
|
user.hasViewRole(UserRoles.part) &&
|
||||||
userSettings.isSet('SEARCH_PREVIEW_SHOW_PARTS')
|
userSettings.isSet('SEARCH_PREVIEW_SHOW_PARTS')
|
||||||
@ -173,7 +177,10 @@ export function SearchDrawer({
|
|||||||
model: ModelType.stockitem,
|
model: ModelType.stockitem,
|
||||||
parameters: {
|
parameters: {
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
location_detail: true
|
location_detail: true,
|
||||||
|
in_stock: userSettings.isSet('SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK')
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
enabled:
|
enabled:
|
||||||
user.hasViewRole(UserRoles.stock) &&
|
user.hasViewRole(UserRoles.stock) &&
|
||||||
@ -206,7 +213,12 @@ export function SearchDrawer({
|
|||||||
{
|
{
|
||||||
model: ModelType.purchaseorder,
|
model: ModelType.purchaseorder,
|
||||||
parameters: {
|
parameters: {
|
||||||
supplier_detail: true
|
supplier_detail: true,
|
||||||
|
outstanding: userSettings.isSet(
|
||||||
|
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_PURCHASE_ORDERS'
|
||||||
|
)
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
enabled:
|
enabled:
|
||||||
user.hasViewRole(UserRoles.purchase_order) &&
|
user.hasViewRole(UserRoles.purchase_order) &&
|
||||||
@ -215,7 +227,12 @@ export function SearchDrawer({
|
|||||||
{
|
{
|
||||||
model: ModelType.salesorder,
|
model: ModelType.salesorder,
|
||||||
parameters: {
|
parameters: {
|
||||||
customer_detail: true
|
customer_detail: true,
|
||||||
|
outstanding: userSettings.isSet(
|
||||||
|
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_SALES_ORDERS'
|
||||||
|
)
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
enabled:
|
enabled:
|
||||||
user.hasViewRole(UserRoles.sales_order) &&
|
user.hasViewRole(UserRoles.sales_order) &&
|
||||||
@ -224,7 +241,12 @@ export function SearchDrawer({
|
|||||||
{
|
{
|
||||||
model: ModelType.returnorder,
|
model: ModelType.returnorder,
|
||||||
parameters: {
|
parameters: {
|
||||||
customer_detail: true
|
customer_detail: true,
|
||||||
|
outstanding: userSettings.isSet(
|
||||||
|
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_RETURN_ORDERS'
|
||||||
|
)
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
},
|
},
|
||||||
enabled:
|
enabled:
|
||||||
user.hasViewRole(UserRoles.return_order) &&
|
user.hasViewRole(UserRoles.return_order) &&
|
||||||
@ -250,7 +272,7 @@ export function SearchDrawer({
|
|||||||
|
|
||||||
let params: any = {
|
let params: any = {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 10, // TODO: Make this configurable (based on settings)
|
limit: userSettings.getSetting('SEARCH_PREVIEW_RESULTS', '10'),
|
||||||
search: searchText,
|
search: searchText,
|
||||||
search_regex: searchRegex,
|
search_regex: searchRegex,
|
||||||
search_whole: searchWhole
|
search_whole: searchWhole
|
||||||
|
Loading…
Reference in New Issue
Block a user