diff options
-rw-r--r-- | patchwork/api/base.py | 26 | ||||
-rw-r--r-- | patchwork/api/check.py | 74 | ||||
-rw-r--r-- | patchwork/api/index.py | 36 | ||||
-rw-r--r-- | patchwork/api/patch.py | 86 | ||||
-rw-r--r-- | patchwork/api/person.py | 30 | ||||
-rw-r--r-- | patchwork/api/project.py | 56 | ||||
-rw-r--r-- | patchwork/api/user.py | 28 | ||||
-rw-r--r-- | patchwork/settings/base.py | 4 | ||||
-rw-r--r-- | patchwork/tests/test_rest_api.py | 80 | ||||
-rw-r--r-- | patchwork/urls.py | 63 | ||||
-rw-r--r-- | requirements-dev.txt | 1 | ||||
-rw-r--r-- | requirements-prod.txt | 1 | ||||
-rw-r--r-- | tox.ini | 1 |
13 files changed, 317 insertions, 169 deletions
diff --git a/patchwork/api/base.py b/patchwork/api/base.py index 3639ebe..dbd8148 100644 --- a/patchwork/api/base.py +++ b/patchwork/api/base.py @@ -18,10 +18,10 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from django.conf import settings +from django.shortcuts import get_object_or_404 from rest_framework import permissions from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet class LinkHeaderPagination(PageNumberPagination): @@ -53,11 +53,6 @@ class LinkHeaderPagination(PageNumberPagination): 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: @@ -65,14 +60,15 @@ class PatchworkPermission(permissions.BasePermission): 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 MultipleFieldLookupMixin(object): + """Enable multiple lookups fields.""" -class PatchworkViewSet(ModelViewSet): - pagination_class = LinkHeaderPagination + def get_object(self): + queryset = self.filter_queryset(self.get_queryset()) + filter_kwargs = {} + for field_name, field in zip( + self.lookup_fields, self.lookup_url_kwargs): + if self.kwargs[field]: + filter_kwargs[field_name] = self.kwargs[field] - def get_queryset(self): - return self.serializer_class.Meta.model.objects.all() + return get_object_or_404(queryset, **filter_kwargs) diff --git a/patchwork/api/check.py b/patchwork/api/check.py index 2fd681a..78c2141 100644 --- a/patchwork/api/check.py +++ b/patchwork/api/check.py @@ -17,15 +17,16 @@ # 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.generics import ListCreateAPIView +from rest_framework.generics import RetrieveAPIView 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 rest_framework.serializers import HyperlinkedModelSerializer +from rest_framework.serializers import HyperlinkedIdentityField -from patchwork.api.base import PatchworkViewSet +from patchwork.api.base import MultipleFieldLookupMixin from patchwork.models import Check from patchwork.models import Patch @@ -38,10 +39,29 @@ class CurrentPatchDefault(object): return self.patch -class CheckSerializer(ModelSerializer): +class CheckHyperlinkedIdentityField(HyperlinkedIdentityField): + + def get_url(self, obj, view_name, request, format): + # Unsaved objects will not yet have a valid URL. + if obj.pk is None: + return None + + return self.reverse( + view_name, + kwargs={ + 'patch_id': obj.patch.id, + 'check_id': obj.id, + }, + request=request, + format=format, + ) + + +class CheckSerializer(HyperlinkedModelSerializer): user = HyperlinkedRelatedField( - 'user-detail', read_only=True, default=CurrentUserDefault()) + 'api-user-detail', read_only=True, default=CurrentUserDefault()) patch = HiddenField(default=CurrentPatchDefault()) + url = CheckHyperlinkedIdentityField('api-check-detail') def run_validation(self, data): for val, label in Check.STATE_CHOICES: @@ -53,45 +73,39 @@ class CheckSerializer(ModelSerializer): 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 return data class Meta: model = Check - fields = ('patch', 'user', 'date', 'state', 'target_url', + fields = ('url', 'patch', 'user', 'date', 'state', 'target_url', 'context', 'description') read_only_fields = ('date',) + extra_kwargs = { + 'url': {'view_name': 'api-check-detail'}, + } + +class CheckMixin(object): -class CheckViewSet(PatchworkViewSet): + queryset = Check.objects.prefetch_related('patch', 'user') serializer_class = CheckSerializer - def not_allowed(self, request, **kwargs): - raise PermissionDenied() - update = not_allowed - partial_update = not_allowed - destroy = not_allowed +class CheckListCreate(CheckMixin, ListCreateAPIView): + """List or create checks.""" + + lookup_url_kwarg = 'patch_id' - def create(self, request, patch_pk): - p = Patch.objects.get(id=patch_pk) + def create(self, request, patch_id): + p = Patch.objects.get(id=patch_id) if not p.is_editable(request.user): raise PermissionDenied() request.patch = p - return super(CheckViewSet, self).create(request) + return super(CheckListCreate, 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) +class CheckDetail(CheckMixin, MultipleFieldLookupMixin, RetrieveAPIView): + """Show a check.""" - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + lookup_url_kwargs = ('patch_id', 'check_id') + lookup_fields = ('patch_id', 'id') diff --git a/patchwork/api/index.py b/patchwork/api/index.py new file mode 100644 index 0000000..58aeb87 --- /dev/null +++ b/patchwork/api/index.py @@ -0,0 +1,36 @@ +# 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.response import Response +from rest_framework.views import APIView + + +class IndexView(APIView): + + def get(self, request, format=None): + return Response({ + 'projects': request.build_absolute_uri( + reverse('api-project-list')), + 'users': request.build_absolute_uri(reverse('api-user-list')), + 'people': request.build_absolute_uri(reverse('api-person-list')), + 'patches': request.build_absolute_uri(reverse('api-patch-list')), + 'covers': request.build_absolute_uri(reverse('api-cover-list')), + 'series': request.build_absolute_uri(reverse('api-series-list')), + }) diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py index 811ba1e..8427450 100644 --- a/patchwork/api/patch.py +++ b/patchwork/api/patch.py @@ -19,30 +19,22 @@ import email.parser +from django.core.urlresolvers import reverse from rest_framework.serializers import HyperlinkedModelSerializer -from rest_framework.serializers import ListSerializer +from rest_framework.generics import ListAPIView +from rest_framework.generics import RetrieveUpdateAPIView from rest_framework.serializers import SerializerMethodField from patchwork.api.base import PatchworkPermission -from patchwork.api.base import PatchworkViewSet 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(HyperlinkedModelSerializer): +class PatchListSerializer(HyperlinkedModelSerializer): mbox = SerializerMethodField() state = SerializerMethodField() tags = SerializerMethodField() - headers = SerializerMethodField() check = SerializerMethodField() + checks = SerializerMethodField() def get_state(self, instance): return instance.state.name @@ -58,39 +50,65 @@ class PatchSerializer(HyperlinkedModelSerializer): else: return None - def get_headers(self, instance): - if instance.headers: - return - email.parser.Parser().parsestr(instance.headers, True) - def get_check(self, instance): return instance.combined_check_state - def to_representation(self, instance): - data = super(PatchSerializer, self).to_representation(instance) - data['checks'] = data['url'] + 'checks/' - return data + def get_checks(self, instance): + return self.context.get('request').build_absolute_uri( + reverse('api-check-list', kwargs={'patch_id': instance.id})) class Meta: model = Patch - list_serializer_class = PatchListSerializer fields = ('url', 'project', 'msgid', 'date', 'name', 'commit_ref', 'pull_url', 'state', 'archived', 'hash', 'submitter', - 'delegate', 'mbox', 'check', 'checks', 'tags', 'headers', - 'content', 'diff') - read_only_fields = ('project', 'name', 'date', 'submitter', 'diff', - 'content', 'hash', 'msgid') + 'delegate', 'mbox', 'check', 'checks', 'tags') + read_only_fields = ('project', 'msgid', 'date', 'name', 'hash', + 'submitter', 'mbox', 'mbox', 'series', 'check', + 'checks', 'tags') + extra_kwargs = { + 'url': {'view_name': 'api-patch-detail'}, + 'project': {'view_name': 'api-project-detail'}, + 'submitter': {'view_name': 'api-person-detail'}, + 'delegate': {'view_name': 'api-user-detail'}, + } + + +class PatchDetailSerializer(PatchListSerializer): + headers = SerializerMethodField() + + def get_headers(self, patch): + if patch.headers: + return email.parser.Parser().parsestr(patch.headers, True) + + class Meta: + model = Patch + fields = PatchListSerializer.Meta.fields + ( + 'headers', 'content', 'diff') + read_only_fields = PatchListSerializer.Meta.read_only_fields + ( + 'headers', 'content', 'diff') + extra_kwargs = PatchListSerializer.Meta.extra_kwargs + + +class PatchList(ListAPIView): + """List patches.""" + + permission_classes = (PatchworkPermission,) + serializer_class = PatchListSerializer + + def get_queryset(self): + return Patch.objects.all().with_tag_counts()\ + .prefetch_related('check_set')\ + .select_related('state', 'submitter', 'delegate')\ + .defer('content', 'diff', 'headers') + +class PatchDetail(RetrieveUpdateAPIView): + """Show a patch.""" -class PatchViewSet(PatchworkViewSet): permission_classes = (PatchworkPermission,) - serializer_class = PatchSerializer + serializer_class = PatchDetailSerializer def get_queryset(self): - qs = super(PatchViewSet, self).get_queryset().with_tag_counts()\ + return Patch.objects.all().with_tag_counts()\ .prefetch_related('check_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 index fe1e3b7..c84cff5 100644 --- a/patchwork/api/person.py +++ b/patchwork/api/person.py @@ -18,9 +18,10 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from rest_framework.serializers import HyperlinkedModelSerializer +from rest_framework.generics import ListAPIView +from rest_framework.generics import RetrieveAPIView +from rest_framework.permissions import IsAuthenticated -from patchwork.api.base import AuthenticatedReadOnly -from patchwork.api.base import PatchworkViewSet from patchwork.models import Person @@ -28,12 +29,27 @@ class PersonSerializer(HyperlinkedModelSerializer): class Meta: model = Person fields = ('url', 'name', 'email', 'user') + read_only_fields = fields + extra_kwargs = { + 'url': {'view_name': 'api-person-detail'}, + 'user': {'view_name': 'api-user-detail'}, + } -class PeopleViewSet(PatchworkViewSet): - permission_classes = (AuthenticatedReadOnly,) +class PersonMixin(object): + + queryset = Person.objects.prefetch_related('user') + permission_classes = (IsAuthenticated,) serializer_class = PersonSerializer - def get_queryset(self): - qs = super(PeopleViewSet, self).get_queryset() - return qs.prefetch_related('user') + +class PersonList(PersonMixin, ListAPIView): + """List users.""" + + pass + + +class PersonDetail(PersonMixin, RetrieveAPIView): + """Show a user.""" + + pass diff --git a/patchwork/api/project.py b/patchwork/api/project.py index af2aea0..14bc73d 100644 --- a/patchwork/api/project.py +++ b/patchwork/api/project.py @@ -17,11 +17,13 @@ # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +from django.shortcuts import get_object_or_404 +from rest_framework.generics import ListAPIView +from rest_framework.generics import RetrieveUpdateAPIView from rest_framework.serializers import CharField from rest_framework.serializers import HyperlinkedModelSerializer from patchwork.api.base import PatchworkPermission -from patchwork.api.base import PatchworkViewSet from patchwork.models import Project @@ -34,26 +36,44 @@ class ProjectSerializer(HyperlinkedModelSerializer): model = Project fields = ('url', 'name', 'link_name', 'list_id', 'list_email', 'web_url', 'scm_url', 'webscm_url') + read_only_fields = ('name', 'maintainers') + extra_kwargs = { + 'url': {'view_name': 'api-project-detail'}, + } -class ProjectViewSet(PatchworkViewSet): +class ProjectMixin(object): + + queryset = Project.objects.all() 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() + def get_object(self): + queryset = self.filter_queryset(self.get_queryset()) + + assert 'pk' in self.kwargs + 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) + obj = queryset.get(id=int(self.kwargs['pk'])) + except (ValueError, Project.DoesNotExist): + obj = get_object_or_404(queryset, linkname=self.kwargs['pk']) + + # NOTE(stephenfin): We must do this to make sure the 'url' + # field is populated correctly + self.kwargs['pk'] = obj.id + + self.check_object_permissions(self.request, obj) + + return obj + + +class ProjectList(ProjectMixin, ListAPIView): + """List projects.""" + + pass + + +class ProjectDetail(ProjectMixin, RetrieveUpdateAPIView): + """Show a project.""" + + pass diff --git a/patchwork/api/user.py b/patchwork/api/user.py index 3a4ae1a..8fe3e74 100644 --- a/patchwork/api/user.py +++ b/patchwork/api/user.py @@ -18,18 +18,36 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from django.contrib.auth.models import User +from rest_framework.generics import ListAPIView +from rest_framework.generics import RetrieveAPIView +from rest_framework.permissions import IsAuthenticated from rest_framework.serializers import HyperlinkedModelSerializer -from patchwork.api.base import AuthenticatedReadOnly -from patchwork.api.base import PatchworkViewSet - class UserSerializer(HyperlinkedModelSerializer): + class Meta: model = User fields = ('url', 'username', 'first_name', 'last_name', 'email') + extra_kwargs = { + 'url': {'view_name': 'api-user-detail'}, + } + +class UserMixin(object): -class UserViewSet(PatchworkViewSet): - permission_classes = (AuthenticatedReadOnly,) + queryset = User.objects.all() + permission_classes = (IsAuthenticated,) serializer_class = UserSerializer + + +class UserList(UserMixin, ListAPIView): + """List users.""" + + pass + + +class UserDetail(UserMixin, RetrieveAPIView): + """Show a user.""" + + pass diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py index a32710f..b7b10c3 100644 --- a/patchwork/settings/base.py +++ b/patchwork/settings/base.py @@ -140,7 +140,9 @@ except ImportError: REST_FRAMEWORK = { - 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning' + 'DEFAULT_VERSIONING_CLASS': + 'rest_framework.versioning.NamespaceVersioning', + 'DEFAULT_PAGINATION_CLASS': 'patchwork.api.base.LinkHeaderPagination', } # diff --git a/patchwork/tests/test_rest_api.py b/patchwork/tests/test_rest_api.py index ddce4d7..469fd26 100644 --- a/patchwork/tests/test_rest_api.py +++ b/patchwork/tests/test_rest_api.py @@ -48,8 +48,8 @@ class TestProjectAPI(APITestCase): @staticmethod def api_url(item=None): if item is None: - return reverse('api_1.0:project-list') - return reverse('api_1.0:project-detail', args=[item]) + return reverse('api-project-list') + return reverse('api-project-detail', args=[item]) def test_list_simple(self): """Validate we can list the default test project.""" @@ -89,7 +89,7 @@ class TestProjectAPI(APITestCase): resp = self.client.post( self.api_url(), {'linkname': 'l', 'name': 'n', 'listid': 'l', 'listemail': 'e'}) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) def test_anonymous_update(self): """Ensure anonymous "PATCH" operations are rejected.""" @@ -104,7 +104,7 @@ class TestProjectAPI(APITestCase): project = create_project() resp = self.client.delete(self.api_url(project.id)) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) def test_create(self): """Ensure creations are rejected.""" @@ -117,7 +117,7 @@ class TestProjectAPI(APITestCase): resp = self.client.post( self.api_url(), {'linkname': 'l', 'name': 'n', 'listid': 'l', 'listemail': 'e'}) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) def test_update(self): """Ensure updates can be performed maintainers.""" @@ -147,7 +147,7 @@ class TestProjectAPI(APITestCase): user.save() self.client.force_authenticate(user=user) resp = self.client.delete(self.api_url(project.id)) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) self.assertEqual(1, Project.objects.all().count()) @@ -157,8 +157,8 @@ class TestPersonAPI(APITestCase): @staticmethod def api_url(item=None): if item is None: - return reverse('api_1.0:person-list') - return reverse('api_1.0:person-detail', args=[item]) + return reverse('api-person-list') + return reverse('api-person-detail', args=[item]) def test_anonymous_list(self): """The API should reject anonymous users.""" @@ -196,14 +196,14 @@ class TestPersonAPI(APITestCase): self.client.force_authenticate(user=user) resp = self.client.delete(self.api_url(user.id)) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) resp = self.client.patch(self.api_url(user.id), {'email': 'foo@f.com'}) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) resp = self.client.post(self.api_url(), {'email': 'foo@f.com'}) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) @unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API') @@ -212,8 +212,8 @@ class TestUserAPI(APITestCase): @staticmethod def api_url(item=None): if item is None: - return reverse('api_1.0:user-list') - return reverse('api_1.0:user-detail', args=[item]) + return reverse('api-user-list') + return reverse('api-user-detail', args=[item]) def test_anonymous_list(self): """The API should reject anonymous users.""" @@ -239,13 +239,13 @@ class TestUserAPI(APITestCase): self.client.force_authenticate(user=user) resp = self.client.delete(self.api_url(1)) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) resp = self.client.patch(self.api_url(1), {'email': 'foo@f.com'}) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) resp = self.client.post(self.api_url(), {'email': 'foo@f.com'}) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) @unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API') @@ -255,8 +255,8 @@ class TestPatchAPI(APITestCase): @staticmethod def api_url(item=None): if item is None: - return reverse('api_1.0:patch-list') - return reverse('api_1.0:patch-detail', args=[item]) + return reverse('api-patch-list') + return reverse('api-patch-detail', args=[item]) def test_list_simple(self): """Validate we can list a patch.""" @@ -265,6 +265,8 @@ class TestPatchAPI(APITestCase): self.assertEqual(0, len(resp.data)) patch_obj = create_patch() + + # anonymous user resp = self.client.get(self.api_url()) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(1, len(resp.data)) @@ -274,7 +276,7 @@ class TestPatchAPI(APITestCase): self.assertNotIn('headers', patch_rsp) self.assertNotIn('diff', patch_rsp) - # test while authenticated + # authenticated user user = create_user() self.client.force_authenticate(user=user) resp = self.client.get(self.api_url()) @@ -284,7 +286,7 @@ class TestPatchAPI(APITestCase): self.assertEqual(patch_obj.name, patch_rsp['name']) def test_detail(self): - """Validate we can get a specific project.""" + """Validate we can get a specific patch.""" patch = create_patch() resp = self.client.get(self.api_url(patch.id)) @@ -318,7 +320,7 @@ class TestPatchAPI(APITestCase): } resp = self.client.post(self.api_url(), patch) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) def test_anonymous_update(self): """Ensure anonymous "PATCH" operations are rejected.""" @@ -339,7 +341,7 @@ class TestPatchAPI(APITestCase): patch_url = self.api_url(patch.id) resp = self.client.delete(patch_url) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) def test_create(self): """Ensure creations are rejected.""" @@ -359,7 +361,7 @@ class TestPatchAPI(APITestCase): } resp = self.client.post(self.api_url(), patch) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) def test_update(self): """Ensure updates can be performed by maintainers.""" @@ -391,7 +393,7 @@ class TestPatchAPI(APITestCase): self.client.force_authenticate(user=user) resp = self.client.delete(self.api_url(patch.id)) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) self.assertEqual(1, Patch.objects.all().count()) @@ -399,13 +401,17 @@ class TestPatchAPI(APITestCase): class TestCheckAPI(APITestCase): fixtures = ['default_tags'] + def api_url(self, item=None): + if item is None: + return reverse('api-check-list', args=[self.patch.id]) + return reverse('api-check-detail', kwargs={ + 'patch_id': self.patch.id, 'check_id': item.id}) + def setUp(self): super(TestCheckAPI, self).setUp() project = create_project() self.user = create_maintainer(project) self.patch = create_patch(project=project) - self.urlbase = reverse('api_1.0:patch-detail', args=[self.patch.id]) - self.urlbase += 'checks/' def _create_check(self): values = { @@ -416,13 +422,13 @@ class TestCheckAPI(APITestCase): def test_list_simple(self): """Validate we can list checks on a patch.""" - resp = self.client.get(self.urlbase) + resp = self.client.get(self.api_url()) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(0, len(resp.data)) check_obj = self._create_check() - resp = self.client.get(self.urlbase) + resp = self.client.get(self.api_url()) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(1, len(resp.data)) check_rsp = resp.data[0] @@ -434,7 +440,7 @@ class TestCheckAPI(APITestCase): def test_detail(self): """Validate we can get a specific check.""" check = self._create_check() - resp = self.client.get(self.urlbase + str(check.id) + '/') + resp = self.client.get(self.api_url(check)) self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertEqual(check.target_url, resp.data['target_url']) @@ -446,13 +452,13 @@ class TestCheckAPI(APITestCase): self.client.force_authenticate(user=self.user) # update - resp = self.client.patch( - self.urlbase + str(check.id) + '/', {'target_url': 'fail'}) - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + resp = self.client.patch(self.api_url(check), + {'target_url': 'fail'}) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) # delete - resp = self.client.delete(self.urlbase + str(check.id) + '/') - self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + resp = self.client.delete(self.api_url(check)) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) def test_create(self): """Ensure creations can be performed by user of patch.""" @@ -464,13 +470,13 @@ class TestCheckAPI(APITestCase): } self.client.force_authenticate(user=self.user) - resp = self.client.post(self.urlbase, check) + resp = self.client.post(self.api_url(), check) self.assertEqual(status.HTTP_201_CREATED, resp.status_code) self.assertEqual(1, Check.objects.all().count()) user = create_user() self.client.force_authenticate(user=user) - resp = self.client.post(self.urlbase, check) + resp = self.client.post(self.api_url(), check) self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) def test_create_invalid(self): @@ -483,6 +489,6 @@ class TestCheckAPI(APITestCase): } self.client.force_authenticate(user=self.user) - resp = self.client.post(self.urlbase, check) + resp = self.client.post(self.api_url(), check) self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code) self.assertEqual(0, Check.objects.all().count()) diff --git a/patchwork/urls.py b/patchwork/urls.py index 6671837..56db24c 100644 --- a/patchwork/urls.py +++ b/patchwork/urls.py @@ -152,29 +152,54 @@ if settings.ENABLE_REST_API: raise RuntimeError( 'djangorestframework must be installed to enable the REST API.') - 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') + from patchwork.api import check as api_check_views + from patchwork.api import index as api_index_views + from patchwork.api import patch as api_patch_views + from patchwork.api import person as api_person_views + from patchwork.api import project as api_project_views + from patchwork.api import user as api_user_views + + api_patterns = [ + url(r'^$', + api_index_views.IndexView.as_view(), + name='api-index'), + url(r'^users/$', + api_user_views.UserList.as_view(), + name='api-user-list'), + url(r'^users/(?P<pk>[^/]+)/$', + api_user_views.UserDetail.as_view(), + name='api-user-detail'), + url(r'^people/$', + api_person_views.PersonList.as_view(), + name='api-person-list'), + url(r'^people/(?P<pk>[^/]+)/$', + api_person_views.PersonDetail.as_view(), + name='api-person-detail'), + url(r'^patches/$', + api_patch_views.PatchList.as_view(), + name='api-patch-list'), + url(r'^patches/(?P<pk>[^/]+)/$', + api_patch_views.PatchDetail.as_view(), + name='api-patch-detail'), + url(r'^patches/(?P<patch_id>[^/]+)/checks/$', + api_check_views.CheckListCreate.as_view(), + name='api-check-list'), + url(r'^patches/(?P<patch_id>[^/]+)/checks/(?P<check_id>[^/]+)/$', + api_check_views.CheckDetail.as_view(), + name='api-check-detail'), + url(r'^projects/$', + api_project_views.ProjectList.as_view(), + name='api-project-list'), + url(r'^projects/(?P<pk>[^/]+)/$', + api_project_views.ProjectDetail.as_view(), + name='api-project-detail'), + ] 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')), + url(r'^api/1.0/', include(api_patterns)), ] + # redirect from old urls if settings.COMPAT_REDIR: urlpatterns += [ diff --git a/requirements-dev.txt b/requirements-dev.txt index fdfe00c..0f8f08b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ Django>=1.8,<1.11 djangorestframework>=3.5,<3.6 -drf-nested-routers>=0.11.1,<0.12 -r requirements-test.txt diff --git a/requirements-prod.txt b/requirements-prod.txt index 57e6fce..64d4339 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -1,5 +1,4 @@ Django>=1.8,<1.11 djangorestframework>=3.5,<3.6 -drf-nested-routers>=0.11.1,<0.12 psycopg2>2.6,<2.7 sqlparse @@ -14,7 +14,6 @@ deps = django19: django>=1.9,<1.10 django110: django>=1.10,<1.11 django{18,19,110}: djangorestframework>=3.5,<3.6 - drf-nested-routers>=0.11.1,<0.12 setenv = DJANGO_SETTINGS_MODULE = patchwork.settings.dev PYTHONDONTWRITEBYTECODE = 1 |