--- /dev/null
+# Patchwork - automated patch tracking system
+# Copyright (C) 2018 Red Hat
+#
+# 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.generics import ListAPIView
+from rest_framework.serializers import SerializerMethodField
+
+from patchwork.api.base import BaseHyperlinkedModelSerializer
+from patchwork.api.base import PatchworkPermission
+from patchwork.api.embedded import PersonSerializer
+from patchwork.models import Comment
+
+
+class CommentListSerializer(BaseHyperlinkedModelSerializer):
+
+ subject = SerializerMethodField()
+ headers = SerializerMethodField()
+ submitter = PersonSerializer(read_only=True)
+
+ def get_subject(self, comment):
+ return email.parser.Parser().parsestr(comment.headers,
+ True).get('Subject', '')
+
+ def get_headers(self, comment):
+ headers = {}
+
+ if comment.headers:
+ parsed = email.parser.Parser().parsestr(comment.headers, True)
+ for key in parsed.keys():
+ headers[key] = parsed.get_all(key)
+ # Let's return a single string instead of a list if only one
+ # header with this key is present
+ if len(headers[key]) == 1:
+ headers[key] = headers[key][0]
+
+ return headers
+
+ class Meta:
+ model = Comment
+ fields = ('id', 'msgid', 'date', 'subject', 'submitter', 'content',
+ 'headers')
+ read_only_fields = fields
+
+
+class CommentList(ListAPIView):
+ """List comments"""
+
+ permission_classes = (PatchworkPermission,)
+ serializer_class = CommentListSerializer
+ search_fields = ('subject',)
+ ordering_fields = ('id', 'subject', 'date', 'submitter')
+ ordering = 'id'
+ lookup_url_kwarg = 'pk'
+
+ def get_queryset(self):
+ return Comment.objects.filter(
+ submission=self.kwargs['pk']
+ ).select_related('submitter')
from rest_framework.generics import ListAPIView
from rest_framework.generics import RetrieveAPIView
+from rest_framework.reverse import reverse
from rest_framework.serializers import SerializerMethodField
from patchwork.api.base import BaseHyperlinkedModelSerializer
class CoverLetterDetailSerializer(CoverLetterListSerializer):
headers = SerializerMethodField()
+ comments = SerializerMethodField()
+
+ def get_comments(self, cover):
+ return self.context.get('request').build_absolute_uri(
+ reverse('api-comment-list', kwargs={'pk': cover.id}))
def get_headers(self, instance):
headers = {}
class Meta:
model = CoverLetter
- fields = CoverLetterListSerializer.Meta.fields + ('headers', 'content')
+ fields = CoverLetterListSerializer.Meta.fields + (
+ 'headers', 'content', 'comments')
read_only_fields = fields
extra_kwargs = CoverLetterListSerializer.Meta.extra_kwargs
+ versioned_fields = {
+ '1.1': ('mbox', 'comments'),
+ }
class CoverLetterList(ListAPIView):
from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.relations import RelatedField
from rest_framework.reverse import reverse
-from rest_framework.serializers import HyperlinkedModelSerializer
from rest_framework.serializers import SerializerMethodField
+from patchwork.api.base import BaseHyperlinkedModelSerializer
from patchwork.api.base import PatchworkPermission
from patchwork.api.filters import PatchFilter
from patchwork.api.embedded import PersonSerializer
return State.objects.all()
-class PatchListSerializer(HyperlinkedModelSerializer):
+class PatchListSerializer(BaseHyperlinkedModelSerializer):
project = ProjectSerializer(read_only=True)
state = StateField()
headers = SerializerMethodField()
prefixes = SerializerMethodField()
+ comments = SerializerMethodField()
+
+ def get_comments(self, patch):
+ return self.context.get('request').build_absolute_uri(
+ reverse('api-comment-list', kwargs={'pk': patch.id}))
def get_headers(self, patch):
headers = {}
class Meta:
model = Patch
fields = PatchListSerializer.Meta.fields + (
- 'headers', 'content', 'diff', 'prefixes')
+ 'headers', 'content', 'diff', 'prefixes', 'comments')
read_only_fields = PatchListSerializer.Meta.read_only_fields + (
- 'headers', 'content', 'diff', 'prefixes')
+ 'headers', 'content', 'diff', 'prefixes', 'comments')
extra_kwargs = PatchListSerializer.Meta.extra_kwargs
+ versioned_fields = {
+ '1.1': ('comments', ),
+ }
class PatchList(ListAPIView):
if hasattr(self.submission, 'patch'):
self.submission.patch.refresh_tag_counts()
+ def is_editable(self, user):
+ return False
+
class Meta:
ordering = ['date']
unique_together = [('msgid', 'submission')]
--- /dev/null
+# Patchwork - automated patch tracking system
+# Copyright (C) 2018 Red Hat
+#
+# 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 unittest
+
+from django.conf import settings
+
+from patchwork.compat import NoReverseMatch
+from patchwork.compat import reverse
+from patchwork.tests.utils import create_comment
+from patchwork.tests.utils import create_cover
+from patchwork.tests.utils import create_patch
+from patchwork.tests.utils import SAMPLE_CONTENT
+
+if settings.ENABLE_REST_API:
+ from rest_framework import status
+ from rest_framework.test import APITestCase
+else:
+ # stub out APITestCase
+ from django.test import TestCase
+ APITestCase = TestCase # noqa
+
+
+@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
+class TestCoverComments(APITestCase):
+ @staticmethod
+ def api_url(cover, version=None):
+ kwargs = {}
+ if version:
+ kwargs['version'] = version
+ kwargs['pk'] = cover.id
+
+ return reverse('api-comment-list', kwargs=kwargs)
+
+ def assertSerialized(self, comment_obj, comment_json):
+ self.assertEqual(comment_obj.id, comment_json['id'])
+ self.assertEqual(comment_obj.submitter.id,
+ comment_json['submitter']['id'])
+ self.assertIn(SAMPLE_CONTENT, comment_json['content'])
+
+ def test_list(self):
+ cover_obj = create_cover()
+ resp = self.client.get(self.api_url(cover_obj))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertEqual(0, len(resp.data))
+
+ comment_obj = create_comment(submission=cover_obj)
+ resp = self.client.get(self.api_url(cover_obj))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertEqual(1, len(resp.data))
+ self.assertSerialized(comment_obj, resp.data[0])
+
+ create_comment(submission=cover_obj)
+ resp = self.client.get(self.api_url(cover_obj))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertEqual(2, len(resp.data))
+
+ # check we can't access comments using the old version of the API
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(self.api_url(cover_obj, version='1.0'))
+
+
+@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
+class TestPatchComments(APITestCase):
+ @staticmethod
+ def api_url(patch, version=None):
+ kwargs = {}
+ if version:
+ kwargs['version'] = version
+ kwargs['pk'] = patch.id
+
+ return reverse('api-comment-list', kwargs=kwargs)
+
+ def assertSerialized(self, comment_obj, comment_json):
+ self.assertEqual(comment_obj.id, comment_json['id'])
+ self.assertEqual(comment_obj.submitter.id,
+ comment_json['submitter']['id'])
+ self.assertIn(SAMPLE_CONTENT, comment_json['content'])
+
+ def test_list(self):
+ patch_obj = create_patch()
+ resp = self.client.get(self.api_url(patch_obj))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertEqual(0, len(resp.data))
+
+ comment_obj = create_comment(submission=patch_obj)
+ resp = self.client.get(self.api_url(patch_obj))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertEqual(1, len(resp.data))
+ self.assertSerialized(comment_obj, resp.data[0])
+
+ create_comment(submission=patch_obj)
+ resp = self.client.get(self.api_url(patch_obj))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertEqual(2, len(resp.data))
+
+ # check we can't access comments using the old version of the API
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(self.api_url(patch_obj, version='1.0'))
if item is None:
return reverse('api-cover-list', kwargs=kwargs)
- return reverse('api-cover-detail', args=[item], kwargs=kwargs)
+ kwargs['pk'] = item
+ return reverse('api-cover-detail', kwargs=kwargs)
def assertSerialized(self, cover_obj, cover_json):
self.assertEqual(cover_obj.id, cover_json['id'])
for key, value in parsed_headers.items():
self.assertIn(value, resp.data['headers'][key])
+ # test comments
+ resp = self.client.get(self.api_url(cover_obj.id))
+ self.assertIn('comments', resp.data)
+
+ # test old version of API
+ resp = self.client.get(self.api_url(cover_obj.id, version='1.0'))
+ self.assertNotIn('comments', resp.data)
+
def test_create_update_delete(self):
user = create_maintainer()
user.is_superuser = True
fixtures = ['default_tags']
@staticmethod
- def api_url(item=None):
+ def api_url(item=None, version=None):
+ kwargs = {}
+ if version:
+ kwargs['version'] = version
+
if item is None:
- return reverse('api-patch-list')
- return reverse('api-patch-detail', args=[item])
+ return reverse('api-patch-list', kwargs=kwargs)
+ kwargs['pk'] = item
+ return reverse('api-patch-detail', kwargs=kwargs)
def assertSerialized(self, patch_obj, patch_json):
self.assertEqual(patch_obj.id, patch_json['id'])
self.assertEqual(patch.diff, resp.data['diff'])
self.assertEqual(0, len(resp.data['tags']))
+ # test comments
+ resp = self.client.get(self.api_url(patch.id))
+ self.assertIn('comments', resp.data)
+
+ # test old version of API
+ resp = self.client.get(self.api_url(item=patch.id, version='1.0'))
+ self.assertNotIn('comments', resp.data)
+
def test_create(self):
"""Ensure creations are rejected."""
project = create_project()
from patchwork.api import bundle as api_bundle_views # noqa
from patchwork.api import check as api_check_views # noqa
+ from patchwork.api import comment as api_comment_views # noqa
from patchwork.api import cover as api_cover_views # noqa
from patchwork.api import event as api_event_views # noqa
from patchwork.api import index as api_index_views # noqa
name='api-event-list'),
]
+ api_1_1_patterns = [
+ url(r'^patches/(?P<pk>[^/]+)/comments/$',
+ api_comment_views.CommentList.as_view(),
+ name='api-comment-list'),
+ url(r'^covers/(?P<pk>[^/]+)/comments/$',
+ api_comment_views.CommentList.as_view(),
+ name='api-comment-list'),
+ ]
+
urlpatterns += [
url(r'^api/(?:(?P<version>(1.0|1.1))/)?', include(api_patterns)),
+ url(r'^api/(?:(?P<version>1.1)/)?', include(api_1_1_patterns)),
# token change
url(r'^user/generate-token/$', user_views.generate_token,
--- /dev/null
+---
+api:
+ - |
+ Links to related comments are now exposed when checking patch and cover
+ letter details. The comments themselves are then available via
+ ``/patches/{patchID}/comments`` and ``/covers/{coverID}/comments``
+ endpoints. Please note that comments are available only since API
+ version 1.1