From patchwork Mon Sep 17 17:05:10 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Veronika Kabatova X-Patchwork-Id: 970742 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 42DXZh40cwz9sfR for ; Tue, 18 Sep 2018 03:06:04 +1000 (AEST) Authentication-Results: ozlabs.org; dmarc=fail (p=none dis=none) header.from=redhat.com Received: from bilbo.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 42DXZh2qgKzF2QM for ; Tue, 18 Sep 2018 03:06:04 +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=209.132.183.28; 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 (mx1.redhat.com [209.132.183.28]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 42DXYx2X9WzDr4m for ; Tue, 18 Sep 2018 03:05:25 +1000 (AEST) Received: from smtp.corp.redhat.com (int-mx01.intmail.prod.int.phx2.redhat.com [10.5.11.11]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 78AEC3002076; Mon, 17 Sep 2018 17:05:23 +0000 (UTC) Received: from steamlocomotive (unknown [10.43.17.43]) by smtp.corp.redhat.com (Postfix) with ESMTP id C29E817C5F; Mon, 17 Sep 2018 17:05:22 +0000 (UTC) From: vkabatov@redhat.com To: patchwork@lists.ozlabs.org Subject: [PATCH v2 2/4] tagging: add tags and related filters to REST API Date: Mon, 17 Sep 2018 19:05:10 +0200 Message-Id: <20180917170512.24405-1-vkabatov@redhat.com> In-Reply-To: <20180917170335.23838-1-vkabatov@redhat.com> References: <20180917170335.23838-1-vkabatov@redhat.com> X-Scanned-By: MIMEDefang 2.79 on 10.5.11.11 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.47]); Mon, 17 Sep 2018 17:05:23 +0000 (UTC) X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 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 | 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=:` expected! or ' + ' 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=:`` is + now supported. or 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