from rest_framework.generics import get_object_or_404
from rest_framework.generics import ListAPIView
+from rest_framework.generics import RetrieveUpdateAPIView
+from rest_framework.serializers import HiddenField
from rest_framework.serializers import SerializerMethodField
from patchwork.api.base import BaseHyperlinkedModelSerializer
+from patchwork.api.base import MultipleFieldLookupMixin
from patchwork.api.base import PatchworkPermission
+from patchwork.api.base import CurrentCoverDefault
+from patchwork.api.base import CurrentPatchDefault
from patchwork.api.embedded import PersonSerializer
from patchwork.models import Cover
from patchwork.models import CoverComment
class Meta:
fields = ('id', 'web_url', 'msgid', 'list_archive_url', 'date',
- 'subject', 'submitter', 'content', 'headers')
- read_only_fields = fields
+ 'subject', 'submitter', 'content', 'headers', 'addressed')
+ read_only_fields = ('id', 'web_url', 'msgid', 'list_archive_url',
+ 'date', 'subject', 'submitter', 'content',
+ 'headers')
versioned_fields = {
'1.1': ('web_url', ),
'1.2': ('list_archive_url',),
+ '1.3': ('addressed',),
}
-class CoverCommentListSerializer(BaseCommentListSerializer):
+class CoverCommentSerializer(BaseCommentListSerializer):
+
+ cover = HiddenField(default=CurrentCoverDefault())
class Meta:
model = CoverComment
- fields = BaseCommentListSerializer.Meta.fields
- read_only_fields = fields
+ fields = BaseCommentListSerializer.Meta.fields + (
+ 'cover', 'addressed')
+ read_only_fields = BaseCommentListSerializer.Meta.read_only_fields + (
+ 'cover', )
versioned_fields = BaseCommentListSerializer.Meta.versioned_fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-cover-comment-detail'}
+ }
+
+
+class CoverCommentMixin(object):
+
+ permission_classes = (PatchworkPermission,)
+ serializer_class = CoverCommentSerializer
+
+ def get_object(self):
+ queryset = self.filter_queryset(self.get_queryset())
+ comment_id = self.kwargs['comment_id']
+ obj = get_object_or_404(queryset, id=comment_id)
+ self.check_object_permissions(self.request, obj)
+ return obj
+
+ def get_queryset(self):
+ cover_id = self.kwargs['cover_id']
+ get_object_or_404(Cover, pk=cover_id)
+
+ return CoverComment.objects.filter(
+ cover=cover_id
+ ).select_related('submitter')
-class PatchCommentListSerializer(BaseCommentListSerializer):
+class PatchCommentSerializer(BaseCommentListSerializer):
+
+ patch = HiddenField(default=CurrentPatchDefault())
class Meta:
model = PatchComment
- fields = BaseCommentListSerializer.Meta.fields
- read_only_fields = fields
+ fields = BaseCommentListSerializer.Meta.fields + ('patch', )
+ read_only_fields = BaseCommentListSerializer.Meta.read_only_fields + (
+ 'patch', )
versioned_fields = BaseCommentListSerializer.Meta.versioned_fields
+ extra_kwargs = {
+ 'url': {'view_name': 'api-patch-comment-detail'}
+ }
-class CoverCommentList(ListAPIView):
- """List cover comments"""
+class PatchCommentMixin(object):
permission_classes = (PatchworkPermission,)
- serializer_class = CoverCommentListSerializer
+ serializer_class = PatchCommentSerializer
+
+ def get_object(self):
+ queryset = self.filter_queryset(self.get_queryset())
+ comment_id = self.kwargs['comment_id']
+ obj = get_object_or_404(queryset, id=comment_id)
+ self.check_object_permissions(self.request, obj)
+ return obj
+
+ def get_queryset(self):
+ patch_id = self.kwargs['patch_id']
+ get_object_or_404(Patch, id=patch_id)
+
+ return PatchComment.objects.filter(
+ patch=patch_id
+ ).select_related('submitter')
+
+
+class CoverCommentList(CoverCommentMixin, ListAPIView):
+ """List cover comments"""
+
search_fields = ('subject',)
ordering_fields = ('id', 'subject', 'date', 'submitter')
ordering = 'id'
lookup_url_kwarg = 'cover_id'
- def get_queryset(self):
- get_object_or_404(Cover, pk=self.kwargs['cover_id'])
- return CoverComment.objects.filter(
- cover=self.kwargs['cover_id']
- ).select_related('submitter')
+class CoverCommentDetail(CoverCommentMixin, MultipleFieldLookupMixin,
+ RetrieveUpdateAPIView):
+ """
+ get:
+ Show a cover comment.
+ patch:
+ Update a cover comment.
-class PatchCommentList(ListAPIView):
- """List comments"""
+ put:
+ Update a cover comment.
+ """
+ lookup_url_kwargs = ('cover_id', 'comment_id')
+ lookup_fields = ('cover_id', 'id')
+
+
+class PatchCommentList(PatchCommentMixin, ListAPIView):
+ """List patch comments"""
- permission_classes = (PatchworkPermission,)
- serializer_class = PatchCommentListSerializer
search_fields = ('subject',)
ordering_fields = ('id', 'subject', 'date', 'submitter')
ordering = 'id'
lookup_url_kwarg = 'patch_id'
- def get_queryset(self):
- get_object_or_404(Patch, id=self.kwargs['patch_id'])
- return PatchComment.objects.filter(
- patch=self.kwargs['patch_id']
- ).select_related('submitter')
+class PatchCommentDetail(PatchCommentMixin, MultipleFieldLookupMixin,
+ RetrieveUpdateAPIView):
+ """
+ get:
+ Show a patch comment.
+
+ patch:
+ Update a patch comment.
+
+ put:
+ Update a patch comment.
+ """
+ lookup_url_kwargs = ('patch_id', 'comment_id')
+ lookup_fields = ('patch_id', 'id')
from django.urls import NoReverseMatch
from django.urls import reverse
+from patchwork.models import PatchComment
+from patchwork.models import CoverComment
from patchwork.tests.api import utils
from patchwork.tests.utils import create_cover
from patchwork.tests.utils import create_cover_comment
from patchwork.tests.utils import create_patch
from patchwork.tests.utils import create_patch_comment
+from patchwork.tests.utils import create_maintainer
+from patchwork.tests.utils import create_project
+from patchwork.tests.utils import create_person
+from patchwork.tests.utils import create_user
from patchwork.tests.utils import SAMPLE_CONTENT
if settings.ENABLE_REST_API:
class TestCoverComments(utils.APITestCase):
@staticmethod
- def api_url(cover, version=None):
- kwargs = {}
+ def api_url(cover, version=None, item=None):
+ kwargs = {'cover_id': cover.id}
if version:
kwargs['version'] = version
- kwargs['cover_id'] = cover.id
+ if item is None:
+ return reverse('api-cover-comment-list', kwargs=kwargs)
+ kwargs['comment_id'] = item.id
+ return reverse('api-cover-comment-detail', kwargs=kwargs)
- return reverse('api-cover-comment-list', kwargs=kwargs)
+ def setUp(self):
+ super(TestCoverComments, self).setUp()
+ self.project = create_project()
+ self.user = create_maintainer(self.project)
+ self.cover = create_cover(project=self.project)
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.assertEqual(comment_obj.addressed, comment_json['addressed'])
self.assertIn(SAMPLE_CONTENT, comment_json['content'])
def test_list_empty(self):
@utils.store_samples('cover-comment-list')
def test_list(self):
"""List cover letter comments."""
- cover = create_cover()
- comment = create_cover_comment(cover=cover)
+ comment = create_cover_comment(cover=self.cover)
- resp = self.client.get(self.api_url(cover))
+ resp = self.client.get(self.api_url(self.cover))
self.assertEqual(status.HTTP_200_OK, resp.status_code)
self.assertEqual(1, len(resp.data))
self.assertSerialized(comment, resp.data[0])
def test_list_version_1_1(self):
"""List cover letter comments using API v1.1."""
- cover = create_cover()
- comment = create_cover_comment(cover=cover)
+ create_cover_comment(cover=self.cover)
- resp = self.client.get(self.api_url(cover, version='1.1'))
+ resp = self.client.get(self.api_url(self.cover, version='1.1'))
self.assertEqual(status.HTTP_200_OK, resp.status_code)
self.assertEqual(1, len(resp.data))
- self.assertSerialized(comment, resp.data[0])
self.assertNotIn('list_archive_url', resp.data[0])
+ self.assertNotIn('addressed', resp.data[0])
def test_list_version_1_0(self):
- """List cover letter comments using API v1.0."""
- cover = create_cover()
- create_cover_comment(cover=cover)
+ """List cover letter comments using API v1.0.
- # check we can't access comments using the old version of the API
+ Ensure we can't access cover comments using the old version of the API.
+ """
with self.assertRaises(NoReverseMatch):
- self.client.get(self.api_url(cover, version='1.0'))
+ self.client.get(self.api_url(self.cover, version='1.0'))
def test_list_non_existent_cover(self):
"""Ensure we get a 404 for a non-existent cover letter."""
reverse('api-cover-comment-list', kwargs={'pk': 'foo'}),
)
+ @utils.store_samples('cover-comment-detail')
+ def test_detail(self):
+ """Show a cover letter comment."""
+ comment = create_cover_comment(cover=self.cover)
+
+ resp = self.client.get(self.api_url(self.cover, item=comment))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertSerialized(comment, resp.data)
+
+ def test_detail_version_1_3(self):
+ """Show a cover letter comment using API v1.3."""
+ comment = create_cover_comment(cover=self.cover)
+
+ resp = self.client.get(
+ self.api_url(self.cover, version='1.3', item=comment))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertSerialized(comment, resp.data)
+
+ def test_detail_version_1_2(self):
+ """Show a cover letter comment using API v1.2."""
+ comment = create_cover_comment(cover=self.cover)
+
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(
+ self.api_url(self.cover, version='1.2', item=comment))
+
+ def test_detail_version_1_1(self):
+ """Show a cover letter comment using API v1.1."""
+ comment = create_cover_comment(cover=self.cover)
+
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(
+ self.api_url(self.cover, version='1.1', item=comment))
+
+ def test_detail_version_1_0(self):
+ """Show a cover letter comment using API v1.0."""
+ comment = create_cover_comment(cover=self.cover)
+
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(
+ self.api_url(self.cover, version='1.0', item=comment))
+
+ @utils.store_samples('cover-comment-detail-error-not-found')
+ def test_detail_invalid_cover(self):
+ """Ensure we handle non-existent cover letters."""
+ comment = create_cover_comment()
+ resp = self.client.get(
+ reverse('api-cover-comment-detail', kwargs={
+ 'cover_id': '99999',
+ 'comment_id': comment.id}
+ ),
+ )
+ self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code)
+
+ def _test_update(self, person, **kwargs):
+ submitter = kwargs.get('submitter', person)
+ cover = kwargs.get('cover', self.cover)
+ comment = create_cover_comment(submitter=submitter, cover=cover)
+
+ if kwargs.get('authenticate', True):
+ self.client.force_authenticate(user=person.user)
+ return self.client.patch(
+ self.api_url(cover, item=comment),
+ {'addressed': kwargs.get('addressed', True)},
+ validate_request=kwargs.get('validate_request', True)
+ )
+
+ @utils.store_samples('cover-comment-detail-update-authorized')
+ def test_update_authorized(self):
+ """Update an existing cover letter comment as an authorized user.
+
+ To be authorized users must meet at least one of the following:
+ - project maintainer, cover letter submitter, or cover letter
+ comment submitter
+
+ Ensure updates can only be performed by authorized users.
+ """
+ # Update as maintainer
+ person = create_person(user=self.user)
+ resp = self._test_update(person=person)
+ self.assertEqual(1, CoverComment.objects.all().count())
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertTrue(resp.data['addressed'])
+
+ # Update as cover letter submitter
+ person = create_person(name='cover-submitter', user=create_user())
+ cover = create_cover(submitter=person)
+ resp = self._test_update(person=person, cover=cover)
+ self.assertEqual(2, CoverComment.objects.all().count())
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertTrue(resp.data['addressed'])
+
+ # Update as cover letter comment submitter
+ person = create_person(name='comment-submitter', user=create_user())
+ cover = create_cover()
+ resp = self._test_update(person=person, cover=cover, submitter=person)
+ self.assertEqual(3, CoverComment.objects.all().count())
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertTrue(resp.data['addressed'])
+
+ @utils.store_samples('cover-comment-detail-update-not-authorized')
+ def test_update_not_authorized(self):
+ """Update an existing cover letter comment when not signed in and
+ not authorized.
+
+ To be authorized users must meet at least one of the following:
+ - project maintainer, cover letter submitter, or cover letter
+ comment submitter
+
+ Ensure updates can only be performed by authorized users.
+ """
+ person = create_person(user=self.user)
+ resp = self._test_update(person=person, authenticate=False)
+ self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
+
+ person = create_person() # normal user without edit permissions
+ resp = self._test_update(person=person) # signed-in
+ self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
+
+ @utils.store_samples('cover-comment-detail-update-error-bad-request')
+ def test_update_invalid_addressed(self):
+ """Update an existing cover letter comment using invalid values.
+
+ Ensure we handle invalid cover letter comment addressed values.
+ """
+ person = create_person(name='cover-submitter', user=create_user())
+ cover = create_cover(submitter=person)
+ resp = self._test_update(person=person,
+ cover=cover,
+ addressed='not-valid',
+ validate_request=False)
+ self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code)
+ self.assertFalse(
+ getattr(CoverComment.objects.all().first(), 'addressed')
+ )
+
+ def test_create_delete(self):
+ """Ensure creates and deletes aren't allowed"""
+ comment = create_cover_comment(cover=self.cover)
+ self.user.is_superuser = True
+ self.user.save()
+ self.client.force_authenticate(user=self.user)
+
+ resp = self.client.post(self.api_url(self.cover, item=comment))
+ self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
+ resp = self.client.delete(self.api_url(self.cover, item=comment))
+ self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
class TestPatchComments(utils.APITestCase):
@staticmethod
- def api_url(patch, version=None):
- kwargs = {}
+ def api_url(patch, version=None, item=None):
+ kwargs = {'patch_id': patch.id}
if version:
kwargs['version'] = version
- kwargs['patch_id'] = patch.id
+ if item is None:
+ return reverse('api-patch-comment-list', kwargs=kwargs)
+ kwargs['comment_id'] = item.id
+ return reverse('api-patch-comment-detail', kwargs=kwargs)
- return reverse('api-patch-comment-list', kwargs=kwargs)
+ def setUp(self):
+ super(TestPatchComments, self).setUp()
+ self.project = create_project()
+ self.user = create_maintainer(self.project)
+ self.patch = create_patch(project=self.project)
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.assertEqual(comment_obj.addressed, comment_json['addressed'])
self.assertIn(SAMPLE_CONTENT, comment_json['content'])
def test_list_empty(self):
"""List patch comments when none are present."""
- patch = create_patch()
- resp = self.client.get(self.api_url(patch))
+ resp = self.client.get(self.api_url(self.patch))
self.assertEqual(status.HTTP_200_OK, resp.status_code)
self.assertEqual(0, len(resp.data))
@utils.store_samples('patch-comment-list')
def test_list(self):
"""List patch comments."""
- patch = create_patch()
- comment = create_patch_comment(patch=patch)
+ comment = create_patch_comment(patch=self.patch)
- resp = self.client.get(self.api_url(patch))
+ resp = self.client.get(self.api_url(self.patch))
self.assertEqual(status.HTTP_200_OK, resp.status_code)
self.assertEqual(1, len(resp.data))
self.assertSerialized(comment, resp.data[0])
def test_list_version_1_1(self):
"""List patch comments using API v1.1."""
- patch = create_patch()
- comment = create_patch_comment(patch=patch)
+ create_patch_comment(patch=self.patch)
- resp = self.client.get(self.api_url(patch, version='1.1'))
+ resp = self.client.get(self.api_url(self.patch, version='1.1'))
self.assertEqual(status.HTTP_200_OK, resp.status_code)
self.assertEqual(1, len(resp.data))
- self.assertSerialized(comment, resp.data[0])
self.assertNotIn('list_archive_url', resp.data[0])
+ self.assertNotIn('addressed', resp.data[0])
def test_list_version_1_0(self):
- """List patch comments using API v1.0."""
- patch = create_patch()
- create_patch_comment(patch=patch)
+ """List patch comments using API v1.0.
- # check we can't access comments using the old version of the API
+ Ensure we can't access comments using the old version of the API.
+ """
with self.assertRaises(NoReverseMatch):
- self.client.get(self.api_url(patch, version='1.0'))
+ self.client.get(self.api_url(self.patch, version='1.0'))
def test_list_non_existent_patch(self):
"""Ensure we get a 404 for a non-existent patch."""
self.client.get(
reverse('api-patch-comment-list', kwargs={'patch_id': 'foo'}),
)
+
+ @utils.store_samples('patch-comment-detail')
+ def test_detail(self):
+ """Show a patch comment."""
+ comment = create_patch_comment(patch=self.patch)
+
+ resp = self.client.get(self.api_url(self.patch, item=comment))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertSerialized(comment, resp.data)
+
+ def test_detail_version_1_3(self):
+ """Show a patch comment using API v1.3."""
+ comment = create_patch_comment(patch=self.patch)
+
+ resp = self.client.get(
+ self.api_url(self.patch, version='1.3', item=comment))
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertSerialized(comment, resp.data)
+
+ def test_detail_version_1_2(self):
+ """Show a patch comment using API v1.2."""
+ comment = create_patch_comment(patch=self.patch)
+
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(
+ self.api_url(self.patch, version='1.2', item=comment))
+
+ def test_detail_version_1_1(self):
+ """Show a patch comment using API v1.1."""
+ comment = create_patch_comment(patch=self.patch)
+
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(
+ self.api_url(self.patch, version='1.1', item=comment))
+
+ def test_detail_version_1_0(self):
+ """Show a patch comment using API v1.0."""
+ comment = create_patch_comment(patch=self.patch)
+
+ with self.assertRaises(NoReverseMatch):
+ self.client.get(
+ self.api_url(self.patch, version='1.0', item=comment))
+
+ @utils.store_samples('patch-comment-detail-error-not-found')
+ def test_detail_invalid_patch(self):
+ """Ensure we handle non-existent patches."""
+ comment = create_patch_comment()
+ resp = self.client.get(
+ reverse('api-patch-comment-detail', kwargs={
+ 'patch_id': '99999',
+ 'comment_id': comment.id}
+ ),
+ )
+ self.assertEqual(status.HTTP_404_NOT_FOUND, resp.status_code)
+
+ def _test_update(self, person, **kwargs):
+ submitter = kwargs.get('submitter', person)
+ patch = kwargs.get('patch', self.patch)
+ comment = create_patch_comment(submitter=submitter, patch=patch)
+
+ if kwargs.get('authenticate', True):
+ self.client.force_authenticate(user=person.user)
+ return self.client.patch(
+ self.api_url(patch, item=comment),
+ {'addressed': kwargs.get('addressed', True)},
+ validate_request=kwargs.get('validate_request', True)
+ )
+
+ @utils.store_samples('patch-comment-detail-update-authorized')
+ def test_update_authorized(self):
+ """Update an existing patch comment as an authorized user.
+
+ To be authorized users must meet at least one of the following:
+ - project maintainer, patch submitter, patch delegate, or
+ patch comment submitter
+
+ Ensure updates can only be performed by authorized users.
+ """
+ # Update as maintainer
+ person = create_person(user=self.user)
+ resp = self._test_update(person=person)
+ self.assertEqual(1, PatchComment.objects.all().count())
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertTrue(resp.data['addressed'])
+
+ # Update as patch submitter
+ person = create_person(name='patch-submitter', user=create_user())
+ patch = create_patch(submitter=person)
+ resp = self._test_update(person=person, patch=patch)
+ self.assertEqual(2, PatchComment.objects.all().count())
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertTrue(resp.data['addressed'])
+
+ # Update as patch delegate
+ person = create_person(name='patch-delegate', user=create_user())
+ patch = create_patch(delegate=person.user)
+ resp = self._test_update(person=person, patch=patch)
+ self.assertEqual(3, PatchComment.objects.all().count())
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertTrue(resp.data['addressed'])
+
+ # Update as patch comment submitter
+ person = create_person(name='comment-submitter', user=create_user())
+ patch = create_patch()
+ resp = self._test_update(person=person, patch=patch, submitter=person)
+ self.assertEqual(4, PatchComment.objects.all().count())
+ self.assertEqual(status.HTTP_200_OK, resp.status_code)
+ self.assertTrue(resp.data['addressed'])
+
+ @utils.store_samples('patch-comment-detail-update-not-authorized')
+ def test_update_not_authorized(self):
+ """Update an existing patch comment when not signed in and not authorized.
+
+ To be authorized users must meet at least one of the following:
+ - project maintainer, patch submitter, patch delegate, or
+ patch comment submitter
+
+ Ensure updates can only be performed by authorized users.
+ """
+ person = create_person(user=self.user)
+ resp = self._test_update(person=person, authenticate=False)
+ self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
+
+ person = create_person() # normal user without edit permissions
+ resp = self._test_update(person=person) # signed-in
+ self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code)
+
+ @utils.store_samples('patch-comment-detail-update-error-bad-request')
+ def test_update_invalid_addressed(self):
+ """Update an existing patch comment using invalid values.
+
+ Ensure we handle invalid patch comment addressed values.
+ """
+ person = create_person(name='patch-submitter', user=create_user())
+ patch = create_patch(submitter=person)
+ resp = self._test_update(person=person,
+ patch=patch,
+ addressed='not-valid',
+ validate_request=False)
+ self.assertEqual(status.HTTP_400_BAD_REQUEST, resp.status_code)
+ self.assertFalse(
+ getattr(PatchComment.objects.all().first(), 'addressed')
+ )
+
+ def test_create_delete(self):
+ """Ensure creates and deletes aren't allowed"""
+ comment = create_patch_comment(patch=self.patch)
+ self.user.is_superuser = True
+ self.user.save()
+ self.client.force_authenticate(user=self.user)
+
+ resp = self.client.post(self.api_url(self.patch, item=comment))
+ self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
+ resp = self.client.delete(self.api_url(self.patch, item=comment))
+ self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)