From 935a3848eb684f7982ce838e972a7bcda4ac5699 Mon Sep 17 00:00:00 2001 From: Ivaylo Bachvarof Date: Wed, 22 Jun 2016 21:15:27 +0300 Subject: [PATCH 1/5] Add nested serializers support --- demo/project/organisations/serializers.py | 14 ++++++++ demo/project/organisations/urls.py | 1 + demo/project/organisations/views.py | 10 +++++- rest_framework_docs/api_endpoint.py | 36 +++++++++++-------- .../components/fields_list.html | 14 ++++++++ .../templates/rest_framework_docs/home.html | 6 +--- 6 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 rest_framework_docs/templates/rest_framework_docs/components/fields_list.html diff --git a/demo/project/organisations/serializers.py b/demo/project/organisations/serializers.py index c1fc2b1..1a28ce3 100644 --- a/demo/project/organisations/serializers.py +++ b/demo/project/organisations/serializers.py @@ -27,3 +27,17 @@ class OrganisationDetailSerializer(serializers.ModelSerializer): class Meta: model = Organisation fields = ('name', 'slug', 'is_active') + + +class MembershipSerializer(serializers.ModelSerializer): + class Meta: + model = Membership + fields = ('joined', 'is_owner', 'role') + + +class RetrieveOrganisationSerializer(serializers.ModelSerializer): + membership_set = MembershipSerializer() + + class Meta: + model = Organisation + fields = ('name', 'slug', 'is_active', 'membership_set') diff --git a/demo/project/organisations/urls.py b/demo/project/organisations/urls.py index 423e04d..4d0311e 100644 --- a/demo/project/organisations/urls.py +++ b/demo/project/organisations/urls.py @@ -5,6 +5,7 @@ urlpatterns = [ url(r'^create/$', view=views.CreateOrganisationView.as_view(), name="create"), + url(r'^(?P[\w-]+)/$', view=views.RetrieveOrganisationView.as_view(), name="organisation"), url(r'^(?P[\w-]+)/members/$', view=views.OrganisationMembersView.as_view(), name="members"), url(r'^(?P[\w-]+)/leave/$', view=views.LeaveOrganisationView.as_view(), name="leave") diff --git a/demo/project/organisations/views.py b/demo/project/organisations/views.py index 7242f19..dc3f03e 100644 --- a/demo/project/organisations/views.py +++ b/demo/project/organisations/views.py @@ -2,10 +2,18 @@ from rest_framework.response import Response from project.organisations.models import Organisation, Membership from project.organisations.serializers import ( - CreateOrganisationSerializer, OrganisationMembersSerializer + CreateOrganisationSerializer, OrganisationMembersSerializer, RetrieveOrganisationSerializer ) +class RetrieveOrganisationView(generics.RetrieveAPIView): + + serializer_class = RetrieveOrganisationSerializer + + def get_object(self): + pass + + class CreateOrganisationView(generics.CreateAPIView): serializer_class = CreateOrganisationSerializer diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 8eb07c9..4582236 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -2,6 +2,7 @@ import inspect from django.contrib.admindocs.views import simplify_regex from django.utils.encoding import force_str +from rest_framework.serializers import BaseSerializer class ApiEndpoint(object): @@ -16,8 +17,12 @@ def __init__(self, pattern, parent_pattern=None): self.allowed_methods = self.__get_allowed_methods__() # self.view_name = pattern.callback.__name__ self.errors = None - self.fields = self.__get_serializer_fields__() - self.fields_json = self.__get_serializer_fields_json__() + self.serializer_class = self.__get_serializer_class__() + if self.serializer_class: + self.serializer = self.serializer_class() + self.fields = self.__get_serializer_fields__(self.serializer) + self.fields_json = self.__get_serializer_fields_json__() + self.permissions = self.__get_permissions_class__() def __get_path__(self, parent_pattern): @@ -35,23 +40,26 @@ def __get_permissions_class__(self): for perm_class in self.pattern.callback.cls.permission_classes: return perm_class.__name__ - def __get_serializer_fields__(self): - fields = [] - serializer = None - + def __get_serializer_class__(self): if hasattr(self.callback.cls, 'serializer_class'): - serializer = self.callback.cls.serializer_class + return self.callback.cls.serializer_class - elif hasattr(self.callback.cls, 'get_serializer_class'): - serializer = self.callback.cls.get_serializer_class(self.pattern.callback.cls()) + if hasattr(self.callback.cls, 'get_serializer_class'): + return self.callback.cls.get_serializer_class(self.pattern.callback.cls()) + + def __get_serializer_fields__(self, serializer): + fields = [] if hasattr(serializer, 'get_fields'): try: - fields = [{ - "name": key, - "type": str(field.__class__.__name__), - "required": field.required - } for key, field in serializer().get_fields().items()] + for key, field in serializer.get_fields().items(): + sub_fields = self.__get_serializer_fields__(field) if isinstance(field, BaseSerializer) else None + fields.append({ + "name": key, + "type": str(field.__class__.__name__), + "sub_fields": sub_fields, + "required": field.required + }) except KeyError as e: self.errors = e fields = [] diff --git a/rest_framework_docs/templates/rest_framework_docs/components/fields_list.html b/rest_framework_docs/templates/rest_framework_docs/components/fields_list.html new file mode 100644 index 0000000..51fc0c7 --- /dev/null +++ b/rest_framework_docs/templates/rest_framework_docs/components/fields_list.html @@ -0,0 +1,14 @@ +
    + {% for field in fields %} +
  • + {{ field.name }}: {{ field.type }} + {% if field.required %} + R + {% endif %} + + {% if field.sub_fields %} + {%include "rest_framework_docs/components/fields_list.html" with fields=field.sub_fields %} + {% endif %} +
  • + {% endfor %} +
diff --git a/rest_framework_docs/templates/rest_framework_docs/home.html b/rest_framework_docs/templates/rest_framework_docs/home.html index 76d783d..235a6ee 100644 --- a/rest_framework_docs/templates/rest_framework_docs/home.html +++ b/rest_framework_docs/templates/rest_framework_docs/home.html @@ -65,11 +65,7 @@

{% if endpoint.fields %}

Fields:

-
    - {% for field in endpoint.fields %} -
  • {{ field.name }}: {{ field.type }} {% if field.required %}R{% endif %}
  • - {% endfor %} -
+ {%include "rest_framework_docs/components/fields_list.html" with fields=endpoint.fields %} {% elif not endpoint.errors %}

No fields.

{% endif %} From c38fe528fbf22ec289cb044509b197aaa59196bf Mon Sep 17 00:00:00 2001 From: Ivaylo Bachvarof Date: Wed, 22 Jun 2016 21:33:38 +0300 Subject: [PATCH 2/5] Handle KeyError in ApiEndpoint --- rest_framework_docs/api_endpoint.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index 4582236..37402eb 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -19,7 +19,7 @@ def __init__(self, pattern, parent_pattern=None): self.errors = None self.serializer_class = self.__get_serializer_class__() if self.serializer_class: - self.serializer = self.serializer_class() + self.serializer = self.__get_serializer__() self.fields = self.__get_serializer_fields__(self.serializer) self.fields_json = self.__get_serializer_fields_json__() @@ -40,6 +40,12 @@ def __get_permissions_class__(self): for perm_class in self.pattern.callback.cls.permission_classes: return perm_class.__name__ + def __get_serializer__(self): + try: + return self.serializer_class() + except KeyError as e: + self.errors = e + def __get_serializer_class__(self): if hasattr(self.callback.cls, 'serializer_class'): return self.callback.cls.serializer_class @@ -51,19 +57,14 @@ def __get_serializer_fields__(self, serializer): fields = [] if hasattr(serializer, 'get_fields'): - try: - for key, field in serializer.get_fields().items(): - sub_fields = self.__get_serializer_fields__(field) if isinstance(field, BaseSerializer) else None - fields.append({ - "name": key, - "type": str(field.__class__.__name__), - "sub_fields": sub_fields, - "required": field.required - }) - except KeyError as e: - self.errors = e - fields = [] - + for key, field in serializer.get_fields().items(): + sub_fields = self.__get_serializer_fields__(field) if isinstance(field, BaseSerializer) else None + fields.append({ + "name": key, + "type": str(field.__class__.__name__), + "sub_fields": sub_fields, + "required": field.required + }) # FIXME: # Show more attibutes of `field`? From 954d94c78e29d8a2c92d1ca6d083c1025234b8ed Mon Sep 17 00:00:00 2001 From: "i.donchev" Date: Tue, 28 Jun 2016 18:23:19 +0300 Subject: [PATCH 3/5] Adding nested serializers support for many=True relations --- .gitignore | 3 + demo/project/organisations/serializers.py | 15 +- rest_framework_docs/api_endpoint.py | 12 +- .../static/rest_framework_docs/js/dist.min.js | 36338 +++++++++++++++- .../components/fields_list.html | 4 + 5 files changed, 36351 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index c92aa38..bde1b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,8 @@ drfdocs.egg-info/ site/ +demo/node_modules/ +rest_framework_docs/static/rest_framework_docs/js/dist.min.js + rest_framework_docs/static/node_modules/ rest_framework_docs/static/rest_framework_docs/js/dist.min.js.map diff --git a/demo/project/organisations/serializers.py b/demo/project/organisations/serializers.py index 1a28ce3..07cbfb8 100644 --- a/demo/project/organisations/serializers.py +++ b/demo/project/organisations/serializers.py @@ -3,11 +3,18 @@ from project.accounts.serializers import UserProfileSerializer +class MembershipSerializer(serializers.ModelSerializer): + class Meta: + model = Membership + fields = ('joined', 'is_owner', 'role') + + class CreateOrganisationSerializer(serializers.ModelSerializer): + membership_set = MembershipSerializer(many=True) class Meta: model = Organisation - fields = ('name', 'slug',) + fields = ('name', 'slug', 'membership_set') class OrganisationMembersSerializer(serializers.ModelSerializer): @@ -29,12 +36,6 @@ class Meta: fields = ('name', 'slug', 'is_active') -class MembershipSerializer(serializers.ModelSerializer): - class Meta: - model = Membership - fields = ('joined', 'is_owner', 'role') - - class RetrieveOrganisationSerializer(serializers.ModelSerializer): membership_set = MembershipSerializer() diff --git a/rest_framework_docs/api_endpoint.py b/rest_framework_docs/api_endpoint.py index fba5985..89a33f8 100644 --- a/rest_framework_docs/api_endpoint.py +++ b/rest_framework_docs/api_endpoint.py @@ -91,12 +91,20 @@ def __get_serializer_fields__(self, serializer): if hasattr(serializer, 'get_fields'): for key, field in serializer.get_fields().items(): - sub_fields = self.__get_serializer_fields__(field) if isinstance(field, BaseSerializer) else None + to_many_relation = True if hasattr(field, 'many') else False + sub_fields = [] + + if to_many_relation: + sub_fields = self.__get_serializer_fields__(field.child) if isinstance(field, BaseSerializer) else None + else: + sub_fields = self.__get_serializer_fields__(field) if isinstance(field, BaseSerializer) else None + fields.append({ "name": key, "type": str(field.__class__.__name__), "sub_fields": sub_fields, - "required": field.required + "required": field.required, + "to_many_relation": to_many_relation }) # FIXME: # Show more attibutes of `field`? diff --git a/rest_framework_docs/static/rest_framework_docs/js/dist.min.js b/rest_framework_docs/static/rest_framework_docs/js/dist.min.js index 3149c23..560d47f 100644 --- a/rest_framework_docs/static/rest_framework_docs/js/dist.min.js +++ b/rest_framework_docs/static/rest_framework_docs/js/dist.min.js @@ -1,12 +1,36326 @@ -!function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={exports:{},id:o,loaded:!1};return e[o].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";var o=window.$=window.jQuery=n(1),r=(n(2),n(15)),i=n(16),a=n(173),s=n(174),u={transformMethods:function(e){return e.replace(/\W+/g," ").replace(/^[ ]+|[ ]+$/g,"").split(" ")}};o(".plug").bind("click",function(e){e.stopPropagation(),e.preventDefault(),o("#liveAPIModal").modal("toggle");var t=o(this).data();t.methods=r.isArray(t.methods)?t.methods:u.transformMethods(t.methods),a.render(i.createElement(s,{endpoint:t}),document.getElementById("liveAPIEndpoints"))}),o("#liveAPIModal").on("hidden.bs.modal",function(){a.unmountComponentAtNode(document.getElementById("liveAPIEndpoints"))})},function(e,t,n){var o,r;!function(t,n){"object"==typeof e&&"object"==typeof e.exports?e.exports=t.document?n(t,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return n(e)}:n(t)}("undefined"!=typeof window?window:this,function(n,i){function a(e){var t="length"in e&&e.length,n=ue.type(e);return"function"===n||ue.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function s(e,t,n){if(ue.isFunction(t))return ue.grep(e,function(e,o){return!!t.call(e,o,e)!==n});if(t.nodeType)return ue.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ve.test(t))return ue.filter(t,e,n);t=ue.filter(t,e)}return ue.grep(e,function(e){return ue.inArray(e,t)>=0!==n})}function u(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function l(e){var t=_e[e]={};return ue.each(e.match(xe)||[],function(e,n){t[n]=!0}),t}function c(){ye.addEventListener?(ye.removeEventListener("DOMContentLoaded",p,!1),n.removeEventListener("load",p,!1)):(ye.detachEvent("onreadystatechange",p),n.detachEvent("onload",p))}function p(){(ye.addEventListener||"load"===event.type||"complete"===ye.readyState)&&(c(),ue.ready())}function d(e,t,n){if(void 0===n&&1===e.nodeType){var o="data-"+t.replace(ke,"-$1").toLowerCase();if(n=e.getAttribute(o),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:Oe.test(n)?ue.parseJSON(n):n}catch(r){}ue.data(e,t,n)}else n=void 0}return n}function f(e){var t;for(t in e)if(("data"!==t||!ue.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function h(e,t,n,o){if(ue.acceptData(e)){var r,i,a=ue.expando,s=e.nodeType,u=s?ue.cache:e,l=s?e[a]:e[a]&&a;if(l&&u[l]&&(o||u[l].data)||void 0!==n||"string"!=typeof t)return l||(l=s?e[a]=J.pop()||ue.guid++:a),u[l]||(u[l]=s?{}:{toJSON:ue.noop}),("object"==typeof t||"function"==typeof t)&&(o?u[l]=ue.extend(u[l],t):u[l].data=ue.extend(u[l].data,t)),i=u[l],o||(i.data||(i.data={}),i=i.data),void 0!==n&&(i[ue.camelCase(t)]=n),"string"==typeof t?(r=i[t],null==r&&(r=i[ue.camelCase(t)])):r=i,r}}function m(e,t,n){if(ue.acceptData(e)){var o,r,i=e.nodeType,a=i?ue.cache:e,s=i?e[ue.expando]:ue.expando;if(a[s]){if(t&&(o=n?a[s]:a[s].data)){ue.isArray(t)?t=t.concat(ue.map(t,ue.camelCase)):t in o?t=[t]:(t=ue.camelCase(t),t=t in o?[t]:t.split(" ")),r=t.length;for(;r--;)delete o[t[r]];if(n?!f(o):!ue.isEmptyObject(o))return}(n||(delete a[s].data,f(a[s])))&&(i?ue.cleanData([e],!0):ae.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function v(){return!0}function g(){return!1}function y(){try{return ye.activeElement}catch(e){}}function b(e){var t=Fe.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function E(e,t){var n,o,r=0,i=typeof e.getElementsByTagName!==Te?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Te?e.querySelectorAll(t||"*"):void 0;if(!i)for(i=[],n=e.childNodes||e;null!=(o=n[r]);r++)!t||ue.nodeName(o,t)?i.push(o):ue.merge(i,E(o,t));return void 0===t||t&&ue.nodeName(e,t)?ue.merge([e],i):i}function N(e){Pe.test(e.type)&&(e.defaultChecked=e.checked)}function C(e,t){return ue.nodeName(e,"table")&&ue.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function x(e){return e.type=(null!==ue.find.attr(e,"type"))+"/"+e.type,e}function _(e){var t=Qe.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,o=0;null!=(n=e[o]);o++)ue._data(n,"globalEval",!t||ue._data(t[o],"globalEval"))}function D(e,t){if(1===t.nodeType&&ue.hasData(e)){var n,o,r,i=ue._data(e),a=ue._data(t,i),s=i.events;if(s){delete a.handle,a.events={};for(n in s)for(o=0,r=s[n].length;r>o;o++)ue.event.add(t,n,s[n][o])}a.data&&(a.data=ue.extend({},a.data))}}function T(e,t){var n,o,r;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!ae.noCloneEvent&&t[ue.expando]){r=ue._data(t);for(o in r.events)ue.removeEvent(t,o,r.handle);t.removeAttribute(ue.expando)}"script"===n&&t.text!==e.text?(x(t).text=e.text,_(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),ae.html5Clone&&e.innerHTML&&!ue.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Pe.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function O(e,t){var o,r=ue(t.createElement(e)).appendTo(t.body),i=n.getDefaultComputedStyle&&(o=n.getDefaultComputedStyle(r[0]))?o.display:ue.css(r[0],"display");return r.detach(),i}function k(e){var t=ye,n=ot[e];return n||(n=O(e,t),"none"!==n&&n||(nt=(nt||ue("