diff options
-rw-r--r-- | patchwork/api/bundle.py | 80 | ||||
-rw-r--r-- | patchwork/api/filters.py | 8 | ||||
-rw-r--r-- | patchwork/api/index.py | 1 | ||||
-rw-r--r-- | patchwork/tests/test_rest_api.py | 77 | ||||
-rw-r--r-- | patchwork/urls.py | 7 |
5 files changed, 173 insertions, 0 deletions
diff --git a/patchwork/api/bundle.py b/patchwork/api/bundle.py new file mode 100644 index 0000000..5fa79b8 --- /dev/null +++ b/patchwork/api/bundle.py @@ -0,0 +1,80 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2017 Stephen Finucane <stephen@that.guru> +# +# 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.db.models import Q +from rest_framework.generics import ListAPIView +from rest_framework.generics import RetrieveAPIView +from rest_framework.serializers import HyperlinkedModelSerializer +from rest_framework.serializers import SerializerMethodField + +from patchwork.api.base import PatchworkPermission +from patchwork.api.filters import BundleFilter +from patchwork.models import Bundle + + +class BundleSerializer(HyperlinkedModelSerializer): + + mbox = SerializerMethodField() + + def get_mbox(self, instance): + request = self.context.get('request') + return request.build_absolute_uri(instance.get_mbox_url()) + + class Meta: + model = Bundle + fields = ('id', 'url', 'project', 'name', 'owner', 'patches', + 'public', 'mbox') + read_only_fields = ('owner', 'patches', 'mbox') + extra_kwargs = { + 'url': {'view_name': 'api-bundle-detail'}, + 'project': {'view_name': 'api-project-detail'}, + 'owner': {'view_name': 'api-user-detail'}, + 'patches': {'view_name': 'api-patch-detail'}, + } + + +class BundleMixin(object): + + permission_classes = (PatchworkPermission,) + serializer_class = BundleSerializer + + def get_queryset(self): + if not self.request.user.is_anonymous(): + bundle_filter = Q(owner=self.request.user) | Q(public=True) + else: + bundle_filter = Q(public=True) + + return Bundle.objects\ + .filter(bundle_filter)\ + .prefetch_related('patches',)\ + .select_related('owner', 'project') + + +class BundleList(BundleMixin, ListAPIView): + """List bundles.""" + + filter_class = BundleFilter + search_fields = ('name',) + ordering_fields = ('id', 'name', 'owner') + + +class BundleDetail(BundleMixin, RetrieveAPIView): + """Show a bundle.""" + + pass diff --git a/patchwork/api/filters.py b/patchwork/api/filters.py index 0f2e6e9..eff7ceb 100644 --- a/patchwork/api/filters.py +++ b/patchwork/api/filters.py @@ -20,6 +20,7 @@ from django_filters import FilterSet from django_filters import IsoDateTimeFilter +from patchwork.models import Bundle from patchwork.models import Check from patchwork.models import CoverLetter from patchwork.models import Event @@ -68,3 +69,10 @@ class EventFilter(FilterSet): class Meta: model = Event fields = ('project', 'series', 'patch', 'cover') + + +class BundleFilter(FilterSet): + + class Meta: + model = Bundle + fields = ('project', 'owner', 'public') diff --git a/patchwork/api/index.py b/patchwork/api/index.py index 210c32e..513e8b6 100644 --- a/patchwork/api/index.py +++ b/patchwork/api/index.py @@ -34,4 +34,5 @@ class IndexView(APIView): 'covers': request.build_absolute_uri(reverse('api-cover-list')), 'series': request.build_absolute_uri(reverse('api-series-list')), 'events': request.build_absolute_uri(reverse('api-event-list')), + 'bundles': request.build_absolute_uri(reverse('api-bundle-list')), }) diff --git a/patchwork/tests/test_rest_api.py b/patchwork/tests/test_rest_api.py index b6e6144..3a84f0f 100644 --- a/patchwork/tests/test_rest_api.py +++ b/patchwork/tests/test_rest_api.py @@ -26,6 +26,7 @@ from django.core.urlresolvers import reverse from patchwork.models import Check from patchwork.models import Patch from patchwork.models import Project +from patchwork.tests.utils import create_bundle from patchwork.tests.utils import create_check from patchwork.tests.utils import create_cover from patchwork.tests.utils import create_maintainer @@ -667,3 +668,79 @@ class TestCheckAPI(APITestCase): resp = self.client.delete(self.api_url(check)) self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) + + +@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API') +class TestBundleAPI(APITestCase): + fixtures = ['default_tags'] + + @staticmethod + def api_url(item=None): + if item is None: + return reverse('api-bundle-list') + return reverse('api-bundle-detail', args=[item]) + + def assertSerialized(self, bundle_obj, bundle_json): + self.assertEqual(bundle_obj.id, bundle_json['id']) + self.assertEqual(bundle_obj.name, bundle_json['name']) + self.assertEqual(bundle_obj.public, bundle_json['public']) + self.assertIn(bundle_obj.get_mbox_url(), bundle_json['mbox']) + self.assertEqual(bundle_obj.patches.count(), + len(bundle_json['patches'])) + self.assertIn(TestUserAPI.api_url(bundle_obj.owner.id), + bundle_json['owner']) + self.assertIn(TestProjectAPI.api_url(bundle_obj.project.id), + bundle_json['project']) + + def test_list(self): + """Validate we can list bundles.""" + resp = self.client.get(self.api_url()) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(0, len(resp.data)) + + user = create_user() + bundle_public = create_bundle(public=True, owner=user) + bundle_private = create_bundle(public=False, owner=user) + + # anonymous users + # should only see the public bundle + resp = self.client.get(self.api_url()) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(1, len(resp.data)) + bundle_rsp = resp.data[0] + self.assertSerialized(bundle_public, bundle_rsp) + + # authenticated user + # should see the public and private bundle + self.client.force_authenticate(user=user) + resp = self.client.get(self.api_url()) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(2, len(resp.data)) + for bundle_rsp, bundle_obj in zip( + resp.data, [bundle_public, bundle_private]): + self.assertSerialized(bundle_obj, bundle_rsp) + + def test_detail(self): + """Validate we can get a specific bundle.""" + user = create_user() + bundle = create_bundle(public=True) + + resp = self.client.get(self.api_url(bundle.id)) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertSerialized(bundle, resp.data) + + def test_create_update_delete(self): + """Ensure creates, updates and deletes aren't allowed""" + user = create_maintainer() + user.is_superuser = True + user.save() + self.client.force_authenticate(user=user) + + resp = self.client.post(self.api_url(), {'email': 'foo@f.com'}) + 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_405_METHOD_NOT_ALLOWED, resp.status_code) + + resp = self.client.delete(self.api_url(1)) + self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code) diff --git a/patchwork/urls.py b/patchwork/urls.py index 09b8b31..57d5cd7 100644 --- a/patchwork/urls.py +++ b/patchwork/urls.py @@ -158,6 +158,7 @@ if settings.ENABLE_REST_API: raise RuntimeError( 'djangorestframework must be installed to enable the REST API.') + from patchwork.api import bundle as api_bundle_views from patchwork.api import check as api_check_views from patchwork.api import cover as api_cover_views from patchwork.api import event as api_event_views @@ -208,6 +209,12 @@ if settings.ENABLE_REST_API: url(r'^series/(?P<pk>[^/]+)/$', api_series_views.SeriesDetail.as_view(), name='api-series-detail'), + url(r'^bundles/$', + api_bundle_views.BundleList.as_view(), + name='api-bundle-list'), + url(r'^bundles/(?P<pk>[^/]+)/$', + api_bundle_views.BundleDetail.as_view(), + name='api-bundle-detail'), url(r'^projects/$', api_project_views.ProjectList.as_view(), name='api-project-list'), |