diff mbox

[v2,01/13] requirements: Test older versions of DRF

Message ID 1479574288-24171-2-git-send-email-stephen@that.guru
State Superseded
Headers show

Commit Message

Stephen Finucane Nov. 19, 2016, 4:51 p.m. UTC
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

Comments

Daniel Axtens Nov. 21, 2016, 12:34 a.m. UTC | #1
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 mbox

Patch

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')