api.py 7.3 KB

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