api.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import platform
  2. import sys
  3. from django.conf import settings
  4. from django.contrib.contenttypes.fields import GenericForeignKey
  5. from django.core.exceptions import (
  6. FieldDoesNotExist, FieldError, MultipleObjectsReturned, ObjectDoesNotExist, ValidationError,
  7. )
  8. from django.db.models.fields.related import ManyToOneRel, RelatedField
  9. from django.http import JsonResponse
  10. from django.urls import reverse
  11. from django.utils.translation import gettext_lazy as _
  12. from rest_framework import status
  13. from rest_framework.serializers import Serializer
  14. from rest_framework.views import get_view_name as drf_get_view_name
  15. from extras.constants import HTTP_CONTENT_TYPE_JSON
  16. from netbox.api.fields import RelatedObjectCountField
  17. from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
  18. from .utils import count_related, dict_to_filter_params, dynamic_import, title
  19. __all__ = (
  20. 'get_annotations_for_serializer',
  21. 'get_graphql_type_for_model',
  22. 'get_prefetches_for_serializer',
  23. 'get_related_object_by_attrs',
  24. 'get_serializer_for_model',
  25. 'get_view_name',
  26. 'is_api_request',
  27. 'rest_api_server_error',
  28. )
  29. def get_serializer_for_model(model, prefix=''):
  30. """
  31. Return the appropriate REST API serializer for the given model.
  32. """
  33. app_label, model_name = model._meta.label.split('.')
  34. serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer'
  35. try:
  36. return dynamic_import(serializer_name)
  37. except AttributeError:
  38. raise SerializerNotFound(
  39. f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'"
  40. )
  41. def get_graphql_type_for_model(model):
  42. """
  43. Return the GraphQL type class for the given model.
  44. """
  45. app_label, model_name = model._meta.label.split('.')
  46. class_name = f'{app_label}.graphql.types.{model_name}Type'
  47. try:
  48. return dynamic_import(class_name)
  49. except AttributeError:
  50. raise GraphQLTypeNotFound(f"Could not find GraphQL type for {app_label}.{model_name}")
  51. def is_api_request(request):
  52. """
  53. Return True of the request is being made via the REST API.
  54. """
  55. api_path = reverse('api-root')
  56. return request.path_info.startswith(api_path) and request.content_type == HTTP_CONTENT_TYPE_JSON
  57. def get_view_name(view):
  58. """
  59. Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name()`.
  60. This function is provided to DRF as its VIEW_NAME_FUNCTION.
  61. """
  62. if hasattr(view, 'queryset'):
  63. # Derive the model name from the queryset.
  64. name = title(view.queryset.model._meta.verbose_name)
  65. if suffix := getattr(view, 'suffix', None):
  66. name = f'{name} {suffix}'
  67. return name
  68. # Fall back to DRF's default behavior
  69. return drf_get_view_name(view)
  70. def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
  71. """
  72. Compile and return a list of fields which should be prefetched on the queryset for a serializer.
  73. """
  74. model = serializer_class.Meta.model
  75. # If fields are not specified, default to all
  76. if not fields_to_include:
  77. fields_to_include = serializer_class.Meta.fields
  78. prefetch_fields = []
  79. for field_name in fields_to_include:
  80. serializer_field = serializer_class._declared_fields.get(field_name)
  81. # Determine the name of the model field referenced by the serializer field
  82. model_field_name = field_name
  83. if serializer_field and serializer_field.source:
  84. model_field_name = serializer_field.source
  85. # If the serializer field does not map to a discrete model field, skip it.
  86. try:
  87. field = model._meta.get_field(model_field_name)
  88. if isinstance(field, (RelatedField, ManyToOneRel, GenericForeignKey)):
  89. prefetch_fields.append(field.name)
  90. except FieldDoesNotExist:
  91. continue
  92. # If this field is represented by a nested serializer, recurse to resolve prefetches
  93. # for the related object.
  94. if serializer_field:
  95. if issubclass(type(serializer_field), Serializer):
  96. # Determine which fields to prefetch for the nested object
  97. subfields = serializer_field.Meta.brief_fields if serializer_field.nested else None
  98. for subfield in get_prefetches_for_serializer(type(serializer_field), subfields):
  99. prefetch_fields.append(f'{field_name}__{subfield}')
  100. return prefetch_fields
  101. def get_annotations_for_serializer(serializer_class, fields_to_include=None):
  102. """
  103. Return a mapping of field names to annotations to be applied to the queryset for a serializer.
  104. """
  105. annotations = {}
  106. # If specific fields are not specified, default to all
  107. if not fields_to_include:
  108. fields_to_include = serializer_class.Meta.fields
  109. model = serializer_class.Meta.model
  110. for field_name, field in serializer_class._declared_fields.items():
  111. if field_name in fields_to_include and type(field) is RelatedObjectCountField:
  112. related_field = model._meta.get_field(field.relation).field
  113. annotations[field_name] = count_related(related_field.model, related_field.name)
  114. return annotations
  115. def get_related_object_by_attrs(queryset, attrs):
  116. """
  117. Return an object identified by either a dictionary of attributes or its numeric primary key (ID). This is used
  118. for referencing related objects when creating/updating objects via the REST API.
  119. """
  120. if attrs is None:
  121. return None
  122. # Dictionary of related object attributes
  123. if isinstance(attrs, dict):
  124. params = dict_to_filter_params(attrs)
  125. try:
  126. return queryset.get(**params)
  127. except ObjectDoesNotExist:
  128. raise ValidationError(
  129. _("Related object not found using the provided attributes: {params}").format(params=params))
  130. except MultipleObjectsReturned:
  131. raise ValidationError(
  132. _("Multiple objects match the provided attributes: {params}").format(params=params)
  133. )
  134. except FieldError as e:
  135. raise ValidationError(e)
  136. # Integer PK of related object
  137. try:
  138. # Cast as integer in case a PK was mistakenly sent as a string
  139. pk = int(attrs)
  140. except (TypeError, ValueError):
  141. raise ValidationError(
  142. _(
  143. "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
  144. "unrecognized value: {value}"
  145. ).format(value=attrs)
  146. )
  147. # Look up object by PK
  148. try:
  149. return queryset.get(pk=pk)
  150. except ObjectDoesNotExist:
  151. raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk))
  152. def rest_api_server_error(request, *args, **kwargs):
  153. """
  154. Handle exceptions and return a useful error message for REST API requests.
  155. """
  156. type_, error, traceback = sys.exc_info()
  157. data = {
  158. 'error': str(error),
  159. 'exception': type_.__name__,
  160. 'netbox_version': settings.VERSION,
  161. 'python_version': platform.python_version(),
  162. }
  163. return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)