From patchwork Wed Apr 25 17:33:27 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Veronika Kabatova X-Patchwork-Id: 904631 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [203.11.71.2]) (using TLSv1.2 with cipher ADH-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 40WS3d1s3nz9rxs for ; Thu, 26 Apr 2018 03:33:49 +1000 (AEST) Authentication-Results: ozlabs.org; dmarc=fail (p=none dis=none) header.from=redhat.com Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 40WS3d0FnkzF24Y for ; Thu, 26 Apr 2018 03:33:49 +1000 (AEST) Authentication-Results: lists.ozlabs.org; dmarc=pass (p=none dis=none) header.from=redhat.com X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (mailfrom) smtp.mailfrom=redhat.com (client-ip=66.187.233.73; helo=mx1.redhat.com; envelope-from=vkabatov@redhat.com; receiver=) Authentication-Results: lists.ozlabs.org; dmarc=pass (p=none dis=none) header.from=redhat.com Received: from mx1.redhat.com (mx3-rdu2.redhat.com [66.187.233.73]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 40WS3T2Ss3zDrDT for ; Thu, 26 Apr 2018 03:33:41 +1000 (AEST) Received: from smtp.corp.redhat.com (int-mx04.intmail.prod.int.rdu2.redhat.com [10.11.54.4]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id C14D2A200C for ; Wed, 25 Apr 2018 17:33:37 +0000 (UTC) Received: from vkabatova.usersys.redhat.com (unknown [10.40.205.36]) by smtp.corp.redhat.com (Postfix) with ESMTP id 055AD2023239; Wed, 25 Apr 2018 17:33:36 +0000 (UTC) From: vkabatov@redhat.com To: patchwork@lists.ozlabs.org Subject: [PATCH v4] api: Add comments to patch and cover endpoints Date: Wed, 25 Apr 2018 19:33:27 +0200 Message-Id: <20180425173327.21728-1-vkabatov@redhat.com> X-Scanned-By: MIMEDefang 2.78 on 10.11.54.4 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.11.55.2]); Wed, 25 Apr 2018 17:33:37 +0000 (UTC) X-Greylist: inspected by milter-greylist-4.5.16 (mx1.redhat.com [10.11.55.2]); Wed, 25 Apr 2018 17:33:37 +0000 (UTC) for IP:'10.11.54.4' DOMAIN:'int-mx04.intmail.prod.int.rdu2.redhat.com' HELO:'smtp.corp.redhat.com' FROM:'vkabatov@redhat.com' RCPT:'' X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.26 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" From: Veronika Kabatova Signed-off-by: Veronika Kabatova --- patchwork/api/comment.py | 75 ++++++++++++++ patchwork/api/cover.py | 13 ++- patchwork/api/patch.py | 16 ++- patchwork/models.py | 3 + patchwork/tests/api/test_comment.py | 115 +++++++++++++++++++++ patchwork/tests/api/test_cover.py | 11 +- patchwork/tests/api/test_patch.py | 19 +++- patchwork/urls.py | 11 ++ .../notes/comments-api-b7dff6ee4ce04c9b.yaml | 8 ++ 9 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 patchwork/api/comment.py create mode 100644 patchwork/tests/api/test_comment.py create mode 100644 releasenotes/notes/comments-api-b7dff6ee4ce04c9b.yaml diff --git a/patchwork/api/comment.py b/patchwork/api/comment.py new file mode 100644 index 0000000..fbb7cc8 --- /dev/null +++ b/patchwork/api/comment.py @@ -0,0 +1,75 @@ +# 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') diff --git a/patchwork/api/cover.py b/patchwork/api/cover.py index fc7ae97..19f0f65 100644 --- a/patchwork/api/cover.py +++ b/patchwork/api/cover.py @@ -21,6 +21,7 @@ import email.parser 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 @@ -58,6 +59,11 @@ class CoverLetterListSerializer(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): if instance.headers: @@ -65,9 +71,14 @@ class CoverLetterDetailSerializer(CoverLetterListSerializer): 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): diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py index 115feff..feb7d80 100644 --- a/patchwork/api/patch.py +++ b/patchwork/api/patch.py @@ -24,9 +24,9 @@ from rest_framework.generics import 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 @@ -75,7 +75,7 @@ class StateField(RelatedField): return State.objects.all() -class PatchListSerializer(HyperlinkedModelSerializer): +class PatchListSerializer(BaseHyperlinkedModelSerializer): project = ProjectSerializer(read_only=True) state = StateField() @@ -121,6 +121,11 @@ class PatchDetailSerializer(PatchListSerializer): 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): if patch.headers: @@ -132,10 +137,13 @@ class PatchDetailSerializer(PatchListSerializer): 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): diff --git a/patchwork/models.py b/patchwork/models.py index f91b994..67c2d3a 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -613,6 +613,9 @@ class Comment(EmailMixin, models.Model): 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')] diff --git a/patchwork/tests/api/test_comment.py b/patchwork/tests/api/test_comment.py new file mode 100644 index 0000000..b283bfe --- /dev/null +++ b/patchwork/tests/api/test_comment.py @@ -0,0 +1,115 @@ +# 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]) + + another_comment = 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]) + + another_comment = 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')) diff --git a/patchwork/tests/api/test_cover.py b/patchwork/tests/api/test_cover.py index 3135b7e..9a0fbbb 100644 --- a/patchwork/tests/api/test_cover.py +++ b/patchwork/tests/api/test_cover.py @@ -49,7 +49,8 @@ class TestCoverLetterAPI(APITestCase): 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']) @@ -115,6 +116,14 @@ class TestCoverLetterAPI(APITestCase): self.assertEqual(status.HTTP_200_OK, resp.status_code) self.assertSerialized(cover_obj, resp.data) + # 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 diff --git a/patchwork/tests/api/test_patch.py b/patchwork/tests/api/test_patch.py index 909c1eb..d9d44d3 100644 --- a/patchwork/tests/api/test_patch.py +++ b/patchwork/tests/api/test_patch.py @@ -45,10 +45,15 @@ class TestPatchAPI(APITestCase): 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']) @@ -130,6 +135,14 @@ class TestPatchAPI(APITestCase): 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() diff --git a/patchwork/urls.py b/patchwork/urls.py index 0893fe2..1dc4ffc 100644 --- a/patchwork/urls.py +++ b/patchwork/urls.py @@ -213,6 +213,7 @@ if settings.ENABLE_REST_API: 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 @@ -279,8 +280,18 @@ if settings.ENABLE_REST_API: name='api-event-list'), ] + api_1_1_patterns = [ + url(r'^patches/(?P[^/]+)/comments/$', + api_comment_views.CommentList.as_view(), + name='api-comment-list'), + url(r'^covers/(?P[^/]+)/comments/$', + api_comment_views.CommentList.as_view(), + name='api-comment-list'), + ] + urlpatterns += [ url(r'^api/(?:(?P(1.0|1.1))/)?', include(api_patterns)), + url(r'^api/(?:(?P1.1)/)?', include(api_1_1_patterns)), # token change url(r'^user/generate-token/$', user_views.generate_token, diff --git a/releasenotes/notes/comments-api-b7dff6ee4ce04c9b.yaml b/releasenotes/notes/comments-api-b7dff6ee4ce04c9b.yaml new file mode 100644 index 0000000..6cd6441 --- /dev/null +++ b/releasenotes/notes/comments-api-b7dff6ee4ce04c9b.yaml @@ -0,0 +1,8 @@ +--- +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