From patchwork Sat Nov 19 16:51:16 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stephen Finucane X-Patchwork-Id: 696857 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [103.22.144.68]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 3tLgrm56VNz9t1T for ; Sun, 20 Nov 2016 03:52:24 +1100 (AEDT) Authentication-Results: ozlabs.org; dkim=fail reason="key not found in DNS" (0-bit key; unprotected) header.d=that.guru header.i=@that.guru header.b="kxtMkuIy"; dkim-atps=neutral Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 3tLgrl6vDKzDvmd for ; Sun, 20 Nov 2016 03:52:23 +1100 (AEDT) Authentication-Results: lists.ozlabs.org; dkim=fail reason="key not found in DNS" (0-bit key; unprotected) header.d=that.guru header.i=@that.guru header.b="kxtMkuIy"; dkim-atps=neutral X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Received: from nov-007-i608.relay.mailchannels.net (nov-007-i608.relay.mailchannels.net [46.232.183.162]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 3tLgrZ59PkzDvmt for ; Sun, 20 Nov 2016 03:52:13 +1100 (AEDT) Authentication-Results: lists.ozlabs.org; dkim=fail reason="key not found in DNS" (0-bit key; unprotected) header.d=that.guru header.i=@that.guru header.b="kxtMkuIy"; dkim-atps=neutral X-Sender-Id: mxroute|x-authuser|stephen@that.guru Received: from relay.mailchannels.net (localhost [127.0.0.1]) by relay.mailchannels.net (Postfix) with ESMTP id 499CB120B48 for ; Sat, 19 Nov 2016 16:52:07 +0000 (UTC) Received: from one.mxroute.com (ip-10-27-139-41.us-west-2.compute.internal [10.27.139.41]) by relay.mailchannels.net (Postfix) with ESMTPA id 906131202BD for ; Sat, 19 Nov 2016 16:52:06 +0000 (UTC) X-Sender-Id: mxroute|x-authuser|stephen@that.guru Received: from one.mxroute.com ([UNAVAILABLE]. [10.16.27.41]) (using TLSv1.2 with cipher DHE-RSA-AES256-GCM-SHA384) by 0.0.0.0:2500 (trex/5.7.8); Sat, 19 Nov 2016 16:52:07 +0000 X-MC-Relay: Neutral X-MailChannels-SenderId: mxroute|x-authuser|stephen@that.guru X-MailChannels-Auth-Id: mxroute X-MC-Loop-Signature: 1479574327020:1436667264 X-MC-Ingress-Time: 1479574327020 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=that.guru; s=default; h=References:In-Reply-To:Message-Id:Date:Subject:To:From:Sender: Reply-To:Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=H3zLfi7kEsC3ETn6XqukncBlsqYKQjeuxlEL02v5Uf0=; b=kxtMkuIy2hJu+dyh9qx3cRdT8A qu+H0Q3kQSp377VKvoHQEImDIbh7nfyEAzWDNjjbix9vecM1ZFGLNSYgaYBNH0DucT6PLGcbuVv17 lYXOWOAmhfDeWwQFHfqmzzawO+46pZ9qVNeBZT8by9V/w3n9GFco+mvrQY2WoNbnQdyCbNi2IoeRR q3lu6I//gxQ4vGc9eYiLCc/wFwvm6RbWdFnIzOtrFzYRdvwwtT3WoHeiTCiO/GWrMmEMDMxnqC4ue Dq45mUd98rFF7MJQRh5eUVGJEBRUhIWFDYmBNE//2bevWchYWd9lJ9zpPeIjtuacLYv07c/7udRwL hQd798wg==; From: Stephen Finucane To: patchwork@lists.ozlabs.org Subject: [PATCH v2 01/13] requirements: Test older versions of DRF Date: Sat, 19 Nov 2016 16:51:16 +0000 Message-Id: <1479574288-24171-2-git-send-email-stephen@that.guru> X-Mailer: git-send-email 2.7.4 In-Reply-To: <1479574288-24171-1-git-send-email-stephen@that.guru> References: <1479574288-24171-1-git-send-email-stephen@that.guru> X-OutGoing-Spam-Status: No, score=-10.0 X-AuthUser: stephen@that.guru X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" 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 --- 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')