api.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. from __future__ import unicode_literals
  2. from collections import OrderedDict
  3. import pytz
  4. from taggit.models import Tag
  5. from django.conf import settings
  6. from django.contrib.contenttypes.models import ContentType
  7. from django.core.exceptions import ObjectDoesNotExist
  8. from django.db.models import ManyToManyField
  9. from django.http import Http404
  10. from rest_framework import mixins
  11. from rest_framework.exceptions import APIException
  12. from rest_framework.permissions import BasePermission
  13. from rest_framework.relations import PrimaryKeyRelatedField
  14. from rest_framework.response import Response
  15. from rest_framework.serializers import Field, ModelSerializer, RelatedField, ValidationError
  16. from rest_framework.viewsets import GenericViewSet, ViewSet
  17. from .utils import dynamic_import
  18. WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
  19. class ServiceUnavailable(APIException):
  20. status_code = 503
  21. default_detail = "Service temporarily unavailable, please try again later."
  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. return None
  34. #
  35. # Authentication
  36. #
  37. class IsAuthenticatedOrLoginNotRequired(BasePermission):
  38. """
  39. Returns True if the user is authenticated or LOGIN_REQUIRED is False.
  40. """
  41. def has_permission(self, request, view):
  42. if not settings.LOGIN_REQUIRED:
  43. return True
  44. return request.user.is_authenticated
  45. #
  46. # Fields
  47. #
  48. class TagField(RelatedField):
  49. """
  50. Represent a writable list of Tags associated with an object (use with many=True).
  51. """
  52. def to_internal_value(self, data):
  53. obj = self.parent.parent.instance
  54. content_type = ContentType.objects.get_for_model(obj)
  55. tag, _ = Tag.objects.get_or_create(content_type=content_type, object_id=obj.pk, name=data)
  56. return tag
  57. def to_representation(self, value):
  58. return value.name
  59. class ChoiceField(Field):
  60. """
  61. Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
  62. """
  63. def __init__(self, choices, **kwargs):
  64. self._choices = dict()
  65. for k, v in choices:
  66. # Unpack grouped choices
  67. if type(v) in [list, tuple]:
  68. for k2, v2 in v:
  69. self._choices[k2] = v2
  70. else:
  71. self._choices[k] = v
  72. super(ChoiceField, self).__init__(**kwargs)
  73. def to_representation(self, obj):
  74. return {'value': obj, 'label': self._choices[obj]}
  75. def to_internal_value(self, data):
  76. return data
  77. class ContentTypeField(Field):
  78. """
  79. Represent a ContentType as '<app_label>.<model>'
  80. """
  81. def to_representation(self, obj):
  82. return "{}.{}".format(obj.app_label, obj.model)
  83. def to_internal_value(self, data):
  84. app_label, model = data.split('.')
  85. try:
  86. return ContentType.objects.get_by_natural_key(app_label=app_label, model=model)
  87. except ContentType.DoesNotExist:
  88. raise ValidationError("Invalid content type")
  89. class TimeZoneField(Field):
  90. """
  91. Represent a pytz time zone.
  92. """
  93. def to_representation(self, obj):
  94. return obj.zone if obj else None
  95. def to_internal_value(self, data):
  96. if not data:
  97. return ""
  98. try:
  99. return pytz.timezone(str(data))
  100. except pytz.exceptions.UnknownTimeZoneError:
  101. raise ValidationError('Invalid time zone "{}"'.format(data))
  102. class SerializedPKRelatedField(PrimaryKeyRelatedField):
  103. """
  104. Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
  105. objects in a ManyToManyField while still allowing a set of primary keys to be written.
  106. """
  107. def __init__(self, serializer, **kwargs):
  108. self.serializer = serializer
  109. self.pk_field = kwargs.pop('pk_field', None)
  110. super(SerializedPKRelatedField, self).__init__(**kwargs)
  111. def to_representation(self, value):
  112. return self.serializer(value, context={'request': self.context['request']}).data
  113. #
  114. # Serializers
  115. #
  116. class ValidatedModelSerializer(ModelSerializer):
  117. """
  118. Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
  119. """
  120. def validate(self, data):
  121. # Remove custom field data (if any) prior to model validation
  122. attrs = data.copy()
  123. attrs.pop('custom_fields', None)
  124. # Run clean() on an instance of the model
  125. if self.instance is None:
  126. model = self.Meta.model
  127. # Ignore ManyToManyFields for new instances (a PK is needed for validation)
  128. for field in model._meta.get_fields():
  129. if isinstance(field, ManyToManyField) and field.name in attrs:
  130. attrs.pop(field.name)
  131. instance = self.Meta.model(**attrs)
  132. else:
  133. instance = self.instance
  134. for k, v in attrs.items():
  135. setattr(instance, k, v)
  136. instance.clean()
  137. return data
  138. class WritableNestedSerializer(ModelSerializer):
  139. """
  140. Returns a nested representation of an object on read, but accepts only a primary key on write.
  141. """
  142. def to_internal_value(self, data):
  143. if data is None:
  144. return None
  145. try:
  146. return self.Meta.model.objects.get(pk=data)
  147. except ObjectDoesNotExist:
  148. raise ValidationError("Invalid ID")
  149. #
  150. # Viewsets
  151. #
  152. class ModelViewSet(mixins.CreateModelMixin,
  153. mixins.RetrieveModelMixin,
  154. mixins.UpdateModelMixin,
  155. mixins.DestroyModelMixin,
  156. mixins.ListModelMixin,
  157. GenericViewSet):
  158. """
  159. Accept either a single object or a list of objects to create.
  160. """
  161. def get_serializer(self, *args, **kwargs):
  162. # If a list of objects has been provided, initialize the serializer with many=True
  163. if isinstance(kwargs.get('data', {}), list):
  164. kwargs['many'] = True
  165. return super(ModelViewSet, self).get_serializer(*args, **kwargs)
  166. class FieldChoicesViewSet(ViewSet):
  167. """
  168. Expose the built-in numeric values which represent static choices for a model's field.
  169. """
  170. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  171. fields = []
  172. def __init__(self, *args, **kwargs):
  173. super(FieldChoicesViewSet, self).__init__(*args, **kwargs)
  174. # Compile a dict of all fields in this view
  175. self._fields = OrderedDict()
  176. for cls, field_list in self.fields:
  177. for field_name in field_list:
  178. model_name = cls._meta.verbose_name.lower().replace(' ', '-')
  179. key = ':'.join([model_name, field_name])
  180. choices = []
  181. for k, v in cls._meta.get_field(field_name).choices:
  182. if type(v) in [list, tuple]:
  183. for k2, v2 in v:
  184. choices.append({
  185. 'value': k2,
  186. 'label': v2,
  187. })
  188. else:
  189. choices.append({
  190. 'value': k,
  191. 'label': v,
  192. })
  193. self._fields[key] = choices
  194. def list(self, request):
  195. return Response(self._fields)
  196. def retrieve(self, request, pk):
  197. if pk not in self._fields:
  198. raise Http404
  199. return Response(self._fields[pk])
  200. def get_view_name(self):
  201. return "Field Choices"