├── .gitignore ├── README.md ├── django_rest_hal ├── __init__.py ├── parsers.py ├── renderers.py ├── serializers.py ├── tests.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # idea 57 | .idea 58 | 59 | # Mac DS.Store 60 | .DS_Store 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | django-rest-hal 2 | =============== 3 | 4 | HAL Implementation for Django REST Framework 5 | 6 | Includes: 7 | 8 | * HAL Implemenentation without 'curies' 9 | * Defining fields through url-parameter 'fields' 10 | * Disable link generation through url-parameter 'no-links=true' 11 | 12 | For Django REST Framework 3 the have a look at these libs: 13 | 14 | * https://github.com/seebass/drf-hal-json 15 | * https://github.com/seebass/drf-nested-fields 16 | 17 | ## Setup ## 18 | 19 | Include the following settings in your django settings.py 20 | 21 | 'DEFAULT_MODEL_SERIALIZER_CLASS': 'django_rest_hal.serializers.HalModelSerializer', 22 | 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'django_rest_hal.serializers.HalPaginationSerializer', 23 | 'DEFAULT_PARSER_CLASSES': ('django_rest_hal.parsers.JsonHalParser',), 24 | 'DEFAULT_RENDERER_CLASSES': ( 25 | 'django_rest_hal.renderers.JsonHalRenderer' 26 | ) 27 | 28 | ## Usage ## 29 | 30 | Performing REST-Requests results in following HTTP-Responses: 31 | 32 | GET http://localhost/api/resources/1/ HTTP/1.1 33 | Content-Type application/hal+json 34 | 35 | { 36 | "_links": { 37 | "self": "http://localhost/api/resources/1/", 38 | "relatedResource": "http://localhost/api/related-resources/1/" 39 | }, 40 | "id": 1, 41 | "_embedded": { 42 | "subResource": { 43 | "_links": { 44 | "self": "http://localhost/resources/1/sub-resources/26/" 45 | "subSubResource": "http://localhost/resources/1/sub-resources/26/sub-sub-resources/3" 46 | }, 47 | "id": 26, 48 | "name": "Sub Resource 26" 49 | } 50 | } 51 | } 52 | 53 | Field customization can be declared using the URL-Query-Parameter 'fields': 54 | 55 | GET http://localhost/api/resources/1/?fields=id,subResource.fields(name,subSubResource.fields(id) HTTP/1.1 56 | Content-Type application/hal+json 57 | 58 | { 59 | "_links": { 60 | "self": "http://localhost/api/resources/1/", 61 | }, 62 | "id": 1, 63 | "_embedded": { 64 | "subResource": { 65 | "_links": { 66 | "self": "http://localhost/resources/1/sub-resources/26/" 67 | 68 | }, 69 | "name": "Sub Resource 26" 70 | "_embedded": { 71 | "subSubResource": { 72 | "_links": { 73 | "self": "http://localhost/resources/1/sub-resources/26/sub-sub-resources/3" 74 | } 75 | "id": 3 76 | } 77 | 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /django_rest_hal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seebass/django-rest-hal/981b0bad30830ec8167a377a133795e2190f81f6/django_rest_hal/__init__.py -------------------------------------------------------------------------------- /django_rest_hal/parsers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.parsers import JSONParser 2 | from django_rest_hal.renderers import JsonHalRenderer 3 | 4 | 5 | class JsonHalParser(JSONParser): 6 | media_type = "application/hal+json" 7 | renderer_class = JsonHalRenderer 8 | -------------------------------------------------------------------------------- /django_rest_hal/renderers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.renderers import UnicodeJSONRenderer 2 | 3 | 4 | class JsonHalRenderer(UnicodeJSONRenderer): 5 | media_type = "application/hal+json" 6 | -------------------------------------------------------------------------------- /django_rest_hal/serializers.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from rest_framework.compat import get_concrete_model 3 | from rest_framework.fields import Field 4 | from rest_framework.pagination import BasePaginationSerializer, NextPageField, PreviousPageField 5 | from rest_framework.relations import RelatedField, HyperlinkedIdentityField, HyperlinkedRelatedField 6 | from rest_framework.serializers import Serializer, HyperlinkedModelSerializer, \ 7 | HyperlinkedModelSerializerOptions, ModelSerializer 8 | 9 | 10 | class NestedHalSerializerMixin(): 11 | def __init__(self, parentMeta, *args, **kwargs): 12 | self._parentMeta = parentMeta 13 | super(NestedHalSerializerMixin, self).__init__(*args, **kwargs) 14 | 15 | def _options_class(self, meta): 16 | return self.getOptions(meta) 17 | 18 | def getOptions(self, meta): 19 | options = HyperlinkedModelSerializerOptions(self._parentMeta) 20 | options.nestedFields = getattr(self._parentMeta, 'nested_fields', dict()) 21 | options.noLinks = getattr(self._parentMeta, 'no_links', dict()) 22 | return options 23 | 24 | 25 | class NestedHalLinksSerializer(NestedHalSerializerMixin, HyperlinkedModelSerializer): 26 | def getOptions(self, meta): 27 | options = super(NestedHalLinksSerializer, self).getOptions(meta) 28 | if options.depth > 0: 29 | options.fields = ('self',) 30 | return options 31 | 32 | def get_default_fields(self): 33 | fields = self._dict_class() 34 | base_fields = getattr(self._parentMeta, 'base_fields', {}) 35 | self.__add_fields_if_absent(fields, base_fields) 36 | default_fields = super(NestedHalLinksSerializer, self).get_default_fields() 37 | self.__add_fields_if_absent(fields, default_fields) 38 | self.opts.fields = [field for field in self.opts.fields if field in fields.keys()] 39 | return fields 40 | 41 | def __add_fields_if_absent(self, fields, add_fields): 42 | for key, field in add_fields.items(): 43 | if key in self.opts.nestedFields or key in fields: 44 | continue 45 | if isinstance(field, RelatedField) or isinstance(field, HyperlinkedIdentityField): 46 | fields[key] = field 47 | if isinstance(field, Serializer): 48 | view_name = field.opts.model.__name__.lower() + "-detail" 49 | fields[key] = self._hyperlink_field_class(many=field.many, source=field.source, view_name=view_name) 50 | 51 | 52 | class NestedHalEmbeddedSerializer(NestedHalSerializerMixin, ModelSerializer): 53 | _model_serializer_class = None # we cannot set a default because it's a circular dependency 54 | 55 | def getOptions(self, meta): 56 | options = super(NestedHalEmbeddedSerializer, self).getOptions(meta) 57 | if options.nestedFields: 58 | options.depth = 1 59 | return options 60 | 61 | def get_default_fields(self): 62 | fields = self._dict_class() 63 | base_fields = getattr(self._parentMeta, 'base_fields', {}) 64 | self.__add_fields_if_absent(fields, base_fields) 65 | default_fields = super(NestedHalEmbeddedSerializer, self).get_default_fields() 66 | self.__add_fields_if_absent(fields, default_fields) 67 | self.opts.fields = [field for field in self.opts.fields if field in fields.keys()] 68 | return fields 69 | 70 | def get_nested_field(self, model_field, related_model, to_many): 71 | class NestedModelSerializer(self._model_serializer_class): 72 | class Meta: 73 | model = related_model 74 | depth = self.opts.depth - 1 75 | 76 | if not self.opts.nestedFields: 77 | return NestedModelSerializer(many=to_many) 78 | 79 | fieldName = None 80 | if model_field: 81 | fieldName = model_field.name 82 | else: # else means it is a reverse relationship so the accessor_name must be retrieved 83 | cls = self.opts.model 84 | opts = get_concrete_model(cls)._meta 85 | reverse_rels = opts.get_all_related_objects() 86 | reverse_rels += opts.get_all_related_many_to_many_objects() 87 | for relation in reverse_rels: 88 | accessorName = relation.get_accessor_name() 89 | if relation.model == related_model and accessorName in self.opts.nestedFields: 90 | fieldName = accessorName 91 | break 92 | 93 | customFields = self.opts.nestedFields.get(fieldName) 94 | if customFields is not None: 95 | class CustomFieldSerializer(self._model_serializer_class): 96 | class Meta: 97 | model = related_model 98 | fields = ['self'] + customFields[0] + list(customFields[1].keys()) 99 | nested_fields = customFields[1] 100 | no_links = self.opts.noLinks 101 | exclude = None 102 | 103 | return CustomFieldSerializer(many=to_many) 104 | return self.get_related_field(model_field, related_model, to_many) 105 | 106 | @staticmethod 107 | def __add_fields_if_absent(fields, add_fields): 108 | fields.update({key: field for key, field in add_fields.items() if isinstance(field, Serializer) and not key in fields}) 109 | 110 | 111 | class HalModelSerializerOptions(HyperlinkedModelSerializerOptions): 112 | def __init__(self, meta): 113 | super(HalModelSerializerOptions, self).__init__(None) 114 | self.exclude = getattr(meta, 'exclude', ()) 115 | self.model = getattr(meta, 'model', None) 116 | self.nestedFields = getattr(meta, 'nested_fields', None) 117 | self.read_only_fields = getattr(meta, 'read_only_fields', ()) 118 | self.write_only_fields = getattr(meta, 'write_only_fields', ()) 119 | self.fields = getattr(meta, 'fields', ()) 120 | self.noLinks = getattr(meta, 'no_links', False) 121 | 122 | 123 | class HalModelSerializer(ModelSerializer): 124 | _options_class = HalModelSerializerOptions 125 | _nested_links_serializer_class = NestedHalLinksSerializer 126 | _nested_embedded_serializer_class = NestedHalEmbeddedSerializer 127 | 128 | def __init__(self, instance=None, data=None, files=None, context=None, partial=False, many=False, 129 | allow_add_remove=False, **kwargs): 130 | self._nested_embedded_serializer_class._model_serializer_class = self.__class__ 131 | if data and '_links' not in data: 132 | data['_links'] = {} # put links in data, so that field validation does not fail 133 | super(HalModelSerializer, self).__init__(instance, data, files, context, partial, many, allow_add_remove, **kwargs) 134 | 135 | def get_fields(self): 136 | fields = self._dict_class() 137 | 138 | nested = bool(getattr(self.Meta, 'depth', 0)) 139 | if self.init_data: # if init_data is set, a post/put request is handled and nested fields are ignored 140 | setattr(self.Meta, 'nestedFields', {}) 141 | self.opts.nestedFields = {} 142 | 143 | declared_fields = self.__get_declared_fields() 144 | setattr(self.Meta, 'fields', declared_fields) 145 | 146 | base_fields = copy.deepcopy(self.base_fields) 147 | setattr(self.Meta, 'base_fields', base_fields) 148 | 149 | if not self.opts.noLinks: 150 | fields['_links'] = self._nested_links_serializer_class(self.Meta, source="*") 151 | 152 | self.__add_fields_if_absent(fields, base_fields, declared_fields) 153 | self.__add_fields_if_absent(fields, self.get_default_fields(), declared_fields) 154 | 155 | if nested or self.opts.nestedFields: 156 | fields['_embedded'] = self._nested_embedded_serializer_class(self.Meta, source="*") 157 | 158 | self.__handle_excludes(fields) 159 | 160 | for key, field in fields.items(): 161 | field.initialize(parent=self, field_name=key) 162 | 163 | return fields 164 | 165 | def get_pk_field(self, model_field): 166 | # always include id even it is not set in serializer fields definition 167 | return self.get_field(model_field) 168 | 169 | def __handle_excludes(self, fields): 170 | if self.opts.exclude: 171 | assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple' 172 | for key in self.opts.exclude: 173 | fields.pop(key, None) 174 | 175 | def __get_declared_fields(self): 176 | declared_fields = list(getattr(self.Meta, 'fields', [])) 177 | if declared_fields: 178 | if 'self' not in declared_fields and not self.opts.noLinks: 179 | declared_fields.insert(0, 'self') 180 | if 'id' not in declared_fields: 181 | declared_fields.insert(0, 'id') 182 | return declared_fields 183 | 184 | def __add_fields_if_absent(self, fields, add_fields, declared_fields): 185 | fields.update({key: field for key, field in add_fields.items() if 186 | (not isinstance(field, RelatedField) or self.opts.noLinks) 187 | and not isinstance(field, HyperlinkedIdentityField) and not isinstance(field, Serializer) 188 | and not (declared_fields and key not in declared_fields) and not key in fields}) 189 | 190 | 191 | class HalPaginationSerializer(BasePaginationSerializer): 192 | count = Field(source='paginator.count') 193 | page_size = Field(source='paginator.per_page') 194 | results_field = '_embedded' 195 | 196 | def __init__(self, *args, **kwargs): 197 | super(HalPaginationSerializer, self).__init__(*args, **kwargs) 198 | 199 | class NestedLinksSerializer(Serializer): 200 | class NestedSelfLinkField(Field): 201 | def to_native(self, value): 202 | request = self.context.get('request') 203 | return request and request.build_absolute_uri() or '' 204 | 205 | self = NestedSelfLinkField(source='*') 206 | next = NextPageField(source='*') 207 | previous = PreviousPageField(source='*') 208 | 209 | oldFields = self.fields 210 | self.fields = self._dict_class() 211 | self.fields['_links'] = NestedLinksSerializer(source="*") 212 | self.fields.update(oldFields) 213 | -------------------------------------------------------------------------------- /django_rest_hal/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django_rest_hal/views.py: -------------------------------------------------------------------------------- 1 | import re 2 | from rest_framework.settings import api_settings 3 | from rest_framework.viewsets import ModelViewSet 4 | 5 | from django_rest_hal.serializers import HalModelSerializer 6 | 7 | 8 | class CustomNestedFieldsMixin(): 9 | __GET_FIELDS_PATTERN = re.compile(r"([a-zA-Z0-9_-]+?)\.fields\((.*?)\)\Z") 10 | 11 | def get_serializer_class(self): 12 | serializer_class = self.serializer_class 13 | if serializer_class is None: 14 | class DefaultSerializer(self.model_serializer_class): 15 | class Meta: 16 | model = self.model 17 | 18 | serializer_class = DefaultSerializer 19 | 20 | customFieldSerializerClass = self.__getCustomFieldSerializerClass(serializer_class) 21 | if customFieldSerializerClass: 22 | return customFieldSerializerClass 23 | 24 | return serializer_class 25 | 26 | def __getCustomFieldSerializerClass(self, baseSerializerClass): 27 | if not issubclass(baseSerializerClass, HalModelSerializer): 28 | return None 29 | 30 | request = self.get_serializer_context().get('request') 31 | if request: 32 | customFieldsStr = request.QUERY_PARAMS.get('fields') 33 | noLinks = request.QUERY_PARAMS.get('no-links') == 'true' 34 | if customFieldsStr: 35 | customFields, customNestedFields = self.__getCustomFields(customFieldsStr) 36 | 37 | class CustomFieldSerializer(baseSerializerClass): 38 | class Meta: 39 | model = self.model 40 | fields = customFields + list(customNestedFields.keys()) 41 | nested_fields = customNestedFields 42 | no_links = noLinks 43 | exclude = None 44 | 45 | return CustomFieldSerializer 46 | elif noLinks: 47 | class CustomFieldSerializer(baseSerializerClass): 48 | class Meta: 49 | model = self.model 50 | no_links = noLinks 51 | 52 | return CustomFieldSerializer 53 | return None 54 | 55 | def __getCustomFields(self, customFieldsStr): 56 | customNestedFields = dict() 57 | customFields = [] 58 | splittedCustomFieldStrs = self.__splitCustomFields(customFieldsStr) 59 | for customFieldStr in splittedCustomFieldStrs: 60 | subFieldsMatch = self.__GET_FIELDS_PATTERN.search(customFieldStr) 61 | if subFieldsMatch: 62 | fieldName = subFieldsMatch.group(1) 63 | customNestedFields[fieldName] = self.__getCustomFields(subFieldsMatch.group(2)) 64 | else: 65 | customFields.append(customFieldStr) 66 | return customFields, customNestedFields 67 | 68 | @staticmethod 69 | def __splitCustomFields(customFieldsStr): 70 | parenthesisCounter = 0 71 | splittedCustomFieldStrs = [] 72 | foundCustomField = "" 73 | 74 | for char in customFieldsStr: 75 | if char == "(": 76 | parenthesisCounter += 1 77 | if char == ")": 78 | parenthesisCounter -= 1 79 | if char == "," and parenthesisCounter == 0: 80 | splittedCustomFieldStrs.append(foundCustomField) 81 | foundCustomField = "" 82 | continue 83 | foundCustomField += char 84 | splittedCustomFieldStrs.append(foundCustomField) 85 | return splittedCustomFieldStrs 86 | 87 | 88 | class HalModelViewSet(CustomNestedFieldsMixin, ModelViewSet): 89 | def get_success_headers(self, data): 90 | linksData = data.get('_links') 91 | if not linksData: 92 | return {} 93 | urlFieldData = linksData.get(api_settings.URL_FIELD_NAME) 94 | if not urlFieldData: 95 | return {} 96 | return {'Location': urlFieldData} 97 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | README = """ 5 | Django REST Swagger 6 | 7 | An API documentation generator for Swagger UI and Django REST Framework version 2.3+ 8 | 9 | Installation 10 | From pip: 11 | 12 | pip install django-rest-swagger 13 | 14 | Docs & details @ 15 | https://github.com/marcgibbons/django-rest-swagger 16 | """ 17 | 18 | # allow setup.py to be run from any path 19 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 20 | 21 | setup( 22 | name='django-rest-hal', 23 | version='0.1.0', 24 | packages=['django_rest_hal'], 25 | # package_data={'django_rest_hal': ['django_rest_hal/*']}, 26 | # include_package_data=True, 27 | license='FreeBSD License', 28 | description='HAL Implementation for Django REST Framework 2.3+', 29 | long_description=README, 30 | install_requires=[ 31 | 'django>=1.6', 32 | 'djangorestframework>=2.3.5', 33 | ], 34 | 35 | url='http://github.com/seebass', 36 | author='Sebastian Bredehöft', 37 | author_email='bredehoeft.sebastian@gmail.com', 38 | classifiers=[ 39 | 'Environment :: Web Environment', 40 | 'Framework :: Django', 41 | 'Intended Audience :: Developers', 42 | 'License :: OSI Approved :: BSD License', 43 | 'Operating System :: OS Independent', 44 | 'Programming Language :: Python :: 2.7', 45 | 'Topic :: Internet :: WWW/HTTP', 46 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 47 | ], 48 | ) 49 | --------------------------------------------------------------------------------