2020-03-02 16:51:16 -05:00
|
|
|
import logging
|
2017-10-10 16:41:35 -04:00
|
|
|
from collections import OrderedDict
|
2017-05-24 11:33:11 -04:00
|
|
|
|
2018-11-02 15:20:08 -04:00
|
|
|
import pytz
|
2017-03-07 17:17:39 -05:00
|
|
|
from django.conf import settings
|
2017-04-03 14:00:15 -04:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2020-05-21 10:51:40 -04:00
|
|
|
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist, PermissionDenied
|
|
|
|
from django.db import transaction
|
2020-05-29 14:12:24 -04:00
|
|
|
from django.db.models import ManyToManyField, ProtectedError
|
2020-02-21 17:21:04 -05:00
|
|
|
from django.urls import reverse
|
2016-03-01 11:23:03 -05:00
|
|
|
from rest_framework.exceptions import APIException
|
2017-10-10 14:21:20 -04:00
|
|
|
from rest_framework.permissions import BasePermission
|
2019-03-06 12:42:47 -05:00
|
|
|
from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
|
2017-10-10 16:41:35 -04:00
|
|
|
from rest_framework.response import Response
|
2018-08-03 09:43:03 -04:00
|
|
|
from rest_framework.serializers import Field, ModelSerializer, ValidationError
|
2020-05-14 17:44:46 -04:00
|
|
|
from rest_framework.viewsets import ModelViewSet as _ModelViewSet
|
2017-03-07 17:17:39 -05:00
|
|
|
|
2020-05-21 10:51:40 -04:00
|
|
|
from netbox.api import TokenPermissions
|
2019-04-16 18:02:52 -04:00
|
|
|
from .utils import dict_to_filter_params, dynamic_import
|
2018-06-19 14:57:03 -04:00
|
|
|
|
2016-03-01 11:23:03 -05:00
|
|
|
|
|
|
|
class ServiceUnavailable(APIException):
|
|
|
|
status_code = 503
|
|
|
|
default_detail = "Service temporarily unavailable, please try again later."
|
2017-01-27 14:36:13 -05:00
|
|
|
|
|
|
|
|
2019-04-02 11:17:14 -04:00
|
|
|
class SerializerNotFound(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2018-06-19 14:57:03 -04:00
|
|
|
def get_serializer_for_model(model, prefix=''):
|
|
|
|
"""
|
|
|
|
Dynamically resolve and return the appropriate serializer for a model.
|
|
|
|
"""
|
|
|
|
app_name, model_name = model._meta.label.split('.')
|
|
|
|
serializer_name = '{}.api.serializers.{}{}Serializer'.format(
|
|
|
|
app_name, prefix, model_name
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
return dynamic_import(serializer_name)
|
2018-07-16 13:26:26 -04:00
|
|
|
except AttributeError:
|
2019-04-02 11:17:14 -04:00
|
|
|
raise SerializerNotFound(
|
2019-03-22 16:24:53 -04:00
|
|
|
"Could not determine serializer for {}.{} with prefix '{}'".format(app_name, model_name, prefix)
|
|
|
|
)
|
2018-06-19 14:57:03 -04:00
|
|
|
|
|
|
|
|
2020-02-21 17:21:04 -05:00
|
|
|
def is_api_request(request):
|
|
|
|
"""
|
|
|
|
Return True of the request is being made via the REST API.
|
|
|
|
"""
|
|
|
|
api_path = reverse('api-root')
|
|
|
|
return request.path_info.startswith(api_path)
|
|
|
|
|
|
|
|
|
2017-08-02 11:17:57 -04:00
|
|
|
#
|
|
|
|
# Authentication
|
|
|
|
#
|
|
|
|
|
|
|
|
class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
|
|
|
"""
|
|
|
|
Returns True if the user is authenticated or LOGIN_REQUIRED is False.
|
|
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
|
|
if not settings.LOGIN_REQUIRED:
|
|
|
|
return True
|
2018-03-30 10:39:22 -04:00
|
|
|
return request.user.is_authenticated
|
2017-08-02 11:17:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
#
|
2018-04-04 15:39:14 -04:00
|
|
|
# Fields
|
2017-08-02 11:17:57 -04:00
|
|
|
#
|
|
|
|
|
2018-07-30 12:41:20 -04:00
|
|
|
class ChoiceField(Field):
|
2017-02-08 16:00:42 -05:00
|
|
|
"""
|
2020-02-10 15:10:33 -05:00
|
|
|
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Accepts a single value on write.
|
|
|
|
|
|
|
|
:param choices: An iterable of choices in the form (value, key).
|
|
|
|
:param allow_blank: Allow blank values in addition to the listed choices.
|
2017-02-08 16:00:42 -05:00
|
|
|
"""
|
2020-02-10 15:10:33 -05:00
|
|
|
def __init__(self, choices, allow_blank=False, **kwargs):
|
2019-11-07 09:32:25 -05:00
|
|
|
self.choiceset = choices
|
2020-02-10 15:10:33 -05:00
|
|
|
self.allow_blank = allow_blank
|
2017-03-16 16:50:18 -04:00
|
|
|
self._choices = dict()
|
2019-11-07 09:32:25 -05:00
|
|
|
|
|
|
|
# Unpack grouped choices
|
2017-03-16 16:50:18 -04:00
|
|
|
for k, v in choices:
|
|
|
|
if type(v) in [list, tuple]:
|
|
|
|
for k2, v2 in v:
|
|
|
|
self._choices[k2] = v2
|
|
|
|
else:
|
|
|
|
self._choices[k] = v
|
2019-11-07 09:32:25 -05:00
|
|
|
|
2018-11-27 10:52:24 -05:00
|
|
|
super().__init__(**kwargs)
|
2017-02-08 16:00:42 -05:00
|
|
|
|
2020-02-10 15:10:33 -05:00
|
|
|
def validate_empty_values(self, data):
|
|
|
|
# Convert null to an empty string unless allow_null == True
|
|
|
|
if data is None:
|
|
|
|
if self.allow_null:
|
|
|
|
return True, None
|
|
|
|
else:
|
|
|
|
data = ''
|
|
|
|
return super().validate_empty_values(data)
|
|
|
|
|
2017-02-08 16:00:42 -05:00
|
|
|
def to_representation(self, obj):
|
2018-10-26 12:25:11 -04:00
|
|
|
if obj is '':
|
|
|
|
return None
|
2018-11-26 14:02:38 -05:00
|
|
|
data = OrderedDict([
|
|
|
|
('value', obj),
|
|
|
|
('label', self._choices[obj])
|
|
|
|
])
|
2019-11-07 09:32:25 -05:00
|
|
|
|
2019-12-10 12:07:54 -05:00
|
|
|
# TODO: Remove in v2.8
|
2019-11-07 09:32:25 -05:00
|
|
|
# Include legacy numeric ID (where applicable)
|
2019-12-10 12:07:54 -05:00
|
|
|
if hasattr(self.choiceset, 'LEGACY_MAP') and obj in self.choiceset.LEGACY_MAP:
|
2019-11-07 09:32:25 -05:00
|
|
|
data['id'] = self.choiceset.LEGACY_MAP.get(obj)
|
|
|
|
|
2018-11-26 14:02:38 -05:00
|
|
|
return data
|
2017-02-08 16:00:42 -05:00
|
|
|
|
|
|
|
def to_internal_value(self, data):
|
2020-02-10 15:10:33 -05:00
|
|
|
if data is '':
|
|
|
|
if self.allow_blank:
|
|
|
|
return data
|
|
|
|
raise ValidationError("This field may not be blank.")
|
2018-12-11 17:00:20 -05:00
|
|
|
|
2019-08-10 10:04:13 +01:00
|
|
|
# Provide an explicit error message if the request is trying to write a dict or list
|
|
|
|
if isinstance(data, (dict, list)):
|
|
|
|
raise ValidationError('Value must be passed directly (e.g. "foo": 123); do not use a dictionary or list.')
|
2018-12-11 17:00:20 -05:00
|
|
|
|
|
|
|
# Check for string representations of boolean/integer values
|
2018-08-07 13:48:29 -04:00
|
|
|
if hasattr(data, 'lower'):
|
|
|
|
if data.lower() == 'true':
|
2018-12-11 17:00:20 -05:00
|
|
|
data = True
|
|
|
|
elif data.lower() == 'false':
|
|
|
|
data = False
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
data = int(data)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
2019-08-10 10:04:13 +01:00
|
|
|
try:
|
|
|
|
if data in self._choices:
|
|
|
|
return data
|
2019-11-07 09:32:25 -05:00
|
|
|
# Check if data is a legacy numeric ID
|
|
|
|
slug = self.choiceset.id_to_slug(data)
|
|
|
|
if slug is not None:
|
|
|
|
return slug
|
2019-08-10 10:04:13 +01:00
|
|
|
except TypeError: # Input is an unhashable type
|
|
|
|
pass
|
2018-12-11 17:00:20 -05:00
|
|
|
|
2019-08-10 10:04:13 +01:00
|
|
|
raise ValidationError("{} is not a valid choice.".format(data))
|
2017-02-08 16:00:42 -05:00
|
|
|
|
2019-03-22 16:24:53 -04:00
|
|
|
@property
|
|
|
|
def choices(self):
|
|
|
|
return self._choices
|
|
|
|
|
2017-02-08 16:00:42 -05:00
|
|
|
|
2019-03-06 12:42:47 -05:00
|
|
|
class ContentTypeField(RelatedField):
|
2017-04-03 14:00:15 -04:00
|
|
|
"""
|
|
|
|
Represent a ContentType as '<app_label>.<model>'
|
|
|
|
"""
|
2019-03-06 12:42:47 -05:00
|
|
|
default_error_messages = {
|
|
|
|
"does_not_exist": "Invalid content type: {content_type}",
|
|
|
|
"invalid": "Invalid value. Specify a content type as '<app_label>.<model_name>'.",
|
|
|
|
}
|
|
|
|
|
2017-04-03 14:00:15 -04:00
|
|
|
def to_internal_value(self, data):
|
|
|
|
try:
|
2019-03-06 12:42:47 -05:00
|
|
|
app_label, model = data.split('.')
|
2017-04-03 14:00:15 -04:00
|
|
|
return ContentType.objects.get_by_natural_key(app_label=app_label, model=model)
|
2019-03-06 12:42:47 -05:00
|
|
|
except ObjectDoesNotExist:
|
|
|
|
self.fail('does_not_exist', content_type=data)
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
self.fail('invalid')
|
|
|
|
|
|
|
|
def to_representation(self, obj):
|
|
|
|
return "{}.{}".format(obj.app_label, obj.model)
|
2017-04-03 14:00:15 -04:00
|
|
|
|
|
|
|
|
2017-12-19 17:24:14 -05:00
|
|
|
class TimeZoneField(Field):
|
|
|
|
"""
|
|
|
|
Represent a pytz time zone.
|
|
|
|
"""
|
|
|
|
def to_representation(self, obj):
|
|
|
|
return obj.zone if obj else None
|
|
|
|
|
|
|
|
def to_internal_value(self, data):
|
|
|
|
if not data:
|
|
|
|
return ""
|
2018-08-20 16:53:23 -04:00
|
|
|
if data not in pytz.common_timezones:
|
|
|
|
raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data))
|
|
|
|
return pytz.timezone(data)
|
2017-12-19 17:24:14 -05:00
|
|
|
|
|
|
|
|
2018-04-06 12:42:25 -04:00
|
|
|
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
|
|
|
"""
|
|
|
|
Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
|
|
|
|
objects in a ManyToManyField while still allowing a set of primary keys to be written.
|
|
|
|
"""
|
|
|
|
def __init__(self, serializer, **kwargs):
|
|
|
|
self.serializer = serializer
|
|
|
|
self.pk_field = kwargs.pop('pk_field', None)
|
2018-11-27 10:52:24 -05:00
|
|
|
super().__init__(**kwargs)
|
2018-04-06 12:42:25 -04:00
|
|
|
|
|
|
|
def to_representation(self, value):
|
|
|
|
return self.serializer(value, context={'request': self.context['request']}).data
|
|
|
|
|
|
|
|
|
2018-04-04 15:39:14 -04:00
|
|
|
#
|
|
|
|
# Serializers
|
|
|
|
#
|
|
|
|
|
2018-08-03 10:45:53 -04:00
|
|
|
# TODO: We should probably take a fresh look at exactly what we're doing with this. There might be a more elegant
|
|
|
|
# way to enforce model validation on the serializer.
|
2018-04-04 15:39:14 -04:00
|
|
|
class ValidatedModelSerializer(ModelSerializer):
|
|
|
|
"""
|
|
|
|
Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
|
|
|
|
"""
|
|
|
|
def validate(self, data):
|
|
|
|
|
2018-08-03 09:43:03 -04:00
|
|
|
# Remove custom fields data and tags (if any) prior to model validation
|
2018-04-04 15:39:14 -04:00
|
|
|
attrs = data.copy()
|
|
|
|
attrs.pop('custom_fields', None)
|
2018-08-03 09:43:03 -04:00
|
|
|
attrs.pop('tags', None)
|
2018-04-04 15:39:14 -04:00
|
|
|
|
2018-08-03 10:45:53 -04:00
|
|
|
# Skip ManyToManyFields
|
|
|
|
for field in self.Meta.model._meta.get_fields():
|
|
|
|
if isinstance(field, ManyToManyField):
|
|
|
|
attrs.pop(field.name, None)
|
|
|
|
|
2018-04-04 15:39:14 -04:00
|
|
|
# Run clean() on an instance of the model
|
|
|
|
if self.instance is None:
|
|
|
|
instance = self.Meta.model(**attrs)
|
|
|
|
else:
|
|
|
|
instance = self.instance
|
|
|
|
for k, v in attrs.items():
|
|
|
|
setattr(instance, k, v)
|
|
|
|
instance.clean()
|
2020-03-10 19:15:24 -04:00
|
|
|
instance.validate_unique()
|
2018-04-04 15:39:14 -04:00
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
class WritableNestedSerializer(ModelSerializer):
|
|
|
|
"""
|
|
|
|
Returns a nested representation of an object on read, but accepts only a primary key on write.
|
|
|
|
"""
|
2019-04-16 18:02:52 -04:00
|
|
|
|
2018-04-04 15:39:14 -04:00
|
|
|
def to_internal_value(self, data):
|
2019-04-16 18:02:52 -04:00
|
|
|
|
2018-04-05 14:12:43 -04:00
|
|
|
if data is None:
|
|
|
|
return None
|
2019-04-16 18:02:52 -04:00
|
|
|
|
|
|
|
# Dictionary of related object attributes
|
|
|
|
if isinstance(data, dict):
|
|
|
|
params = dict_to_filter_params(data)
|
|
|
|
try:
|
|
|
|
return self.Meta.model.objects.get(**params)
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
raise ValidationError(
|
|
|
|
"Related object not found using the provided attributes: {}".format(params)
|
|
|
|
)
|
|
|
|
except MultipleObjectsReturned:
|
|
|
|
raise ValidationError(
|
|
|
|
"Multiple objects match the provided attributes: {}".format(params)
|
|
|
|
)
|
|
|
|
except FieldError as e:
|
|
|
|
raise ValidationError(e)
|
|
|
|
|
|
|
|
# Integer PK of related object
|
|
|
|
if isinstance(data, int):
|
|
|
|
pk = data
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
# PK might have been mistakenly passed as a string
|
|
|
|
pk = int(data)
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
raise ValidationError(
|
|
|
|
"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
|
|
|
|
"unrecognized value: {}".format(data)
|
|
|
|
)
|
|
|
|
|
|
|
|
# Look up object by PK
|
2018-04-04 15:39:14 -04:00
|
|
|
try:
|
2018-08-08 15:09:30 -04:00
|
|
|
return self.Meta.model.objects.get(pk=int(data))
|
2018-04-04 15:39:14 -04:00
|
|
|
except ObjectDoesNotExist:
|
2019-04-16 18:02:52 -04:00
|
|
|
raise ValidationError(
|
|
|
|
"Related object not found using the provided numeric ID: {}".format(pk)
|
|
|
|
)
|
2018-04-04 15:39:14 -04:00
|
|
|
|
|
|
|
|
2017-10-10 16:41:35 -04:00
|
|
|
#
|
2017-11-07 15:36:10 -05:00
|
|
|
# Viewsets
|
2017-10-10 16:41:35 -04:00
|
|
|
#
|
|
|
|
|
2018-07-30 12:49:08 -04:00
|
|
|
class ModelViewSet(_ModelViewSet):
|
2017-11-07 15:36:10 -05:00
|
|
|
"""
|
2018-04-05 14:12:43 -04:00
|
|
|
Accept either a single object or a list of objects to create.
|
2017-11-07 15:36:10 -05:00
|
|
|
"""
|
|
|
|
def get_serializer(self, *args, **kwargs):
|
2018-07-30 12:49:08 -04:00
|
|
|
|
2017-11-07 15:36:10 -05:00
|
|
|
# If a list of objects has been provided, initialize the serializer with many=True
|
|
|
|
if isinstance(kwargs.get('data', {}), list):
|
|
|
|
kwargs['many'] = True
|
2018-07-30 12:49:08 -04:00
|
|
|
|
2018-11-27 10:52:24 -05:00
|
|
|
return super().get_serializer(*args, **kwargs)
|
2017-11-07 15:36:10 -05:00
|
|
|
|
2018-10-04 13:43:44 -04:00
|
|
|
def get_serializer_class(self):
|
2020-03-02 16:51:16 -05:00
|
|
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
2018-10-04 13:43:44 -04:00
|
|
|
|
|
|
|
# If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one
|
|
|
|
# exists
|
|
|
|
request = self.get_serializer_context()['request']
|
2020-03-02 16:51:16 -05:00
|
|
|
if request.query_params.get('brief'):
|
|
|
|
logger.debug("Request is for 'brief' format; initializing nested serializer")
|
2019-04-02 11:17:14 -04:00
|
|
|
try:
|
2020-03-02 16:51:16 -05:00
|
|
|
serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
|
|
|
|
logger.debug(f"Using serializer {serializer}")
|
|
|
|
return serializer
|
2019-04-02 11:17:14 -04:00
|
|
|
except SerializerNotFound:
|
|
|
|
pass
|
2018-10-04 13:43:44 -04:00
|
|
|
|
|
|
|
# Fall back to the hard-coded serializer class
|
2020-03-02 16:51:16 -05:00
|
|
|
logger.debug(f"Using serializer {self.serializer_class}")
|
2018-10-04 13:43:44 -04:00
|
|
|
return self.serializer_class
|
|
|
|
|
2020-05-14 17:44:46 -04:00
|
|
|
def initial(self, request, *args, **kwargs):
|
|
|
|
super().initial(request, *args, **kwargs)
|
|
|
|
|
|
|
|
if not request.user.is_authenticated or request.user.is_superuser:
|
|
|
|
return
|
|
|
|
|
2020-06-01 10:45:49 -04:00
|
|
|
# TODO: Reconcile this with TokenPermissions.perms_map
|
|
|
|
action = {
|
|
|
|
'GET': 'view',
|
|
|
|
'OPTIONS': None,
|
|
|
|
'HEAD': 'view',
|
|
|
|
'POST': 'add',
|
|
|
|
'PUT': 'change',
|
|
|
|
'PATCH': 'change',
|
|
|
|
'DELETE': 'delete',
|
|
|
|
}[request.method]
|
2020-05-14 17:44:46 -04:00
|
|
|
|
2020-05-29 15:09:08 -04:00
|
|
|
# Restrict the view's QuerySet to allow only the permitted objects
|
2020-06-01 10:45:49 -04:00
|
|
|
if action:
|
|
|
|
self.queryset = self.queryset.restrict(request.user, action)
|
2020-05-14 17:44:46 -04:00
|
|
|
|
2019-05-28 21:11:23 +02:00
|
|
|
def dispatch(self, request, *args, **kwargs):
|
2020-03-02 16:51:16 -05:00
|
|
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
|
|
|
|
2019-05-28 21:11:23 +02:00
|
|
|
try:
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
except ProtectedError as e:
|
2020-03-02 16:51:16 -05:00
|
|
|
models = [
|
|
|
|
'{} ({})'.format(o, o._meta) for o in e.protected_objects.all()
|
|
|
|
]
|
2019-05-29 10:33:29 -04:00
|
|
|
msg = 'Unable to delete object. The following dependent objects were found: {}'.format(', '.join(models))
|
2020-03-02 16:51:16 -05:00
|
|
|
logger.warning(msg)
|
2019-05-28 21:11:23 +02:00
|
|
|
return self.finalize_response(
|
|
|
|
request,
|
|
|
|
Response({'detail': msg}, status=409),
|
|
|
|
*args,
|
|
|
|
**kwargs
|
|
|
|
)
|
|
|
|
|
2020-05-21 10:51:40 -04:00
|
|
|
def _validate_objects(self, instance):
|
2019-04-15 03:55:33 -04:00
|
|
|
"""
|
2020-05-21 10:51:40 -04:00
|
|
|
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.
|
2019-04-15 03:55:33 -04:00
|
|
|
"""
|
2020-05-21 10:51:40 -04:00
|
|
|
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
|
|
|
|
else:
|
|
|
|
# Check that the instance is matched by the view's queryset
|
|
|
|
self.queryset.get(pk=instance.pk)
|
2020-03-04 13:32:45 -05:00
|
|
|
|
|
|
|
def perform_create(self, serializer):
|
2020-05-21 10:51:40 -04:00
|
|
|
model = self.queryset.model
|
2020-03-04 13:32:45 -05:00
|
|
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
|
|
|
logger.info(f"Creating new {model._meta.verbose_name}")
|
2020-05-21 10:51:40 -04:00
|
|
|
|
|
|
|
# Enforce object-level permissions on save()
|
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
|
|
|
instance = serializer.save()
|
|
|
|
self._validate_objects(instance)
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
raise PermissionDenied()
|
2020-03-04 13:32:45 -05:00
|
|
|
|
|
|
|
def perform_update(self, serializer):
|
2020-05-21 10:51:40 -04:00
|
|
|
model = self.queryset.model
|
2020-03-04 13:32:45 -05:00
|
|
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
2020-05-21 10:51:40 -04:00
|
|
|
logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")
|
|
|
|
|
|
|
|
# Enforce object-level permissions on save()
|
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
|
|
|
instance = serializer.save()
|
|
|
|
self._validate_objects(instance)
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
raise PermissionDenied()
|
2020-03-04 13:32:45 -05:00
|
|
|
|
|
|
|
def perform_destroy(self, instance):
|
2020-05-21 10:51:40 -04:00
|
|
|
model = self.queryset.model
|
2020-03-04 13:32:45 -05:00
|
|
|
logger = logging.getLogger('netbox.api.views.ModelViewSet')
|
2020-05-21 10:51:40 -04:00
|
|
|
logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
|
|
|
|
|
2020-03-04 13:32:45 -05:00
|
|
|
return super().perform_destroy(instance)
|