mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-24 18:32:04 +01:00
* Closes #21263: Prefetch related objects after creating/updating objects via REST API * Add comment re: ordering by PK
192 lines
6.8 KiB
Python
192 lines
6.8 KiB
Python
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.db import router, transaction
|
|
from django.http import Http404
|
|
from rest_framework import status
|
|
from rest_framework.response import Response
|
|
|
|
from core.models import ObjectType
|
|
from extras.models import ExportTemplate
|
|
from netbox.api.serializers import BulkOperationSerializer
|
|
|
|
__all__ = (
|
|
'BulkDestroyModelMixin',
|
|
'BulkUpdateModelMixin',
|
|
'CustomFieldsMixin',
|
|
'ExportTemplatesMixin',
|
|
'ObjectValidationMixin',
|
|
'SequentialBulkCreatesMixin',
|
|
)
|
|
|
|
|
|
class CustomFieldsMixin:
|
|
"""
|
|
For models which support custom fields, populate the `custom_fields` context.
|
|
"""
|
|
def get_serializer_context(self):
|
|
context = super().get_serializer_context()
|
|
|
|
if hasattr(self.queryset.model, 'custom_fields'):
|
|
object_type = ObjectType.objects.get_for_model(self.queryset.model)
|
|
context.update({
|
|
'custom_fields': object_type.custom_fields.all(),
|
|
})
|
|
|
|
return context
|
|
|
|
|
|
class ExportTemplatesMixin:
|
|
"""
|
|
Enable ExportTemplate support for list views.
|
|
"""
|
|
def list(self, request, *args, **kwargs):
|
|
if 'export' in request.GET:
|
|
object_type = ObjectType.objects.get_for_model(self.get_serializer_class().Meta.model)
|
|
et = ExportTemplate.objects.filter(object_types=object_type, name=request.GET['export']).first()
|
|
if et is None:
|
|
raise Http404
|
|
queryset = self.filter_queryset(self.get_queryset())
|
|
return et.render_to_response(queryset=queryset)
|
|
|
|
return super().list(request, *args, **kwargs)
|
|
|
|
|
|
class SequentialBulkCreatesMixin:
|
|
"""
|
|
Perform bulk creation of new objects sequentially, rather than all at once. This ensures that any validation
|
|
which depends on the evaluation of existing objects (such as checking for free space within a rack) functions
|
|
appropriately.
|
|
"""
|
|
def create(self, request, *args, **kwargs):
|
|
with transaction.atomic(using=router.db_for_write(self.queryset.model)):
|
|
if not isinstance(request.data, list):
|
|
# Creating a single object
|
|
return super().create(request, *args, **kwargs)
|
|
|
|
return_data = []
|
|
for data in request.data:
|
|
serializer = self.get_serializer(data=data)
|
|
serializer.is_valid(raise_exception=True)
|
|
self.perform_create(serializer)
|
|
return_data.append(serializer.data)
|
|
|
|
headers = self.get_success_headers(serializer.data)
|
|
|
|
return Response(return_data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class BulkUpdateModelMixin:
|
|
"""
|
|
Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
|
|
or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.
|
|
For example:
|
|
|
|
PATCH /api/dcim/sites/
|
|
[
|
|
{
|
|
"id": 123,
|
|
"name": "New name"
|
|
},
|
|
{
|
|
"id": 456,
|
|
"status": "planned"
|
|
}
|
|
]
|
|
"""
|
|
def get_bulk_update_queryset(self):
|
|
return self.get_queryset()
|
|
|
|
def bulk_update(self, request, *args, **kwargs):
|
|
partial = kwargs.pop('partial', False)
|
|
serializer = BulkOperationSerializer(data=request.data, many=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
qs = self.get_bulk_update_queryset().filter(
|
|
pk__in=[o['id'] for o in serializer.data]
|
|
)
|
|
|
|
# Map update data by object ID
|
|
update_data = {
|
|
obj.pop('id'): obj for obj in request.data
|
|
}
|
|
|
|
object_pks = self.perform_bulk_update(qs, update_data, partial=partial)
|
|
|
|
# Prefetch related objects for all updated instances
|
|
qs = self.get_queryset().filter(pk__in=object_pks)
|
|
serializer = self.get_serializer(qs, many=True)
|
|
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
def perform_bulk_update(self, objects, update_data, partial):
|
|
updated_pks = []
|
|
with transaction.atomic(using=router.db_for_write(self.queryset.model)):
|
|
for obj in objects:
|
|
data = update_data.get(obj.id)
|
|
if hasattr(obj, 'snapshot'):
|
|
obj.snapshot()
|
|
serializer = self.get_serializer(obj, data=data, partial=partial)
|
|
serializer.is_valid(raise_exception=True)
|
|
self.perform_update(serializer)
|
|
updated_pks.append(obj.pk)
|
|
|
|
return updated_pks
|
|
|
|
def bulk_partial_update(self, request, *args, **kwargs):
|
|
kwargs['partial'] = True
|
|
return self.bulk_update(request, *args, **kwargs)
|
|
|
|
|
|
class BulkDestroyModelMixin:
|
|
"""
|
|
Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one
|
|
or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:
|
|
|
|
DELETE /api/dcim/sites/
|
|
[
|
|
{"id": 123},
|
|
{"id": 456}
|
|
]
|
|
"""
|
|
def get_bulk_destroy_queryset(self):
|
|
return self.get_queryset()
|
|
|
|
def bulk_destroy(self, request, *args, **kwargs):
|
|
serializer = BulkOperationSerializer(data=request.data, many=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
qs = self.get_bulk_destroy_queryset().filter(
|
|
pk__in=[o['id'] for o in serializer.validated_data]
|
|
)
|
|
|
|
# Compile any changelog messages to be recorded on the objects being deleted
|
|
changelog_messages = {
|
|
o['id']: o.get('changelog_message') for o in serializer.validated_data
|
|
}
|
|
|
|
self.perform_bulk_destroy(qs, changelog_messages)
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
def perform_bulk_destroy(self, objects, changelog_messages=None):
|
|
changelog_messages = changelog_messages or {}
|
|
with transaction.atomic(using=router.db_for_write(self.queryset.model)):
|
|
for obj in objects:
|
|
if hasattr(obj, 'snapshot'):
|
|
obj.snapshot()
|
|
obj._changelog_message = changelog_messages.get(obj.pk)
|
|
self.perform_destroy(obj)
|
|
|
|
|
|
class ObjectValidationMixin:
|
|
|
|
def _validate_objects(self, instance):
|
|
"""
|
|
Check that the provided instance or list of instances are matched by the current queryset. This confirms that
|
|
any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
|
|
"""
|
|
if type(instance) is list:
|
|
# Check that all instances are still included in the view's queryset
|
|
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
|
|
if conforming_count != len(instance):
|
|
raise ObjectDoesNotExist
|
|
elif not self.queryset.filter(pk=instance.pk).exists():
|
|
raise ObjectDoesNotExist
|