api.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. from collections import OrderedDict
  2. import pytz
  3. from django.conf import settings
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.exceptions import ObjectDoesNotExist
  6. from django.db.models import ManyToManyField
  7. from django.http import Http404
  8. from django.utils.decorators import method_decorator
  9. from django.views.decorators.cache import cache_page
  10. from rest_framework.exceptions import APIException
  11. from rest_framework.permissions import BasePermission
  12. from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
  13. from rest_framework.response import Response
  14. from rest_framework.serializers import Field, ModelSerializer, ValidationError
  15. from rest_framework.viewsets import ModelViewSet as _ModelViewSet, ViewSet
  16. from .utils import dynamic_import
  17. class ServiceUnavailable(APIException):
  18. status_code = 503
  19. default_detail = "Service temporarily unavailable, please try again later."
  20. class SerializerNotFound(Exception):
  21. pass
  22. def get_serializer_for_model(model, prefix=''):
  23. """
  24. Dynamically resolve and return the appropriate serializer for a model.
  25. """
  26. app_name, model_name = model._meta.label.split('.')
  27. serializer_name = '{}.api.serializers.{}{}Serializer'.format(
  28. app_name, prefix, model_name
  29. )
  30. try:
  31. return dynamic_import(serializer_name)
  32. except AttributeError:
  33. raise SerializerNotFound(
  34. "Could not determine serializer for {}.{} with prefix '{}'".format(app_name, model_name, prefix)
  35. )
  36. #
  37. # Authentication
  38. #
  39. class IsAuthenticatedOrLoginNotRequired(BasePermission):
  40. """
  41. Returns True if the user is authenticated or LOGIN_REQUIRED is False.
  42. """
  43. def has_permission(self, request, view):
  44. if not settings.LOGIN_REQUIRED:
  45. return True
  46. return request.user.is_authenticated
  47. #
  48. # Fields
  49. #
  50. class ChoiceField(Field):
  51. """
  52. Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
  53. """
  54. def __init__(self, choices, **kwargs):
  55. self._choices = dict()
  56. for k, v in choices:
  57. # Unpack grouped choices
  58. if type(v) in [list, tuple]:
  59. for k2, v2 in v:
  60. self._choices[k2] = v2
  61. else:
  62. self._choices[k] = v
  63. super().__init__(**kwargs)
  64. def to_representation(self, obj):
  65. if obj is '':
  66. return None
  67. data = OrderedDict([
  68. ('value', obj),
  69. ('label', self._choices[obj])
  70. ])
  71. return data
  72. def to_internal_value(self, data):
  73. # Provide an explicit error message if the request is trying to write a dict
  74. if type(data) is dict:
  75. raise ValidationError('Value must be passed directly (e.g. "foo": 123); do not use a dictionary.')
  76. # Check for string representations of boolean/integer values
  77. if hasattr(data, 'lower'):
  78. if data.lower() == 'true':
  79. data = True
  80. elif data.lower() == 'false':
  81. data = False
  82. else:
  83. try:
  84. data = int(data)
  85. except ValueError:
  86. pass
  87. if data not in self._choices:
  88. raise ValidationError("{} is not a valid choice.".format(data))
  89. return data
  90. @property
  91. def choices(self):
  92. return self._choices
  93. class ContentTypeField(RelatedField):
  94. """
  95. Represent a ContentType as '<app_label>.<model>'
  96. """
  97. default_error_messages = {
  98. "does_not_exist": "Invalid content type: {content_type}",
  99. "invalid": "Invalid value. Specify a content type as '<app_label>.<model_name>'.",
  100. }
  101. def to_internal_value(self, data):
  102. try:
  103. app_label, model = data.split('.')
  104. return ContentType.objects.get_by_natural_key(app_label=app_label, model=model)
  105. except ObjectDoesNotExist:
  106. self.fail('does_not_exist', content_type=data)
  107. except (TypeError, ValueError):
  108. self.fail('invalid')
  109. def to_representation(self, obj):
  110. return "{}.{}".format(obj.app_label, obj.model)
  111. class TimeZoneField(Field):
  112. """
  113. Represent a pytz time zone.
  114. """
  115. def to_representation(self, obj):
  116. return obj.zone if obj else None
  117. def to_internal_value(self, data):
  118. if not data:
  119. return ""
  120. if data not in pytz.common_timezones:
  121. raise ValidationError('Unknown time zone "{}" (see pytz.common_timezones for all options)'.format(data))
  122. return pytz.timezone(data)
  123. class SerializedPKRelatedField(PrimaryKeyRelatedField):
  124. """
  125. Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
  126. objects in a ManyToManyField while still allowing a set of primary keys to be written.
  127. """
  128. def __init__(self, serializer, **kwargs):
  129. self.serializer = serializer
  130. self.pk_field = kwargs.pop('pk_field', None)
  131. super().__init__(**kwargs)
  132. def to_representation(self, value):
  133. return self.serializer(value, context={'request': self.context['request']}).data
  134. #
  135. # Serializers
  136. #
  137. # TODO: We should probably take a fresh look at exactly what we're doing with this. There might be a more elegant
  138. # way to enforce model validation on the serializer.
  139. class ValidatedModelSerializer(ModelSerializer):
  140. """
  141. Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
  142. """
  143. def validate(self, data):
  144. # Remove custom fields data and tags (if any) prior to model validation
  145. attrs = data.copy()
  146. attrs.pop('custom_fields', None)
  147. attrs.pop('tags', None)
  148. # Skip ManyToManyFields
  149. for field in self.Meta.model._meta.get_fields():
  150. if isinstance(field, ManyToManyField):
  151. attrs.pop(field.name, None)
  152. # Run clean() on an instance of the model
  153. if self.instance is None:
  154. instance = self.Meta.model(**attrs)
  155. else:
  156. instance = self.instance
  157. for k, v in attrs.items():
  158. setattr(instance, k, v)
  159. instance.clean()
  160. return data
  161. class WritableNestedSerializer(ModelSerializer):
  162. """
  163. Returns a nested representation of an object on read, but accepts only a primary key on write.
  164. """
  165. def run_validators(self, value):
  166. # DRF v3.8.2: Skip running validators on the data, since we only accept an integer PK instead of a dict. For
  167. # more context, see:
  168. # https://github.com/encode/django-rest-framework/pull/5922/commits/2227bc47f8b287b66775948ffb60b2d9378ac84f
  169. # https://github.com/encode/django-rest-framework/issues/6053
  170. return
  171. def to_internal_value(self, data):
  172. if data is None:
  173. return None
  174. try:
  175. return self.Meta.model.objects.get(pk=int(data))
  176. except (TypeError, ValueError):
  177. raise ValidationError("Primary key must be an integer")
  178. except ObjectDoesNotExist:
  179. raise ValidationError("Invalid ID")
  180. #
  181. # Viewsets
  182. #
  183. class ModelViewSet(_ModelViewSet):
  184. """
  185. Accept either a single object or a list of objects to create.
  186. """
  187. def get_serializer(self, *args, **kwargs):
  188. # If a list of objects has been provided, initialize the serializer with many=True
  189. if isinstance(kwargs.get('data', {}), list):
  190. kwargs['many'] = True
  191. return super().get_serializer(*args, **kwargs)
  192. def get_serializer_class(self):
  193. # If 'brief' has been passed as a query param, find and return the nested serializer for this model, if one
  194. # exists
  195. request = self.get_serializer_context()['request']
  196. if request.query_params.get('brief', False):
  197. try:
  198. return get_serializer_for_model(self.queryset.model, prefix='Nested')
  199. except SerializerNotFound:
  200. pass
  201. # Fall back to the hard-coded serializer class
  202. return self.serializer_class
  203. @method_decorator(cache_page(settings.CACHE_TIMEOUT))
  204. def list(self, *args, **kwargs):
  205. """
  206. Call to super to allow for caching
  207. """
  208. return super().list(*args, **kwargs)
  209. @method_decorator(cache_page(settings.CACHE_TIMEOUT))
  210. def retrieve(self, *args, **kwargs):
  211. """
  212. Call to super to allow for caching
  213. """
  214. return super().retrieve(*args, **kwargs)
  215. class FieldChoicesViewSet(ViewSet):
  216. """
  217. Expose the built-in numeric values which represent static choices for a model's field.
  218. """
  219. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  220. fields = []
  221. def __init__(self, *args, **kwargs):
  222. super().__init__(*args, **kwargs)
  223. # Compile a dict of all fields in this view
  224. self._fields = OrderedDict()
  225. for cls, field_list in self.fields:
  226. for field_name in field_list:
  227. model_name = cls._meta.verbose_name.lower().replace(' ', '-')
  228. key = ':'.join([model_name, field_name])
  229. serializer = get_serializer_for_model(cls)()
  230. choices = []
  231. for k, v in serializer.get_fields()[field_name].choices.items():
  232. if type(v) in [list, tuple]:
  233. for k2, v2 in v:
  234. choices.append({
  235. 'value': k2,
  236. 'label': v2,
  237. })
  238. else:
  239. choices.append({
  240. 'value': k,
  241. 'label': v,
  242. })
  243. self._fields[key] = choices
  244. @method_decorator(cache_page(settings.CACHE_TIMEOUT))
  245. def list(self, request):
  246. return Response(self._fields)
  247. @method_decorator(cache_page(settings.CACHE_TIMEOUT))
  248. def retrieve(self, request, pk):
  249. if pk not in self._fields:
  250. raise Http404
  251. return Response(self._fields[pk])
  252. def get_view_name(self):
  253. return "Field Choices"