api.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. from __future__ import unicode_literals
  2. from collections import OrderedDict
  3. from django.conf import settings
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.http import Http404
  6. from rest_framework import mixins
  7. from rest_framework.exceptions import APIException
  8. from rest_framework.permissions import BasePermission
  9. from rest_framework.response import Response
  10. from rest_framework.serializers import Field, ModelSerializer, ValidationError
  11. from rest_framework.viewsets import GenericViewSet, ViewSet
  12. WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
  13. class ServiceUnavailable(APIException):
  14. status_code = 503
  15. default_detail = "Service temporarily unavailable, please try again later."
  16. #
  17. # Authentication
  18. #
  19. class IsAuthenticatedOrLoginNotRequired(BasePermission):
  20. """
  21. Returns True if the user is authenticated or LOGIN_REQUIRED is False.
  22. """
  23. def has_permission(self, request, view):
  24. if not settings.LOGIN_REQUIRED:
  25. return True
  26. return request.user.is_authenticated()
  27. #
  28. # Serializers
  29. #
  30. class ValidatedModelSerializer(ModelSerializer):
  31. """
  32. Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
  33. """
  34. def validate(self, data):
  35. # Remove custom field data (if any) prior to model validation
  36. attrs = data.copy()
  37. attrs.pop('custom_fields', None)
  38. # Run clean() on an instance of the model
  39. if self.instance is None:
  40. instance = self.Meta.model(**attrs)
  41. else:
  42. instance = self.instance
  43. for k, v in attrs.items():
  44. setattr(instance, k, v)
  45. instance.clean()
  46. return data
  47. class ChoiceFieldSerializer(Field):
  48. """
  49. Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
  50. """
  51. def __init__(self, choices, **kwargs):
  52. self._choices = dict()
  53. for k, v in choices:
  54. # Unpack grouped choices
  55. if type(v) in [list, tuple]:
  56. for k2, v2 in v:
  57. self._choices[k2] = v2
  58. else:
  59. self._choices[k] = v
  60. super(ChoiceFieldSerializer, self).__init__(**kwargs)
  61. def to_representation(self, obj):
  62. return {'value': obj, 'label': self._choices[obj]}
  63. def to_internal_value(self, data):
  64. return self._choices.get(data)
  65. class ContentTypeFieldSerializer(Field):
  66. """
  67. Represent a ContentType as '<app_label>.<model>'
  68. """
  69. def to_representation(self, obj):
  70. return "{}.{}".format(obj.app_label, obj.model)
  71. def to_internal_value(self, data):
  72. app_label, model = data.split('.')
  73. try:
  74. return ContentType.objects.get_by_natural_key(app_label=app_label, model=model)
  75. except ContentType.DoesNotExist:
  76. raise ValidationError("Invalid content type")
  77. #
  78. # Viewsets
  79. #
  80. class ModelViewSet(mixins.CreateModelMixin,
  81. mixins.RetrieveModelMixin,
  82. mixins.UpdateModelMixin,
  83. mixins.DestroyModelMixin,
  84. mixins.ListModelMixin,
  85. GenericViewSet):
  86. """
  87. Substitute DRF's built-in ModelViewSet for our own, which introduces a bit of additional functionality:
  88. 1. Use an alternate serializer (if provided) for write operations
  89. 2. Accept either a single object or a list of objects to create
  90. """
  91. def get_serializer_class(self):
  92. # Check for a different serializer to use for write operations
  93. if self.action in WRITE_OPERATIONS and hasattr(self, 'write_serializer_class'):
  94. return self.write_serializer_class
  95. return self.serializer_class
  96. def get_serializer(self, *args, **kwargs):
  97. # If a list of objects has been provided, initialize the serializer with many=True
  98. if isinstance(kwargs.get('data', {}), list):
  99. kwargs['many'] = True
  100. return super(ModelViewSet, self).get_serializer(*args, **kwargs)
  101. class FieldChoicesViewSet(ViewSet):
  102. """
  103. Expose the built-in numeric values which represent static choices for a model's field.
  104. """
  105. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  106. fields = []
  107. def __init__(self, *args, **kwargs):
  108. super(FieldChoicesViewSet, self).__init__(*args, **kwargs)
  109. # Compile a dict of all fields in this view
  110. self._fields = OrderedDict()
  111. for cls, field_list in self.fields:
  112. for field_name in field_list:
  113. model_name = cls._meta.verbose_name.lower().replace(' ', '-')
  114. key = ':'.join([model_name, field_name])
  115. choices = []
  116. for k, v in cls._meta.get_field(field_name).choices:
  117. if type(v) in [list, tuple]:
  118. for k2, v2 in v:
  119. choices.append({
  120. 'value': k2,
  121. 'label': v2,
  122. })
  123. else:
  124. choices.append({
  125. 'value': k,
  126. 'label': v,
  127. })
  128. self._fields[key] = choices
  129. def list(self, request):
  130. return Response(self._fields)
  131. def retrieve(self, request, pk):
  132. if pk not in self._fields:
  133. raise Http404
  134. return Response(self._fields[pk])
  135. def get_view_name(self):
  136. return "Field Choices"