Message ID | 1479574288-24171-2-git-send-email-stephen@that.guru |
---|---|
State | Superseded |
Headers | show |
Hi Stephen, Apologies for the reviews on the old version. Those comments still apply, I think. With this patch, I think the commit message has somehow got munged? It doesn't seem to bear any resemblence to the actual patch. Regards, Daniel > We still care about Django 1.6 and 1.7, at least until 2.0 is released. > Start testing REST functionality on these versions by using older > versions. > > Signed-off-by: Stephen Finucane <stephen@that.guru> > --- > v2: > - Rebase onto master > --- > patchwork/api/__init__.py | 92 ++++++++++++++++++++++ > patchwork/api/check.py | 98 +++++++++++++++++++++++ > patchwork/api/patch.py | 84 ++++++++++++++++++++ > patchwork/api/person.py | 38 +++++++++ > patchwork/api/project.py | 60 +++++++++++++++ > patchwork/api/user.py | 37 +++++++++ > patchwork/rest_serializers.py | 147 ----------------------------------- > patchwork/urls.py | 20 ++++- > patchwork/views/rest_api.py | 175 ------------------------------------------ > 9 files changed, 428 insertions(+), 323 deletions(-) > create mode 100644 patchwork/api/__init__.py > create mode 100644 patchwork/api/check.py > create mode 100644 patchwork/api/patch.py > create mode 100644 patchwork/api/person.py > create mode 100644 patchwork/api/project.py > create mode 100644 patchwork/api/user.py > delete mode 100644 patchwork/rest_serializers.py > delete mode 100644 patchwork/views/rest_api.py > > diff --git a/patchwork/api/__init__.py b/patchwork/api/__init__.py > new file mode 100644 > index 0000000..dc88a85 > --- /dev/null > +++ b/patchwork/api/__init__.py > @@ -0,0 +1,92 @@ > +# Patchwork - automated patch tracking system > +# Copyright (C) 2016 Linaro Corporation > +# > +# This file is part of the Patchwork package. > +# > +# Patchwork is free software; you can redistribute it and/or modify > +# it under the terms of the GNU General Public License as published by > +# the Free Software Foundation; either version 2 of the License, or > +# (at your option) any later version. > +# > +# Patchwork is distributed in the hope that it will be useful, > +# but WITHOUT ANY WARRANTY; without even the implied warranty of > +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > +# GNU General Public License for more details. > +# > +# You should have received a copy of the GNU General Public License > +# along with Patchwork; if not, write to the Free Software > +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > + > +from django.conf import settings > + > +from rest_framework import permissions > +from rest_framework.pagination import PageNumberPagination > +from rest_framework.response import Response > +from rest_framework.serializers import HyperlinkedModelSerializer > +from rest_framework.serializers import HyperlinkedRelatedField > +from rest_framework.viewsets import ModelViewSet > + > + > +class URLSerializer(HyperlinkedModelSerializer): > + """Just like parent but puts _url for fields""" > + > + def to_representation(self, instance): > + data = super(URLSerializer, self).to_representation(instance) > + for name, field in self.fields.items(): > + if isinstance(field, HyperlinkedRelatedField) and name != 'url': > + data[name + '_url'] = data.pop(name) > + return data > + > + > +class LinkHeaderPagination(PageNumberPagination): > + """Provide pagination based on rfc5988. > + > + This is the Link header, similar to how GitHub does it. See: > + > + https://tools.ietf.org/html/rfc5988#section-5 > + https://developer.github.com/guides/traversing-with-pagination > + """ > + page_size = settings.REST_RESULTS_PER_PAGE > + page_size_query_param = 'per_page' > + > + def get_paginated_response(self, data): > + next_url = self.get_next_link() > + previous_url = self.get_previous_link() > + > + link = '' > + if next_url is not None and previous_url is not None: > + link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"' > + elif next_url is not None: > + link = '<{next_url}>; rel="next"' > + elif previous_url is not None: > + link = '<{previous_url}>; rel="prev"' > + link = link.format(next_url=next_url, previous_url=previous_url) > + headers = {'Link': link} if link else {} > + return Response(data, headers=headers) > + > + > +class PatchworkPermission(permissions.BasePermission): > + """This permission works for Project and Patch model objects""" > + def has_permission(self, request, view): > + if request.method in ('POST', 'DELETE'): > + return False > + return super(PatchworkPermission, self).has_permission(request, view) > + > + def has_object_permission(self, request, view, obj): > + # read only for everyone > + if request.method in permissions.SAFE_METHODS: > + return True > + return obj.is_editable(request.user) > + > + > +class AuthenticatedReadOnly(permissions.BasePermission): > + def has_permission(self, request, view): > + authenticated = request.user.is_authenticated() > + return authenticated and request.method in permissions.SAFE_METHODS > + > + > +class PatchworkViewSet(ModelViewSet): > + pagination_class = LinkHeaderPagination > + > + def get_queryset(self): > + return self.serializer_class.Meta.model.objects.all() > diff --git a/patchwork/api/check.py b/patchwork/api/check.py > new file mode 100644 > index 0000000..12706be > --- /dev/null > +++ b/patchwork/api/check.py > @@ -0,0 +1,98 @@ > +# Patchwork - automated patch tracking system > +# Copyright (C) 2016 Linaro Corporation > +# > +# This file is part of the Patchwork package. > +# > +# Patchwork is free software; you can redistribute it and/or modify > +# it under the terms of the GNU General Public License as published by > +# the Free Software Foundation; either version 2 of the License, or > +# (at your option) any later version. > +# > +# Patchwork is distributed in the hope that it will be useful, > +# but WITHOUT ANY WARRANTY; without even the implied warranty of > +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > +# GNU General Public License for more details. > +# > +# You should have received a copy of the GNU General Public License > +# along with Patchwork; if not, write to the Free Software > +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > + > +from django.core.urlresolvers import reverse > +from rest_framework.exceptions import PermissionDenied > +from rest_framework.relations import HyperlinkedRelatedField > +from rest_framework.response import Response > +from rest_framework.serializers import CurrentUserDefault > +from rest_framework.serializers import HiddenField > +from rest_framework.serializers import ModelSerializer > + > +from patchwork.api import PatchworkViewSet > +from patchwork.models import Check > +from patchwork.models import Patch > + > + > +class CurrentPatchDefault(object): > + def set_context(self, serializer_field): > + self.patch = serializer_field.context['request'].patch > + > + def __call__(self): > + return self.patch > + > + > +class CheckSerializer(ModelSerializer): > + user = HyperlinkedRelatedField( > + 'user-detail', read_only=True, default=CurrentUserDefault()) > + patch = HiddenField(default=CurrentPatchDefault()) > + > + def run_validation(self, data): > + for val, label in Check.STATE_CHOICES: > + if label == data['state']: > + data['state'] = val > + break > + return super(CheckSerializer, self).run_validation(data) > + > + def to_representation(self, instance): > + data = super(CheckSerializer, self).to_representation(instance) > + data['state'] = instance.get_state_display() > + # drf-nested doesn't handle HyperlinkedModelSerializers properly, > + # so we have to put the url in by hand here. > + url = self.context['request'].build_absolute_uri(reverse( > + 'api_1.0:patch-detail', args=[instance.patch.id])) > + data['url'] = url + 'checks/%s/' % instance.id > + data['users_url'] = data.pop('user') > + return data > + > + class Meta: > + model = Check > + fields = ('patch', 'user', 'date', 'state', 'target_url', > + 'description', 'context',) > + read_only_fields = ('date',) > + > + > +class CheckViewSet(PatchworkViewSet): > + serializer_class = CheckSerializer > + > + def not_allowed(self, request, **kwargs): > + raise PermissionDenied() > + > + update = not_allowed > + partial_update = not_allowed > + destroy = not_allowed > + > + def create(self, request, patch_pk): > + p = Patch.objects.get(id=patch_pk) > + if not p.is_editable(request.user): > + raise PermissionDenied() > + request.patch = p > + return super(CheckViewSet, self).create(request) > + > + def list(self, request, patch_pk): > + queryset = self.filter_queryset(self.get_queryset()) > + queryset = queryset.filter(patch=patch_pk) > + > + page = self.paginate_queryset(queryset) > + if page is not None: > + serializer = self.get_serializer(page, many=True) > + return self.get_paginated_response(serializer.data) > + > + serializer = self.get_serializer(queryset, many=True) > + return Response(serializer.data) > diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py > new file mode 100644 > index 0000000..e8b1903 > --- /dev/null > +++ b/patchwork/api/patch.py > @@ -0,0 +1,84 @@ > +# Patchwork - automated patch tracking system > +# Copyright (C) 2016 Linaro Corporation > +# > +# This file is part of the Patchwork package. > +# > +# Patchwork is free software; you can redistribute it and/or modify > +# it under the terms of the GNU General Public License as published by > +# the Free Software Foundation; either version 2 of the License, or > +# (at your option) any later version. > +# > +# Patchwork is distributed in the hope that it will be useful, > +# but WITHOUT ANY WARRANTY; without even the implied warranty of > +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > +# GNU General Public License for more details. > +# > +# You should have received a copy of the GNU General Public License > +# along with Patchwork; if not, write to the Free Software > +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > + > +import email.parser > + > +from rest_framework.serializers import ListSerializer > +from rest_framework.serializers import SerializerMethodField > + > +from patchwork.api import PatchworkPermission > +from patchwork.api import PatchworkViewSet > +from patchwork.api import URLSerializer > +from patchwork.models import Patch > + > + > +class PatchListSerializer(ListSerializer): > + """Semi hack to make the list of patches more efficient""" > + def to_representation(self, data): > + del self.child.fields['content'] > + del self.child.fields['headers'] > + del self.child.fields['diff'] > + return super(PatchListSerializer, self).to_representation(data) > + > + > +class PatchSerializer(URLSerializer): > + mbox_url = SerializerMethodField() > + state = SerializerMethodField() > + > + class Meta: > + model = Patch > + list_serializer_class = PatchListSerializer > + read_only_fields = ('project', 'name', 'date', 'submitter', 'diff', > + 'content', 'hash', 'msgid') > + # there's no need to expose an entire "tags" endpoint, so we custom > + # render this field > + exclude = ('tags',) > + > + def get_state(self, obj): > + return obj.state.name > + > + def get_mbox_url(self, patch): > + request = self.context.get('request', None) > + return request.build_absolute_uri(patch.get_mbox_url()) > + > + def to_representation(self, instance): > + data = super(PatchSerializer, self).to_representation(instance) > + data['checks_url'] = data['url'] + 'checks/' > + data['check'] = instance.combined_check_state > + headers = data.get('headers') > + if headers is not None: > + data['headers'] = email.parser.Parser().parsestr(headers, True) > + data['tags'] = [{'name': x.tag.name, 'count': x.count} > + for x in instance.patchtag_set.all()] > + return data > + > + > +class PatchViewSet(PatchworkViewSet): > + permission_classes = (PatchworkPermission,) > + serializer_class = PatchSerializer > + > + def get_queryset(self): > + qs = super(PatchViewSet, self).get_queryset( > + ).prefetch_related( > + 'check_set', 'patchtag_set' > + ).select_related('state', 'submitter', 'delegate') > + if 'pk' not in self.kwargs: > + # we are doing a listing, we don't need these fields > + qs = qs.defer('content', 'diff', 'headers') > + return qs > diff --git a/patchwork/api/person.py b/patchwork/api/person.py > new file mode 100644 > index 0000000..9a97dbb > --- /dev/null > +++ b/patchwork/api/person.py > @@ -0,0 +1,38 @@ > +# Patchwork - automated patch tracking system > +# Copyright (C) 2016 Linaro Corporation > +# > +# This file is part of the Patchwork package. > +# > +# Patchwork is free software; you can redistribute it and/or modify > +# it under the terms of the GNU General Public License as published by > +# the Free Software Foundation; either version 2 of the License, or > +# (at your option) any later version. > +# > +# Patchwork is distributed in the hope that it will be useful, > +# but WITHOUT ANY WARRANTY; without even the implied warranty of > +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > +# GNU General Public License for more details. > +# > +# You should have received a copy of the GNU General Public License > +# along with Patchwork; if not, write to the Free Software > +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > + > +from patchwork.api import AuthenticatedReadOnly > +from patchwork.api import PatchworkViewSet > +from patchwork.api import URLSerializer > +from patchwork.models import Person > + > + > +class PersonSerializer(URLSerializer): > + class Meta: > + model = Person > + fields = ('email', 'name', 'user') > + > + > +class PeopleViewSet(PatchworkViewSet): > + permission_classes = (AuthenticatedReadOnly,) > + serializer_class = PersonSerializer > + > + def get_queryset(self): > + qs = super(PeopleViewSet, self).get_queryset() > + return qs.prefetch_related('user') > diff --git a/patchwork/api/project.py b/patchwork/api/project.py > new file mode 100644 > index 0000000..ea09acc > --- /dev/null > +++ b/patchwork/api/project.py > @@ -0,0 +1,60 @@ > +# Patchwork - automated patch tracking system > +# Copyright (C) 2016 Linaro Corporation > +# > +# This file is part of the Patchwork package. > +# > +# Patchwork is free software; you can redistribute it and/or modify > +# it under the terms of the GNU General Public License as published by > +# the Free Software Foundation; either version 2 of the License, or > +# (at your option) any later version. > +# > +# Patchwork is distributed in the hope that it will be useful, > +# but WITHOUT ANY WARRANTY; without even the implied warranty of > +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > +# GNU General Public License for more details. > +# > +# You should have received a copy of the GNU General Public License > +# along with Patchwork; if not, write to the Free Software > +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > + > +from rest_framework.serializers import HyperlinkedModelSerializer > + > +from patchwork.api import PatchworkPermission > +from patchwork.api import PatchworkViewSet > +from patchwork.models import Project > + > + > +class ProjectSerializer(HyperlinkedModelSerializer): > + class Meta: > + model = Project > + exclude = ('send_notifications', 'use_tags') > + > + def to_representation(self, instance): > + data = super(ProjectSerializer, self).to_representation(instance) > + data['link_name'] = data.pop('linkname') > + data['list_email'] = data.pop('listemail') > + data['list_id'] = data.pop('listid') > + return data > + > + > +class ProjectViewSet(PatchworkViewSet): > + permission_classes = (PatchworkPermission,) > + serializer_class = ProjectSerializer > + > + def _handle_linkname(self, pk): > + '''Make it easy for users to list by project-id or linkname''' > + qs = self.get_queryset() > + try: > + qs.get(id=pk) > + except (self.serializer_class.Meta.model.DoesNotExist, ValueError): > + # probably a non-numeric value which means we are going by linkname > + self.kwargs = {'linkname': pk} # try and lookup by linkname > + self.lookup_field = 'linkname' > + > + def retrieve(self, request, pk=None): > + self._handle_linkname(pk) > + return super(ProjectViewSet, self).retrieve(request, pk) > + > + def partial_update(self, request, pk=None): > + self._handle_linkname(pk) > + return super(ProjectViewSet, self).partial_update(request, pk) > diff --git a/patchwork/api/user.py b/patchwork/api/user.py > new file mode 100644 > index 0000000..aa788b8 > --- /dev/null > +++ b/patchwork/api/user.py > @@ -0,0 +1,37 @@ > +# Patchwork - automated patch tracking system > +# Copyright (C) 2016 Linaro Corporation > +# > +# This file is part of the Patchwork package. > +# > +# Patchwork is free software; you can redistribute it and/or modify > +# it under the terms of the GNU General Public License as published by > +# the Free Software Foundation; either version 2 of the License, or > +# (at your option) any later version. > +# > +# Patchwork is distributed in the hope that it will be useful, > +# but WITHOUT ANY WARRANTY; without even the implied warranty of > +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > +# GNU General Public License for more details. > +# > +# You should have received a copy of the GNU General Public License > +# along with Patchwork; if not, write to the Free Software > +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > + > +from django.contrib.auth.models import User > +from rest_framework.serializers import HyperlinkedModelSerializer > + > +from patchwork.api import AuthenticatedReadOnly > +from patchwork.api import PatchworkViewSet > + > + > +class UserSerializer(HyperlinkedModelSerializer): > + class Meta: > + model = User > + exclude = ('date_joined', 'groups', 'is_active', 'is_staff', > + 'is_superuser', 'last_login', 'password', > + 'user_permissions') > + > + > +class UserViewSet(PatchworkViewSet): > + permission_classes = (AuthenticatedReadOnly,) > + serializer_class = UserSerializer > diff --git a/patchwork/rest_serializers.py b/patchwork/rest_serializers.py > deleted file mode 100644 > index 7bbad8d..0000000 > --- a/patchwork/rest_serializers.py > +++ /dev/null > @@ -1,147 +0,0 @@ > -# Patchwork - automated patch tracking system > -# Copyright (C) 2016 Linaro Corporation > -# > -# This file is part of the Patchwork package. > -# > -# Patchwork is free software; you can redistribute it and/or modify > -# it under the terms of the GNU General Public License as published by > -# the Free Software Foundation; either version 2 of the License, or > -# (at your option) any later version. > -# > -# Patchwork is distributed in the hope that it will be useful, > -# but WITHOUT ANY WARRANTY; without even the implied warranty of > -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > -# GNU General Public License for more details. > -# > -# You should have received a copy of the GNU General Public License > -# along with Patchwork; if not, write to the Free Software > -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > - > -import email.parser > - > -from django.contrib.auth.models import User > -from django.core.urlresolvers import reverse > - > -from rest_framework.relations import HyperlinkedRelatedField > -from rest_framework.serializers import ( > - CurrentUserDefault, HiddenField, HyperlinkedModelSerializer, > - ListSerializer, ModelSerializer, SerializerMethodField) > - > -from patchwork.models import Check, Patch, Person, Project > - > - > -class URLSerializer(HyperlinkedModelSerializer): > - """Just like parent but puts _url for fields""" > - > - def to_representation(self, instance): > - data = super(URLSerializer, self).to_representation(instance) > - for name, field in self.fields.items(): > - if isinstance(field, HyperlinkedRelatedField) and name != 'url': > - data[name + '_url'] = data.pop(name) > - return data > - > - > -class PersonSerializer(URLSerializer): > - class Meta: > - model = Person > - fields = ('email', 'name', 'user',) > - > - > -class UserSerializer(HyperlinkedModelSerializer): > - class Meta: > - model = User > - exclude = ('date_joined', 'groups', 'is_active', 'is_staff', > - 'is_superuser', 'last_login', 'password', > - 'user_permissions') > - > - > -class ProjectSerializer(HyperlinkedModelSerializer): > - class Meta: > - model = Project > - exclude = ('send_notifications', 'use_tags') > - > - def to_representation(self, instance): > - data = super(ProjectSerializer, self).to_representation(instance) > - data['link_name'] = data.pop('linkname') > - data['list_email'] = data.pop('listemail') > - data['list_id'] = data.pop('listid') > - return data > - > - > -class PatchListSerializer(ListSerializer): > - """Semi hack to make the list of patches more efficient""" > - def to_representation(self, data): > - del self.child.fields['content'] > - del self.child.fields['headers'] > - del self.child.fields['diff'] > - return super(PatchListSerializer, self).to_representation(data) > - > - > -class PatchSerializer(URLSerializer): > - class Meta: > - model = Patch > - list_serializer_class = PatchListSerializer > - read_only_fields = ('project', 'name', 'date', 'submitter', 'diff', > - 'content', 'hash', 'msgid') > - # there's no need to expose an entire "tags" endpoint, so we custom > - # render this field > - exclude = ('tags',) > - check_names = dict(Check.STATE_CHOICES) > - mbox_url = SerializerMethodField() > - state = SerializerMethodField() > - > - def get_state(self, obj): > - return obj.state.name > - > - def get_mbox_url(self, patch): > - request = self.context.get('request', None) > - return request.build_absolute_uri(patch.get_mbox_url()) > - > - def to_representation(self, instance): > - data = super(PatchSerializer, self).to_representation(instance) > - data['checks_url'] = data['url'] + 'checks/' > - data['check'] = instance.combined_check_state > - headers = data.get('headers') > - if headers is not None: > - data['headers'] = email.parser.Parser().parsestr(headers, True) > - data['tags'] = [{'name': x.tag.name, 'count': x.count} > - for x in instance.patchtag_set.all()] > - return data > - > - > -class CurrentPatchDefault(object): > - def set_context(self, serializer_field): > - self.patch = serializer_field.context['request'].patch > - > - def __call__(self): > - return self.patch > - > - > -class ChecksSerializer(ModelSerializer): > - user = HyperlinkedRelatedField( > - 'user-detail', read_only=True, default=CurrentUserDefault()) > - patch = HiddenField(default=CurrentPatchDefault()) > - > - def run_validation(self, data): > - for val, label in Check.STATE_CHOICES: > - if label == data['state']: > - data['state'] = val > - break > - return super(ChecksSerializer, self).run_validation(data) > - > - def to_representation(self, instance): > - data = super(ChecksSerializer, self).to_representation(instance) > - data['state'] = instance.get_state_display() > - # drf-nested doesn't handle HyperlinkedModelSerializers properly, > - # so we have to put the url in by hand here. > - url = self.context['request'].build_absolute_uri(reverse( > - 'api_1.0:patch-detail', args=[instance.patch.id])) > - data['url'] = url + 'checks/%s/' % instance.id > - data['users_url'] = data.pop('user') > - return data > - > - class Meta: > - model = Check > - fields = ('patch', 'user', 'date', 'state', 'target_url', > - 'description', 'context',) > - read_only_fields = ('date',) > diff --git a/patchwork/urls.py b/patchwork/urls.py > index 33e4781..7644da9 100644 > --- a/patchwork/urls.py > +++ b/patchwork/urls.py > @@ -146,7 +146,25 @@ if settings.ENABLE_REST_API: > if 'rest_framework' not in settings.INSTALLED_APPS: > raise RuntimeError( > 'djangorestframework must be installed to enable the REST API.') > - from patchwork.views.rest_api import router, patches_router > + > + from rest_framework.routers import DefaultRouter > + from rest_framework_nested.routers import NestedSimpleRouter > + > + from patchwork.api.check import CheckViewSet > + from patchwork.api.patch import PatchViewSet > + from patchwork.api.person import PeopleViewSet > + from patchwork.api.project import ProjectViewSet > + from patchwork.api.user import UserViewSet > + > + router = DefaultRouter() > + router.register('patches', PatchViewSet, 'patch') > + router.register('people', PeopleViewSet, 'person') > + router.register('projects', ProjectViewSet, 'project') > + router.register('users', UserViewSet, 'user') > + > + patches_router = NestedSimpleRouter(router, r'patches', lookup='patch') > + patches_router.register(r'checks', CheckViewSet, base_name='patch-checks') > + > urlpatterns += [ > url(r'^api/1.0/', include(router.urls, namespace='api_1.0')), > url(r'^api/1.0/', include(patches_router.urls, namespace='api_1.0')), > diff --git a/patchwork/views/rest_api.py b/patchwork/views/rest_api.py > deleted file mode 100644 > index 9c58669..0000000 > --- a/patchwork/views/rest_api.py > +++ /dev/null > @@ -1,175 +0,0 @@ > -# Patchwork - automated patch tracking system > -# Copyright (C) 2016 Linaro Corporation > -# > -# This file is part of the Patchwork package. > -# > -# Patchwork is free software; you can redistribute it and/or modify > -# it under the terms of the GNU General Public License as published by > -# the Free Software Foundation; either version 2 of the License, or > -# (at your option) any later version. > -# > -# Patchwork is distributed in the hope that it will be useful, > -# but WITHOUT ANY WARRANTY; without even the implied warranty of > -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > -# GNU General Public License for more details. > -# > -# You should have received a copy of the GNU General Public License > -# along with Patchwork; if not, write to the Free Software > -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA > - > -from django.conf import settings > -from patchwork.models import Patch > -from patchwork.rest_serializers import ( > - ChecksSerializer, PatchSerializer, PersonSerializer, ProjectSerializer, > - UserSerializer) > - > -from rest_framework import permissions > -from rest_framework.exceptions import PermissionDenied > -from rest_framework.pagination import PageNumberPagination > -from rest_framework.response import Response > -from rest_framework.routers import DefaultRouter > -from rest_framework.viewsets import ModelViewSet > -from rest_framework_nested.routers import NestedSimpleRouter > - > - > -class LinkHeaderPagination(PageNumberPagination): > - """Provide pagination based on rfc5988 (how github does it) > - https://tools.ietf.org/html/rfc5988#section-5 > - https://developer.github.com/guides/traversing-with-pagination > - """ > - page_size = settings.REST_RESULTS_PER_PAGE > - page_size_query_param = 'per_page' > - > - def get_paginated_response(self, data): > - next_url = self.get_next_link() > - previous_url = self.get_previous_link() > - > - link = '' > - if next_url is not None and previous_url is not None: > - link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"' > - elif next_url is not None: > - link = '<{next_url}>; rel="next"' > - elif previous_url is not None: > - link = '<{previous_url}>; rel="prev"' > - link = link.format(next_url=next_url, previous_url=previous_url) > - headers = {'Link': link} if link else {} > - return Response(data, headers=headers) > - > - > -class PatchworkPermission(permissions.BasePermission): > - """This permission works for Project and Patch model objects""" > - def has_permission(self, request, view): > - if request.method in ('POST', 'DELETE'): > - return False > - return super(PatchworkPermission, self).has_permission(request, view) > - > - def has_object_permission(self, request, view, obj): > - # read only for everyone > - if request.method in permissions.SAFE_METHODS: > - return True > - return obj.is_editable(request.user) > - > - > -class AuthenticatedReadOnly(permissions.BasePermission): > - def has_permission(self, request, view): > - authenticated = request.user.is_authenticated() > - return authenticated and request.method in permissions.SAFE_METHODS > - > - > -class PatchworkViewSet(ModelViewSet): > - pagination_class = LinkHeaderPagination > - > - def get_queryset(self): > - return self.serializer_class.Meta.model.objects.all() > - > - > -class UserViewSet(PatchworkViewSet): > - permission_classes = (AuthenticatedReadOnly, ) > - serializer_class = UserSerializer > - > - > -class PeopleViewSet(PatchworkViewSet): > - permission_classes = (AuthenticatedReadOnly, ) > - serializer_class = PersonSerializer > - > - def get_queryset(self): > - qs = super(PeopleViewSet, self).get_queryset() > - return qs.prefetch_related('user') > - > - > -class ProjectViewSet(PatchworkViewSet): > - permission_classes = (PatchworkPermission, ) > - serializer_class = ProjectSerializer > - > - def _handle_linkname(self, pk): > - '''Make it easy for users to list by project-id or linkname''' > - qs = self.get_queryset() > - try: > - qs.get(id=pk) > - except (self.serializer_class.Meta.model.DoesNotExist, ValueError): > - # probably a non-numeric value which means we are going by linkname > - self.kwargs = {'linkname': pk} # try and lookup by linkname > - self.lookup_field = 'linkname' > - > - def retrieve(self, request, pk=None): > - self._handle_linkname(pk) > - return super(ProjectViewSet, self).retrieve(request, pk) > - > - def partial_update(self, request, pk=None): > - self._handle_linkname(pk) > - return super(ProjectViewSet, self).partial_update(request, pk) > - > - > -class PatchViewSet(PatchworkViewSet): > - permission_classes = (PatchworkPermission,) > - serializer_class = PatchSerializer > - > - def get_queryset(self): > - qs = super(PatchViewSet, self).get_queryset( > - ).prefetch_related( > - 'check_set', 'patchtag_set' > - ).select_related('state', 'submitter', 'delegate') > - if 'pk' not in self.kwargs: > - # we are doing a listing, we don't need these fields > - qs = qs.defer('content', 'diff', 'headers') > - return qs > - > - > -class CheckViewSet(PatchworkViewSet): > - serializer_class = ChecksSerializer > - > - def not_allowed(self, request, **kwargs): > - raise PermissionDenied() > - > - update = not_allowed > - partial_update = not_allowed > - destroy = not_allowed > - > - def create(self, request, patch_pk): > - p = Patch.objects.get(id=patch_pk) > - if not p.is_editable(request.user): > - raise PermissionDenied() > - request.patch = p > - return super(CheckViewSet, self).create(request) > - > - def list(self, request, patch_pk): > - queryset = self.filter_queryset(self.get_queryset()) > - queryset = queryset.filter(patch=patch_pk) > - > - page = self.paginate_queryset(queryset) > - if page is not None: > - serializer = self.get_serializer(page, many=True) > - return self.get_paginated_response(serializer.data) > - > - serializer = self.get_serializer(queryset, many=True) > - return Response(serializer.data) > - > - > -router = DefaultRouter() > -router.register('patches', PatchViewSet, 'patch') > -router.register('people', PeopleViewSet, 'person') > -router.register('projects', ProjectViewSet, 'project') > -router.register('users', UserViewSet, 'user') > - > -patches_router = NestedSimpleRouter(router, r'patches', lookup='patch') > -patches_router.register(r'checks', CheckViewSet, base_name='patch-checks') > -- > 2.7.4 > > _______________________________________________ > Patchwork mailing list > Patchwork@lists.ozlabs.org > https://lists.ozlabs.org/listinfo/patchwork
diff --git a/patchwork/api/__init__.py b/patchwork/api/__init__.py new file mode 100644 index 0000000..dc88a85 --- /dev/null +++ b/patchwork/api/__init__.py @@ -0,0 +1,92 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2016 Linaro Corporation +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.conf import settings + +from rest_framework import permissions +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response +from rest_framework.serializers import HyperlinkedModelSerializer +from rest_framework.serializers import HyperlinkedRelatedField +from rest_framework.viewsets import ModelViewSet + + +class URLSerializer(HyperlinkedModelSerializer): + """Just like parent but puts _url for fields""" + + def to_representation(self, instance): + data = super(URLSerializer, self).to_representation(instance) + for name, field in self.fields.items(): + if isinstance(field, HyperlinkedRelatedField) and name != 'url': + data[name + '_url'] = data.pop(name) + return data + + +class LinkHeaderPagination(PageNumberPagination): + """Provide pagination based on rfc5988. + + This is the Link header, similar to how GitHub does it. See: + + https://tools.ietf.org/html/rfc5988#section-5 + https://developer.github.com/guides/traversing-with-pagination + """ + page_size = settings.REST_RESULTS_PER_PAGE + page_size_query_param = 'per_page' + + def get_paginated_response(self, data): + next_url = self.get_next_link() + previous_url = self.get_previous_link() + + link = '' + if next_url is not None and previous_url is not None: + link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"' + elif next_url is not None: + link = '<{next_url}>; rel="next"' + elif previous_url is not None: + link = '<{previous_url}>; rel="prev"' + link = link.format(next_url=next_url, previous_url=previous_url) + headers = {'Link': link} if link else {} + return Response(data, headers=headers) + + +class PatchworkPermission(permissions.BasePermission): + """This permission works for Project and Patch model objects""" + def has_permission(self, request, view): + if request.method in ('POST', 'DELETE'): + return False + return super(PatchworkPermission, self).has_permission(request, view) + + def has_object_permission(self, request, view, obj): + # read only for everyone + if request.method in permissions.SAFE_METHODS: + return True + return obj.is_editable(request.user) + + +class AuthenticatedReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + authenticated = request.user.is_authenticated() + return authenticated and request.method in permissions.SAFE_METHODS + + +class PatchworkViewSet(ModelViewSet): + pagination_class = LinkHeaderPagination + + def get_queryset(self): + return self.serializer_class.Meta.model.objects.all() diff --git a/patchwork/api/check.py b/patchwork/api/check.py new file mode 100644 index 0000000..12706be --- /dev/null +++ b/patchwork/api/check.py @@ -0,0 +1,98 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2016 Linaro Corporation +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.core.urlresolvers import reverse +from rest_framework.exceptions import PermissionDenied +from rest_framework.relations import HyperlinkedRelatedField +from rest_framework.response import Response +from rest_framework.serializers import CurrentUserDefault +from rest_framework.serializers import HiddenField +from rest_framework.serializers import ModelSerializer + +from patchwork.api import PatchworkViewSet +from patchwork.models import Check +from patchwork.models import Patch + + +class CurrentPatchDefault(object): + def set_context(self, serializer_field): + self.patch = serializer_field.context['request'].patch + + def __call__(self): + return self.patch + + +class CheckSerializer(ModelSerializer): + user = HyperlinkedRelatedField( + 'user-detail', read_only=True, default=CurrentUserDefault()) + patch = HiddenField(default=CurrentPatchDefault()) + + def run_validation(self, data): + for val, label in Check.STATE_CHOICES: + if label == data['state']: + data['state'] = val + break + return super(CheckSerializer, self).run_validation(data) + + def to_representation(self, instance): + data = super(CheckSerializer, self).to_representation(instance) + data['state'] = instance.get_state_display() + # drf-nested doesn't handle HyperlinkedModelSerializers properly, + # so we have to put the url in by hand here. + url = self.context['request'].build_absolute_uri(reverse( + 'api_1.0:patch-detail', args=[instance.patch.id])) + data['url'] = url + 'checks/%s/' % instance.id + data['users_url'] = data.pop('user') + return data + + class Meta: + model = Check + fields = ('patch', 'user', 'date', 'state', 'target_url', + 'description', 'context',) + read_only_fields = ('date',) + + +class CheckViewSet(PatchworkViewSet): + serializer_class = CheckSerializer + + def not_allowed(self, request, **kwargs): + raise PermissionDenied() + + update = not_allowed + partial_update = not_allowed + destroy = not_allowed + + def create(self, request, patch_pk): + p = Patch.objects.get(id=patch_pk) + if not p.is_editable(request.user): + raise PermissionDenied() + request.patch = p + return super(CheckViewSet, self).create(request) + + def list(self, request, patch_pk): + queryset = self.filter_queryset(self.get_queryset()) + queryset = queryset.filter(patch=patch_pk) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py new file mode 100644 index 0000000..e8b1903 --- /dev/null +++ b/patchwork/api/patch.py @@ -0,0 +1,84 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2016 Linaro Corporation +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import email.parser + +from rest_framework.serializers import ListSerializer +from rest_framework.serializers import SerializerMethodField + +from patchwork.api import PatchworkPermission +from patchwork.api import PatchworkViewSet +from patchwork.api import URLSerializer +from patchwork.models import Patch + + +class PatchListSerializer(ListSerializer): + """Semi hack to make the list of patches more efficient""" + def to_representation(self, data): + del self.child.fields['content'] + del self.child.fields['headers'] + del self.child.fields['diff'] + return super(PatchListSerializer, self).to_representation(data) + + +class PatchSerializer(URLSerializer): + mbox_url = SerializerMethodField() + state = SerializerMethodField() + + class Meta: + model = Patch + list_serializer_class = PatchListSerializer + read_only_fields = ('project', 'name', 'date', 'submitter', 'diff', + 'content', 'hash', 'msgid') + # there's no need to expose an entire "tags" endpoint, so we custom + # render this field + exclude = ('tags',) + + def get_state(self, obj): + return obj.state.name + + def get_mbox_url(self, patch): + request = self.context.get('request', None) + return request.build_absolute_uri(patch.get_mbox_url()) + + def to_representation(self, instance): + data = super(PatchSerializer, self).to_representation(instance) + data['checks_url'] = data['url'] + 'checks/' + data['check'] = instance.combined_check_state + headers = data.get('headers') + if headers is not None: + data['headers'] = email.parser.Parser().parsestr(headers, True) + data['tags'] = [{'name': x.tag.name, 'count': x.count} + for x in instance.patchtag_set.all()] + return data + + +class PatchViewSet(PatchworkViewSet): + permission_classes = (PatchworkPermission,) + serializer_class = PatchSerializer + + def get_queryset(self): + qs = super(PatchViewSet, self).get_queryset( + ).prefetch_related( + 'check_set', 'patchtag_set' + ).select_related('state', 'submitter', 'delegate') + if 'pk' not in self.kwargs: + # we are doing a listing, we don't need these fields + qs = qs.defer('content', 'diff', 'headers') + return qs diff --git a/patchwork/api/person.py b/patchwork/api/person.py new file mode 100644 index 0000000..9a97dbb --- /dev/null +++ b/patchwork/api/person.py @@ -0,0 +1,38 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2016 Linaro Corporation +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from patchwork.api import AuthenticatedReadOnly +from patchwork.api import PatchworkViewSet +from patchwork.api import URLSerializer +from patchwork.models import Person + + +class PersonSerializer(URLSerializer): + class Meta: + model = Person + fields = ('email', 'name', 'user') + + +class PeopleViewSet(PatchworkViewSet): + permission_classes = (AuthenticatedReadOnly,) + serializer_class = PersonSerializer + + def get_queryset(self): + qs = super(PeopleViewSet, self).get_queryset() + return qs.prefetch_related('user') diff --git a/patchwork/api/project.py b/patchwork/api/project.py new file mode 100644 index 0000000..ea09acc --- /dev/null +++ b/patchwork/api/project.py @@ -0,0 +1,60 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2016 Linaro Corporation +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from rest_framework.serializers import HyperlinkedModelSerializer + +from patchwork.api import PatchworkPermission +from patchwork.api import PatchworkViewSet +from patchwork.models import Project + + +class ProjectSerializer(HyperlinkedModelSerializer): + class Meta: + model = Project + exclude = ('send_notifications', 'use_tags') + + def to_representation(self, instance): + data = super(ProjectSerializer, self).to_representation(instance) + data['link_name'] = data.pop('linkname') + data['list_email'] = data.pop('listemail') + data['list_id'] = data.pop('listid') + return data + + +class ProjectViewSet(PatchworkViewSet): + permission_classes = (PatchworkPermission,) + serializer_class = ProjectSerializer + + def _handle_linkname(self, pk): + '''Make it easy for users to list by project-id or linkname''' + qs = self.get_queryset() + try: + qs.get(id=pk) + except (self.serializer_class.Meta.model.DoesNotExist, ValueError): + # probably a non-numeric value which means we are going by linkname + self.kwargs = {'linkname': pk} # try and lookup by linkname + self.lookup_field = 'linkname' + + def retrieve(self, request, pk=None): + self._handle_linkname(pk) + return super(ProjectViewSet, self).retrieve(request, pk) + + def partial_update(self, request, pk=None): + self._handle_linkname(pk) + return super(ProjectViewSet, self).partial_update(request, pk) diff --git a/patchwork/api/user.py b/patchwork/api/user.py new file mode 100644 index 0000000..aa788b8 --- /dev/null +++ b/patchwork/api/user.py @@ -0,0 +1,37 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2016 Linaro Corporation +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.contrib.auth.models import User +from rest_framework.serializers import HyperlinkedModelSerializer + +from patchwork.api import AuthenticatedReadOnly +from patchwork.api import PatchworkViewSet + + +class UserSerializer(HyperlinkedModelSerializer): + class Meta: + model = User + exclude = ('date_joined', 'groups', 'is_active', 'is_staff', + 'is_superuser', 'last_login', 'password', + 'user_permissions') + + +class UserViewSet(PatchworkViewSet): + permission_classes = (AuthenticatedReadOnly,) + serializer_class = UserSerializer diff --git a/patchwork/rest_serializers.py b/patchwork/rest_serializers.py deleted file mode 100644 index 7bbad8d..0000000 --- a/patchwork/rest_serializers.py +++ /dev/null @@ -1,147 +0,0 @@ -# Patchwork - automated patch tracking system -# Copyright (C) 2016 Linaro Corporation -# -# This file is part of the Patchwork package. -# -# Patchwork is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# Patchwork is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Patchwork; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import email.parser - -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse - -from rest_framework.relations import HyperlinkedRelatedField -from rest_framework.serializers import ( - CurrentUserDefault, HiddenField, HyperlinkedModelSerializer, - ListSerializer, ModelSerializer, SerializerMethodField) - -from patchwork.models import Check, Patch, Person, Project - - -class URLSerializer(HyperlinkedModelSerializer): - """Just like parent but puts _url for fields""" - - def to_representation(self, instance): - data = super(URLSerializer, self).to_representation(instance) - for name, field in self.fields.items(): - if isinstance(field, HyperlinkedRelatedField) and name != 'url': - data[name + '_url'] = data.pop(name) - return data - - -class PersonSerializer(URLSerializer): - class Meta: - model = Person - fields = ('email', 'name', 'user',) - - -class UserSerializer(HyperlinkedModelSerializer): - class Meta: - model = User - exclude = ('date_joined', 'groups', 'is_active', 'is_staff', - 'is_superuser', 'last_login', 'password', - 'user_permissions') - - -class ProjectSerializer(HyperlinkedModelSerializer): - class Meta: - model = Project - exclude = ('send_notifications', 'use_tags') - - def to_representation(self, instance): - data = super(ProjectSerializer, self).to_representation(instance) - data['link_name'] = data.pop('linkname') - data['list_email'] = data.pop('listemail') - data['list_id'] = data.pop('listid') - return data - - -class PatchListSerializer(ListSerializer): - """Semi hack to make the list of patches more efficient""" - def to_representation(self, data): - del self.child.fields['content'] - del self.child.fields['headers'] - del self.child.fields['diff'] - return super(PatchListSerializer, self).to_representation(data) - - -class PatchSerializer(URLSerializer): - class Meta: - model = Patch - list_serializer_class = PatchListSerializer - read_only_fields = ('project', 'name', 'date', 'submitter', 'diff', - 'content', 'hash', 'msgid') - # there's no need to expose an entire "tags" endpoint, so we custom - # render this field - exclude = ('tags',) - check_names = dict(Check.STATE_CHOICES) - mbox_url = SerializerMethodField() - state = SerializerMethodField() - - def get_state(self, obj): - return obj.state.name - - def get_mbox_url(self, patch): - request = self.context.get('request', None) - return request.build_absolute_uri(patch.get_mbox_url()) - - def to_representation(self, instance): - data = super(PatchSerializer, self).to_representation(instance) - data['checks_url'] = data['url'] + 'checks/' - data['check'] = instance.combined_check_state - headers = data.get('headers') - if headers is not None: - data['headers'] = email.parser.Parser().parsestr(headers, True) - data['tags'] = [{'name': x.tag.name, 'count': x.count} - for x in instance.patchtag_set.all()] - return data - - -class CurrentPatchDefault(object): - def set_context(self, serializer_field): - self.patch = serializer_field.context['request'].patch - - def __call__(self): - return self.patch - - -class ChecksSerializer(ModelSerializer): - user = HyperlinkedRelatedField( - 'user-detail', read_only=True, default=CurrentUserDefault()) - patch = HiddenField(default=CurrentPatchDefault()) - - def run_validation(self, data): - for val, label in Check.STATE_CHOICES: - if label == data['state']: - data['state'] = val - break - return super(ChecksSerializer, self).run_validation(data) - - def to_representation(self, instance): - data = super(ChecksSerializer, self).to_representation(instance) - data['state'] = instance.get_state_display() - # drf-nested doesn't handle HyperlinkedModelSerializers properly, - # so we have to put the url in by hand here. - url = self.context['request'].build_absolute_uri(reverse( - 'api_1.0:patch-detail', args=[instance.patch.id])) - data['url'] = url + 'checks/%s/' % instance.id - data['users_url'] = data.pop('user') - return data - - class Meta: - model = Check - fields = ('patch', 'user', 'date', 'state', 'target_url', - 'description', 'context',) - read_only_fields = ('date',) diff --git a/patchwork/urls.py b/patchwork/urls.py index 33e4781..7644da9 100644 --- a/patchwork/urls.py +++ b/patchwork/urls.py @@ -146,7 +146,25 @@ if settings.ENABLE_REST_API: if 'rest_framework' not in settings.INSTALLED_APPS: raise RuntimeError( 'djangorestframework must be installed to enable the REST API.') - from patchwork.views.rest_api import router, patches_router + + from rest_framework.routers import DefaultRouter + from rest_framework_nested.routers import NestedSimpleRouter + + from patchwork.api.check import CheckViewSet + from patchwork.api.patch import PatchViewSet + from patchwork.api.person import PeopleViewSet + from patchwork.api.project import ProjectViewSet + from patchwork.api.user import UserViewSet + + router = DefaultRouter() + router.register('patches', PatchViewSet, 'patch') + router.register('people', PeopleViewSet, 'person') + router.register('projects', ProjectViewSet, 'project') + router.register('users', UserViewSet, 'user') + + patches_router = NestedSimpleRouter(router, r'patches', lookup='patch') + patches_router.register(r'checks', CheckViewSet, base_name='patch-checks') + urlpatterns += [ url(r'^api/1.0/', include(router.urls, namespace='api_1.0')), url(r'^api/1.0/', include(patches_router.urls, namespace='api_1.0')), diff --git a/patchwork/views/rest_api.py b/patchwork/views/rest_api.py deleted file mode 100644 index 9c58669..0000000 --- a/patchwork/views/rest_api.py +++ /dev/null @@ -1,175 +0,0 @@ -# Patchwork - automated patch tracking system -# Copyright (C) 2016 Linaro Corporation -# -# This file is part of the Patchwork package. -# -# Patchwork is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# Patchwork is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Patchwork; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -from django.conf import settings -from patchwork.models import Patch -from patchwork.rest_serializers import ( - ChecksSerializer, PatchSerializer, PersonSerializer, ProjectSerializer, - UserSerializer) - -from rest_framework import permissions -from rest_framework.exceptions import PermissionDenied -from rest_framework.pagination import PageNumberPagination -from rest_framework.response import Response -from rest_framework.routers import DefaultRouter -from rest_framework.viewsets import ModelViewSet -from rest_framework_nested.routers import NestedSimpleRouter - - -class LinkHeaderPagination(PageNumberPagination): - """Provide pagination based on rfc5988 (how github does it) - https://tools.ietf.org/html/rfc5988#section-5 - https://developer.github.com/guides/traversing-with-pagination - """ - page_size = settings.REST_RESULTS_PER_PAGE - page_size_query_param = 'per_page' - - def get_paginated_response(self, data): - next_url = self.get_next_link() - previous_url = self.get_previous_link() - - link = '' - if next_url is not None and previous_url is not None: - link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"' - elif next_url is not None: - link = '<{next_url}>; rel="next"' - elif previous_url is not None: - link = '<{previous_url}>; rel="prev"' - link = link.format(next_url=next_url, previous_url=previous_url) - headers = {'Link': link} if link else {} - return Response(data, headers=headers) - - -class PatchworkPermission(permissions.BasePermission): - """This permission works for Project and Patch model objects""" - def has_permission(self, request, view): - if request.method in ('POST', 'DELETE'): - return False - return super(PatchworkPermission, self).has_permission(request, view) - - def has_object_permission(self, request, view, obj): - # read only for everyone - if request.method in permissions.SAFE_METHODS: - return True - return obj.is_editable(request.user) - - -class AuthenticatedReadOnly(permissions.BasePermission): - def has_permission(self, request, view): - authenticated = request.user.is_authenticated() - return authenticated and request.method in permissions.SAFE_METHODS - - -class PatchworkViewSet(ModelViewSet): - pagination_class = LinkHeaderPagination - - def get_queryset(self): - return self.serializer_class.Meta.model.objects.all() - - -class UserViewSet(PatchworkViewSet): - permission_classes = (AuthenticatedReadOnly, ) - serializer_class = UserSerializer - - -class PeopleViewSet(PatchworkViewSet): - permission_classes = (AuthenticatedReadOnly, ) - serializer_class = PersonSerializer - - def get_queryset(self): - qs = super(PeopleViewSet, self).get_queryset() - return qs.prefetch_related('user') - - -class ProjectViewSet(PatchworkViewSet): - permission_classes = (PatchworkPermission, ) - serializer_class = ProjectSerializer - - def _handle_linkname(self, pk): - '''Make it easy for users to list by project-id or linkname''' - qs = self.get_queryset() - try: - qs.get(id=pk) - except (self.serializer_class.Meta.model.DoesNotExist, ValueError): - # probably a non-numeric value which means we are going by linkname - self.kwargs = {'linkname': pk} # try and lookup by linkname - self.lookup_field = 'linkname' - - def retrieve(self, request, pk=None): - self._handle_linkname(pk) - return super(ProjectViewSet, self).retrieve(request, pk) - - def partial_update(self, request, pk=None): - self._handle_linkname(pk) - return super(ProjectViewSet, self).partial_update(request, pk) - - -class PatchViewSet(PatchworkViewSet): - permission_classes = (PatchworkPermission,) - serializer_class = PatchSerializer - - def get_queryset(self): - qs = super(PatchViewSet, self).get_queryset( - ).prefetch_related( - 'check_set', 'patchtag_set' - ).select_related('state', 'submitter', 'delegate') - if 'pk' not in self.kwargs: - # we are doing a listing, we don't need these fields - qs = qs.defer('content', 'diff', 'headers') - return qs - - -class CheckViewSet(PatchworkViewSet): - serializer_class = ChecksSerializer - - def not_allowed(self, request, **kwargs): - raise PermissionDenied() - - update = not_allowed - partial_update = not_allowed - destroy = not_allowed - - def create(self, request, patch_pk): - p = Patch.objects.get(id=patch_pk) - if not p.is_editable(request.user): - raise PermissionDenied() - request.patch = p - return super(CheckViewSet, self).create(request) - - def list(self, request, patch_pk): - queryset = self.filter_queryset(self.get_queryset()) - queryset = queryset.filter(patch=patch_pk) - - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - - -router = DefaultRouter() -router.register('patches', PatchViewSet, 'patch') -router.register('people', PeopleViewSet, 'person') -router.register('projects', ProjectViewSet, 'project') -router.register('users', UserViewSet, 'user') - -patches_router = NestedSimpleRouter(router, r'patches', lookup='patch') -patches_router.register(r'checks', CheckViewSet, base_name='patch-checks')
We still care about Django 1.6 and 1.7, at least until 2.0 is released. Start testing REST functionality on these versions by using older versions. Signed-off-by: Stephen Finucane <stephen@that.guru> --- v2: - Rebase onto master --- patchwork/api/__init__.py | 92 ++++++++++++++++++++++ patchwork/api/check.py | 98 +++++++++++++++++++++++ patchwork/api/patch.py | 84 ++++++++++++++++++++ patchwork/api/person.py | 38 +++++++++ patchwork/api/project.py | 60 +++++++++++++++ patchwork/api/user.py | 37 +++++++++ patchwork/rest_serializers.py | 147 ----------------------------------- patchwork/urls.py | 20 ++++- patchwork/views/rest_api.py | 175 ------------------------------------------ 9 files changed, 428 insertions(+), 323 deletions(-) create mode 100644 patchwork/api/__init__.py create mode 100644 patchwork/api/check.py create mode 100644 patchwork/api/patch.py create mode 100644 patchwork/api/person.py create mode 100644 patchwork/api/project.py create mode 100644 patchwork/api/user.py delete mode 100644 patchwork/rest_serializers.py delete mode 100644 patchwork/views/rest_api.py