api.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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 FieldDoesNotExist
  6. from django.db.models.fields.related import ManyToOneRel, RelatedField
  7. from django.http import JsonResponse
  8. from django.urls import reverse
  9. from rest_framework import status
  10. from rest_framework.serializers import Serializer
  11. from rest_framework.utils import formatting
  12. from netbox.api.fields import RelatedObjectCountField
  13. from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
  14. from utilities.utils import count_related
  15. from .utils import dynamic_import
  16. __all__ = (
  17. 'get_annotations_for_serializer',
  18. 'get_graphql_type_for_model',
  19. 'get_prefetches_for_serializer',
  20. 'get_serializer_for_model',
  21. 'get_view_name',
  22. 'is_api_request',
  23. 'rest_api_server_error',
  24. )
  25. def get_serializer_for_model(model, prefix=''):
  26. """
  27. Dynamically resolve and return the appropriate serializer for a model.
  28. """
  29. app_label, model_name = model._meta.label.split('.')
  30. serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer'
  31. try:
  32. return dynamic_import(serializer_name)
  33. except AttributeError:
  34. raise SerializerNotFound(
  35. f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'"
  36. )
  37. def get_graphql_type_for_model(model):
  38. """
  39. Return the GraphQL type class for the given model.
  40. """
  41. app_name, model_name = model._meta.label.split('.')
  42. # Object types for Django's auth models are in the users app
  43. if app_name == 'auth':
  44. app_name = 'users'
  45. class_name = f'{app_name}.graphql.types.{model_name}Type'
  46. try:
  47. return dynamic_import(class_name)
  48. except AttributeError:
  49. raise GraphQLTypeNotFound(f"Could not find GraphQL type for {app_name}.{model_name}")
  50. def is_api_request(request):
  51. """
  52. Return True of the request is being made via the REST API.
  53. """
  54. api_path = reverse('api-root')
  55. return request.path_info.startswith(api_path) and request.content_type == 'application/json'
  56. def get_view_name(view, suffix=None):
  57. """
  58. Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name`.
  59. """
  60. if hasattr(view, 'queryset'):
  61. # Determine the model name from the queryset.
  62. name = view.queryset.model._meta.verbose_name
  63. name = ' '.join([w[0].upper() + w[1:] for w in name.split()]) # Capitalize each word
  64. else:
  65. # Replicate DRF's built-in behavior.
  66. name = view.__class__.__name__
  67. name = formatting.remove_trailing_string(name, 'View')
  68. name = formatting.remove_trailing_string(name, 'ViewSet')
  69. name = formatting.camelcase_to_spaces(name)
  70. if suffix:
  71. name += ' ' + suffix
  72. return name
  73. def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
  74. """
  75. Compile and return a list of fields which should be prefetched on the queryset for a serializer.
  76. """
  77. model = serializer_class.Meta.model
  78. # If specific fields are not specified, default to all
  79. if not fields_to_include:
  80. fields_to_include = serializer_class.Meta.fields
  81. prefetch_fields = []
  82. for field_name in fields_to_include:
  83. serializer_field = serializer_class._declared_fields.get(field_name)
  84. # Determine the name of the model field referenced by the serializer field
  85. model_field_name = field_name
  86. if serializer_field and serializer_field.source:
  87. model_field_name = serializer_field.source
  88. # If the serializer field does not map to a discrete model field, skip it.
  89. try:
  90. field = model._meta.get_field(model_field_name)
  91. if isinstance(field, (RelatedField, ManyToOneRel, GenericForeignKey)):
  92. prefetch_fields.append(field.name)
  93. except FieldDoesNotExist:
  94. continue
  95. # If this field is represented by a nested serializer, recurse to resolve prefetches
  96. # for the related object.
  97. if serializer_field:
  98. if issubclass(type(serializer_field), Serializer):
  99. for subfield in get_prefetches_for_serializer(type(serializer_field)):
  100. prefetch_fields.append(f'{field_name}__{subfield}')
  101. return prefetch_fields
  102. def get_annotations_for_serializer(serializer_class, fields_to_include=None):
  103. """
  104. Return a mapping of field names to annotations to be applied to the queryset for a serializer.
  105. """
  106. annotations = {}
  107. # If specific fields are not specified, default to all
  108. if not fields_to_include:
  109. fields_to_include = serializer_class.Meta.fields
  110. model = serializer_class.Meta.model
  111. for field_name, field in serializer_class._declared_fields.items():
  112. if field_name in fields_to_include and type(field) is RelatedObjectCountField:
  113. related_field = model._meta.get_field(field.relation).field
  114. annotations[field_name] = count_related(related_field.model, related_field.name)
  115. return annotations
  116. def rest_api_server_error(request, *args, **kwargs):
  117. """
  118. Handle exceptions and return a useful error message for REST API requests.
  119. """
  120. type_, error, traceback = sys.exc_info()
  121. data = {
  122. 'error': str(error),
  123. 'exception': type_.__name__,
  124. 'netbox_version': settings.VERSION,
  125. 'python_version': platform.python_version(),
  126. }
  127. return JsonResponse(data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)