[PATCH v2 2/4] tagging: add tags and related filters to REST API
vkabatov at redhat.com
vkabatov at redhat.com
Tue Sep 18 03:05:10 AEST 2018
From: Veronika Kabatova <vkabatov at redhat.com>
Signed-off-by: Veronika Kabatova <vkabatov at redhat.com>
---
patchwork/api/comment.py | 12 +++++-
patchwork/api/cover.py | 14 ++++++-
patchwork/api/filters.py | 42 ++++++++++++++++++-
patchwork/api/patch.py | 13 +++---
patchwork/tests/api/test_patch.py | 3 +-
.../tagging-rework-9907e9dc3f835566.yaml | 11 +++++
6 files changed, 85 insertions(+), 10 deletions(-)
diff --git a/patchwork/api/comment.py b/patchwork/api/comment.py
index 5a5adb1d..a328e2a8 100644
--- a/patchwork/api/comment.py
+++ b/patchwork/api/comment.py
@@ -26,6 +26,7 @@ from patchwork.api.base import BaseHyperlinkedModelSerializer
from patchwork.api.base import PatchworkPermission
from patchwork.api.embedded import PersonSerializer
from patchwork.models import Comment
+from patchwork.models import SubmissionTag
class CommentListSerializer(BaseHyperlinkedModelSerializer):
@@ -34,6 +35,7 @@ class CommentListSerializer(BaseHyperlinkedModelSerializer):
subject = SerializerMethodField()
headers = SerializerMethodField()
submitter = PersonSerializer(read_only=True)
+ tags = SerializerMethodField()
def get_web_url(self, instance):
request = self.context.get('request')
@@ -43,6 +45,13 @@ class CommentListSerializer(BaseHyperlinkedModelSerializer):
return email.parser.Parser().parsestr(comment.headers,
True).get('Subject', '')
+ def get_tags(self, instance):
+ tags = {}
+ for tag_object in instance.all_tags:
+ tags[tag_object.name] = instance.all_tags[tag_object]
+
+ return tags
+
def get_headers(self, comment):
headers = {}
@@ -60,10 +69,11 @@ class CommentListSerializer(BaseHyperlinkedModelSerializer):
class Meta:
model = Comment
fields = ('id', 'web_url', 'msgid', 'date', 'subject', 'submitter',
- 'content', 'headers')
+ 'content', 'headers', 'tags')
read_only_fields = fields
versioned_fields = {
'1.1': ('web_url', ),
+ '1.2': ('tags', ),
}
diff --git a/patchwork/api/cover.py b/patchwork/api/cover.py
index 3a9fc003..191b8418 100644
--- a/patchwork/api/cover.py
+++ b/patchwork/api/cover.py
@@ -30,6 +30,7 @@ from patchwork.api.embedded import PersonSerializer
from patchwork.api.embedded import ProjectSerializer
from patchwork.api.embedded import SeriesSerializer
from patchwork.models import CoverLetter
+from patchwork.models import SubmissionTag
class CoverLetterListSerializer(BaseHyperlinkedModelSerializer):
@@ -40,6 +41,7 @@ class CoverLetterListSerializer(BaseHyperlinkedModelSerializer):
mbox = SerializerMethodField()
series = SeriesSerializer(read_only=True)
comments = SerializerMethodField()
+ tags = SerializerMethodField()
def get_web_url(self, instance):
request = self.context.get('request')
@@ -53,6 +55,13 @@ class CoverLetterListSerializer(BaseHyperlinkedModelSerializer):
return self.context.get('request').build_absolute_uri(
reverse('api-cover-comment-list', kwargs={'pk': cover.id}))
+ def get_tags(self, instance):
+ tags = {}
+ for tag_object in instance.all_tags:
+ tags[tag_object.name] = instance.all_tags[tag_object]
+
+ return tags
+
def to_representation(self, instance):
# NOTE(stephenfin): This is here to ensure our API looks the same even
# after we changed the series-patch relationship from M:N to 1:N. It
@@ -65,10 +74,11 @@ class CoverLetterListSerializer(BaseHyperlinkedModelSerializer):
class Meta:
model = CoverLetter
fields = ('id', 'url', 'web_url', 'project', 'msgid', 'date', 'name',
- 'submitter', 'mbox', 'series', 'comments')
+ 'submitter', 'mbox', 'series', 'comments', 'tags')
read_only_fields = fields
versioned_fields = {
'1.1': ('web_url', 'mbox', 'comments'),
+ '1.2': ('tags', ),
}
extra_kwargs = {
'url': {'view_name': 'api-cover-detail'},
@@ -113,6 +123,7 @@ class CoverLetterList(ListAPIView):
def get_queryset(self):
return CoverLetter.objects.all()\
+ .prefetch_related('tags')\
.select_related('project', 'submitter', 'series')\
.defer('content', 'headers')
@@ -124,4 +135,5 @@ class CoverLetterDetail(RetrieveAPIView):
def get_queryset(self):
return CoverLetter.objects.all()\
+ .prefetch_related('tags')\
.select_related('project', 'submitter', 'series')
diff --git a/patchwork/api/filters.py b/patchwork/api/filters.py
index ddf527fd..416136e4 100644
--- a/patchwork/api/filters.py
+++ b/patchwork/api/filters.py
@@ -21,6 +21,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.models import Q
from django_filters.rest_framework import FilterSet
+from django.filters import Filter
from django_filters import IsoDateTimeFilter
from django_filters import ModelMultipleChoiceFilter
from django.forms import ModelMultipleChoiceField as BaseMultipleChoiceField
@@ -36,6 +37,7 @@ from patchwork.models import Person
from patchwork.models import Project
from patchwork.models import Series
from patchwork.models import State
+from patchwork.models import SubmissionTag
# custom fields, filters
@@ -137,6 +139,40 @@ class StateFilter(ModelMultipleChoiceFilter):
field_class = StateChoiceField
+class TagFilter(Filter):
+
+ def filter(self, qs, query):
+ submissions_and_series = []
+
+ for tag_filter in query:
+ try:
+ tag_name, tag_value = tag_filter.split(':', 1)
+ except ValueError:
+ raise ValidationError(
+ 'Query in format `tag=<name>:<value>` expected! <name> or '
+ '<value> can be missing or wildcard (*) if all tags with '
+ 'given attribute are expected.'
+ )
+ # Map the globbing or missing wildcard to regex syntax
+ if tag_name.strip() in ['', '*']:
+ tag_name = '.*'
+ if tag_value.strip() in ['', '*']:
+ tag_value = '.*'
+
+ submissions_and_series.extend(SubmissionTag.objects.filter(
+ tag__name__regex=tag_name,
+ value__regex=tag_value
+ ).values_list('submission__id', 'series'))
+
+ submission_list = []
+ series_list = []
+ for submission, series in submissions_and_series:
+ submission_list.append(submission)
+ series_list.append(series)
+
+ return qs.filter(Q(id__in=submission_list) | Q(series__in=series_list))
+
+
class UserChoiceField(ModelMultipleChoiceField):
alternate_lookup = 'username__iexact'
@@ -174,10 +210,11 @@ class CoverLetterFilterSet(TimestampMixin, FilterSet):
series = BaseFilter(queryset=Project.objects.all(),
widget=MultipleHiddenInput)
submitter = PersonFilter(queryset=Person.objects.all())
+ tag = TagFilter(widget=MultipleHiddenInput)
class Meta:
model = CoverLetter
- fields = ('project', 'series', 'submitter')
+ fields = ('project', 'series', 'submitter', 'tag')
class PatchFilterSet(TimestampMixin, FilterSet):
@@ -190,11 +227,12 @@ class PatchFilterSet(TimestampMixin, FilterSet):
submitter = PersonFilter(queryset=Person.objects.all())
delegate = UserFilter(queryset=User.objects.all())
state = StateFilter(queryset=State.objects.all())
+ tag = TagFilter(widget=MultipleHiddenInput)
class Meta:
model = Patch
fields = ('project', 'series', 'submitter', 'delegate',
- 'state', 'archived')
+ 'state', 'archived', 'tag')
class CheckFilterSet(TimestampMixin, FilterSet):
diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py
index 549ec4fa..12421952 100644
--- a/patchwork/api/patch.py
+++ b/patchwork/api/patch.py
@@ -35,6 +35,7 @@ from patchwork.api.embedded import SeriesSerializer
from patchwork.api.embedded import UserSerializer
from patchwork.models import Patch
from patchwork.models import State
+from patchwork.models import SubmissionTag
from patchwork.parser import clean_subject
@@ -109,9 +110,11 @@ class PatchListSerializer(BaseHyperlinkedModelSerializer):
reverse('api-check-list', kwargs={'patch_id': instance.id}))
def get_tags(self, instance):
- # TODO(stephenfin): Make tags performant, possibly by reworking the
- # model
- return {}
+ tags = {}
+ for tag_object in instance.all_tags:
+ tags[tag_object.name] = instance.all_tags[tag_object]
+
+ return tags
def to_representation(self, instance):
# NOTE(stephenfin): This is here to ensure our API looks the same even
@@ -183,7 +186,7 @@ class PatchList(ListAPIView):
def get_queryset(self):
return Patch.objects.all()\
- .prefetch_related('check_set')\
+ .prefetch_related('check_set', 'tags')\
.select_related('project', 'state', 'submitter', 'delegate',
'series')\
.defer('content', 'diff', 'headers')
@@ -197,6 +200,6 @@ class PatchDetail(RetrieveUpdateAPIView):
def get_queryset(self):
return Patch.objects.all()\
- .prefetch_related('check_set')\
+ .prefetch_related('check_set', 'tags')\
.select_related('project', 'state', 'submitter', 'delegate',
'series')
diff --git a/patchwork/tests/api/test_patch.py b/patchwork/tests/api/test_patch.py
index 104f7c8a..a8498c10 100644
--- a/patchwork/tests/api/test_patch.py
+++ b/patchwork/tests/api/test_patch.py
@@ -166,7 +166,8 @@ class TestPatchAPI(APITestCase):
self.assertEqual(patch.content, resp.data['content'])
self.assertEqual(patch.diff, resp.data['diff'])
- self.assertEqual(0, len(resp.data['tags']))
+ self.assertEqual(1, len(resp.data['tags']['Reviewed-by']))
+ self.assertEqual(0, len(resp.data['tags']['Acked-by']))
def test_detail_version_1_0(self):
patch = create_patch()
diff --git a/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml b/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml
index 8a525532..fdfd39f0 100644
--- a/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml
+++ b/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml
@@ -6,6 +6,17 @@ features:
accidentally sent the Acked-by email twice, since only a single same pair
tagname-value can be assigned to a patch. Tags from cover letters are now
counted towards each patch in the series.
+api:
+ - |
+ The ``tags`` field of the ``/patches`` is now populated and an equivalent
+ field is added for the cover letters and comments. Tags are listed as
+ key-value pairs, making it easier to find where a specific tag originated
+ from.
+ - |
+ Tag filtering on patches and cover letters using ``?tag=<name>:<value>`` is
+ now supported. <name> or <value> can also be missing or wildcard ``*`` in
+ case all tags with given attribute are wanted. For example,
+ ``?tag=Acked-by:*`` returns all patches / cover letters which were acked.
upgrade:
- |
The ``retag`` command (``python manage.py retag``) needs to be ran after
--
2.17.1
More information about the Patchwork
mailing list