This exposes patches via the REST API. Security Constraints: * Anyone (logged in or not) can read all objects. * No one can create/delete objects. * Project maintainers are allowed to update (ie "patch" attributes)
NOTE: Patch.save was overridden incorrectly and had to be fixed to work with DRF. Signed-off-by: Andy Doan <[email protected]> --- patchwork/models.py | 4 +- patchwork/rest_serializers.py | 39 +++++++++++++- patchwork/tests/test_rest_api.py | 113 ++++++++++++++++++++++++++++++++++++++- patchwork/views/rest_api.py | 19 ++++++- 4 files changed, 168 insertions(+), 7 deletions(-) diff --git a/patchwork/models.py b/patchwork/models.py index 4d454c7..6324273 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -359,14 +359,14 @@ class Patch(Submission): for tag in tags: self._set_tag(tag, counter[tag]) - def save(self): + def save(self, **kwargs): if not hasattr(self, 'state') or not self.state: self.state = get_default_initial_patch_state() if self.hash is None and self.diff is not None: self.hash = hash_patch(self.diff).hexdigest() - super(Patch, self).save() + super(Patch, self).save(**kwargs) self.refresh_tag_counts() diff --git a/patchwork/rest_serializers.py b/patchwork/rest_serializers.py index 0e2e371..9558d45 100644 --- a/patchwork/rest_serializers.py +++ b/patchwork/rest_serializers.py @@ -17,9 +17,14 @@ # along with Patchwork; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from patchwork.models import Person, Project +import email.parser -from rest_framework.serializers import ModelSerializer +from django.core.urlresolvers import reverse + +from patchwork.models import Patch, Person, Project + +from rest_framework.serializers import ( + ListSerializer, ModelSerializer) class PersonSerializer(ModelSerializer): @@ -36,3 +41,33 @@ class PersonSerializer(ModelSerializer): class ProjectSerializer(ModelSerializer): class Meta: model = Project + + +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(ModelSerializer): + class Meta: + model = Patch + list_serializer_class = PatchListSerializer + read_only_fields = ('project', 'name', 'date', 'submitter', 'diff', + 'content', 'hash', 'msgid') + + def to_representation(self, instance): + request = self.context.get('request', None) + data = super(PatchSerializer, self).to_representation(instance) + + data['project'] = request.build_absolute_uri( + reverse('api_1.0:project-detail', args=[data['project']])) + data['submitter'] = request.build_absolute_uri( + reverse('api_1.0:person-detail', args=[data['submitter']])) + headers = data.get('headers') + if headers: + data['headers'] = email.parser.Parser().parsestr(headers, True) + return data diff --git a/patchwork/tests/test_rest_api.py b/patchwork/tests/test_rest_api.py index 3f98595..7107dfa 100644 --- a/patchwork/tests/test_rest_api.py +++ b/patchwork/tests/test_rest_api.py @@ -25,8 +25,9 @@ from django.core.urlresolvers import reverse from rest_framework import status from rest_framework.test import APITestCase -from patchwork.models import Project -from patchwork.tests.utils import defaults, create_maintainer, create_user +from patchwork.models import Patch, Project +from patchwork.tests.utils import ( + defaults, create_maintainer, create_user, create_patches, make_msgid) @unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API') @@ -157,3 +158,111 @@ class TestPersonAPI(APITestCase): resp = self.client.post(self.api_url(), {'email': '[email protected]'}) self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + + [email protected](settings.ENABLE_REST_API, 'requires ENABLE_REST_API') +class TestPatchAPI(APITestCase): + fixtures = ['default_states'] + + @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]) + + def test_list_simple(self): + """Validate we can list a patch.""" + patches = create_patches() + resp = self.client.get(self.api_url()) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(1, len(resp.data)) + patch = resp.data[0] + self.assertEqual(patches[0].name, patch['name']) + + # test while authenticated + user = create_user() + self.client.force_authenticate(user=user) + resp = self.client.get(self.api_url()) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(1, len(resp.data)) + patch = resp.data[0] + self.assertEqual(patches[0].name, patch['name']) + + def test_get(self): + """Validate we can get a specific project.""" + patches = create_patches() + resp = self.client.get(self.api_url(patches[0].id)) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + self.assertEqual(patches[0].name, resp.data['name']) + self.assertIn(TestProjectAPI.api_url(patches[0].project.id), + resp.data['project']) + self.assertEqual(patches[0].msgid, resp.data['msgid']) + self.assertEqual(patches[0].diff, resp.data['diff']) + self.assertIn(TestPersonAPI.api_url(patches[0].submitter.id), + resp.data['submitter']) + self.assertEqual(patches[0].state.id, resp.data['state']) + + def test_anonymous_writes(self): + """Ensure anonymous "write" operations are rejected.""" + patches = create_patches() + patch_url = self.api_url(patches[0].id) + resp = self.client.get(patch_url) + patch = resp.data + patch['msgid'] = 'foo' + patch['name'] = 'this should fail' + + # create + resp = self.client.post(self.api_url(), patch) + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + # update + resp = self.client.patch(patch_url, {'name': 'foo'}) + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + # delete + resp = self.client.delete(patch_url) + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + + def test_create(self): + """Ensure creations are rejected.""" + create_patches() + patch = { + 'project': defaults.project.id, + 'submitter': defaults.patch_author_person.id, + 'msgid': make_msgid(), + 'name': 'test-create-patch', + 'diff': 'patch diff', + } + + user = create_maintainer(defaults.project) + user.is_superuser = True + user.save() + self.client.force_authenticate(user=user) + resp = self.client.post(self.api_url(), patch) + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + + def test_update(self): + """Ensure updates can be performed maintainers.""" + patches = create_patches() + + # A maintainer can update + user = create_maintainer(defaults.project) + self.client.force_authenticate(user=user) + resp = self.client.patch(self.api_url(patches[0].id), {'state': 2}) + self.assertEqual(status.HTTP_200_OK, resp.status_code) + + # A normal user can't + user = create_user() + self.client.force_authenticate(user=user) + resp = self.client.patch(self.api_url(patches[0].id), {'state': 2}) + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + + def test_delete(self): + """Ensure deletions are rejected.""" + patches = create_patches() + + user = create_maintainer(defaults.project) + user.is_superuser = True + user.save() + self.client.force_authenticate(user=user) + resp = self.client.delete(self.api_url(patches[0].id)) + self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + self.assertEqual(1, Patch.objects.all().count()) diff --git a/patchwork/views/rest_api.py b/patchwork/views/rest_api.py index dad9724..1e03c68 100644 --- a/patchwork/views/rest_api.py +++ b/patchwork/views/rest_api.py @@ -19,7 +19,8 @@ from django.conf import settings -from patchwork.rest_serializers import ProjectSerializer, PersonSerializer +from patchwork.rest_serializers import ( + PatchSerializer, ProjectSerializer, PersonSerializer) from rest_framework import permissions from rest_framework.pagination import PageNumberPagination @@ -77,6 +78,21 @@ class PatchworkViewSet(ModelViewSet): return self.serializer_class.Meta.model.objects.all() +class PatchViewSet(PatchworkViewSet): + permission_classes = (PatchworkPermission,) + serializer_class = PatchSerializer + + def get_queryset(self): + qs = super(PatchViewSet, self).get_queryset( + ).prefetch_related( + 'check_set', 'tags' + ).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 PeopleViewSet(PatchworkViewSet): permission_classes = (AuthenticatedReadOnly, ) serializer_class = PersonSerializer @@ -92,5 +108,6 @@ class ProjectViewSet(PatchworkViewSet): router = DefaultRouter() +router.register('patches', PatchViewSet, 'patch') router.register('people', PeopleViewSet, 'person') router.register('projects', ProjectViewSet, 'project') -- 2.7.4 _______________________________________________ Patchwork mailing list [email protected] https://lists.ozlabs.org/listinfo/patchwork
