From patchwork Mon Sep 17 17:03:32 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Veronika Kabatova X-Patchwork-Id: 970738 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 ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 42DXXf2W9Vz9sfR for ; Tue, 18 Sep 2018 03:04:18 +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 42DXXf11gjzF1RG for ; Tue, 18 Sep 2018 03:04:18 +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 42DXX51066zDr2j for ; Tue, 18 Sep 2018 03:03:48 +1000 (AEST) Received: from smtp.corp.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.14]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 492B186669; Mon, 17 Sep 2018 17:03:46 +0000 (UTC) Received: from steamlocomotive (unknown [10.43.17.43]) by smtp.corp.redhat.com (Postfix) with ESMTP id 69D8B18A79; Mon, 17 Sep 2018 17:03:45 +0000 (UTC) From: vkabatov@redhat.com To: patchwork@lists.ozlabs.org Subject: [PATCH v2 1/4] Rework tagging infrastructure Date: Mon, 17 Sep 2018 19:03:32 +0200 Message-Id: <20180917170335.23838-1-vkabatov@redhat.com> X-Scanned-By: MIMEDefang 2.79 on 10.5.11.14 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.26]); Mon, 17 Sep 2018 17:03:46 +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 Solve #113 and #57 GitHub issues, keep track of tag origin to be able to add tags to comments in the API later. Use relations Tag-Patch and Tag-CoverLetter to avoid duplication of tags for each patch in series, and use `series` attribute of SubmissionTag as a notion of a tag which is related to each patch in the series (because it comes from cover letter or it's comments) Signed-off-by: Veronika Kabatova --- Rebased on top of 'Convert Series-Patch relationship to 1:N' series. Stephen, I split up the patch to separate out API, mbox and documentation changes as you suggested; and implemented your comments (simplified the migration in favor of running the retag comment, moved the tag retrieval from the API into a property in models.py, added comment and tag prefetching, increased the API version where needed, added wildcard to API filter and simplified it and some other minor things). The series-patch cleanup definitely helped with some cleanup, but let me know if there are other optimizations that would help with regards to DB performance. --- patchwork/management/commands/retag.py | 15 +- patchwork/migrations/0034_rework_tagging.py | 66 +++++++ patchwork/models.py | 175 ++++++++---------- patchwork/templatetags/patch.py | 3 +- patchwork/tests/test_parser.py | 18 +- patchwork/tests/test_tags.py | 64 +++---- patchwork/views/__init__.py | 3 - .../tagging-rework-9907e9dc3f835566.yaml | 15 ++ 8 files changed, 202 insertions(+), 157 deletions(-) create mode 100644 patchwork/migrations/0034_rework_tagging.py create mode 100644 releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml diff --git a/patchwork/management/commands/retag.py b/patchwork/management/commands/retag.py index 8617ff41..95b2cc1f 100644 --- a/patchwork/management/commands/retag.py +++ b/patchwork/management/commands/retag.py @@ -19,15 +19,15 @@ from django.core.management.base import BaseCommand -from patchwork.models import Patch +from patchwork.models import Submission class Command(BaseCommand): - help = 'Update the tag (Ack/Review/Test) counts on existing patches' - args = '[...]' + help = 'Update tags on existing submissions and associated comments' + args = '[...]' def handle(self, *args, **options): - query = Patch.objects + query = Submission.objects.prefetch_related('comments') if args: query = query.filter(id__in=args) @@ -36,8 +36,11 @@ class Command(BaseCommand): count = query.count() - for i, patch in enumerate(query.iterator()): - patch.refresh_tag_counts() + for i, submission in enumerate(query.iterator()): + submission.refresh_tags() + for comment in submission.comments.all(): + comment.refresh_tags() + if (i % 10) == 0: self.stdout.write('%06d/%06d\r' % (i, count), ending='') self.stdout.flush() diff --git a/patchwork/migrations/0034_rework_tagging.py b/patchwork/migrations/0034_rework_tagging.py new file mode 100644 index 00000000..580a4fd0 --- /dev/null +++ b/patchwork/migrations/0034_rework_tagging.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('patchwork', '0033_remove_patch_series_model'), + ] + + operations = [ + migrations.CreateModel( + name='SubmissionTag', + fields=[ + ('id', models.AutoField(auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID')), + ('value', models.CharField(max_length=255)), + ('comment', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='patchwork.Comment', + null=True + )), + ('submission', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='patchwork.Submission' + )), + ('tag', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='patchwork.Tag' + )), + ('series', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='patchwork.Series', + null=True + )), + ], + ), + migrations.AlterUniqueTogether( + name='patchtag', + unique_together=set([]), + ), + migrations.RemoveField(model_name='patchtag', name='patch',), + migrations.RemoveField(model_name='patchtag', name='tag',), + migrations.RemoveField(model_name='patch', name='tags',), + migrations.DeleteModel(name='PatchTag',), + migrations.AddField( + model_name='submission', + name='tags', + field=models.ManyToManyField( + through='patchwork.SubmissionTag', + to='patchwork.Tag' + ), + ), + migrations.AlterUniqueTogether( + name='submissiontag', + unique_together=set([('submission', + 'tag', + 'value', + 'comment')]), + ), + ] diff --git a/patchwork/models.py b/patchwork/models.py index 14eb74aa..5caf7641 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -20,8 +20,6 @@ from __future__ import absolute_import -from collections import Counter -from collections import OrderedDict import datetime import random import re @@ -30,6 +28,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models +from django.db.models import Q from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from django.utils.functional import cached_property @@ -250,10 +249,6 @@ class Tag(models.Model): ' tag\'s count in the patch list view', default=True) - @property - def attr_name(self): - return 'tag_%d_count' % self.id - def __str__(self): return self.name @@ -261,60 +256,21 @@ class Tag(models.Model): ordering = ['abbrev'] -class PatchTag(models.Model): - patch = models.ForeignKey('Patch', on_delete=models.CASCADE) +class SubmissionTag(models.Model): + submission = models.ForeignKey('Submission', on_delete=models.CASCADE) tag = models.ForeignKey('Tag', on_delete=models.CASCADE) - count = models.IntegerField(default=1) + value = models.CharField(max_length=255) + comment = models.ForeignKey('Comment', null=True, on_delete=models.CASCADE) + series = models.ForeignKey('Series', null=True, on_delete=models.CASCADE) class Meta: - unique_together = [('patch', 'tag')] + unique_together = [('submission', 'tag', 'value', 'comment')] def get_default_initial_patch_state(): return State.objects.get(ordering=0) -class PatchQuerySet(models.query.QuerySet): - - def with_tag_counts(self, project=None): - if project and not project.use_tags: - return self - - # We need the project's use_tags field loaded for Project.tags(). - # Using prefetch_related means we'll share the one instance of - # Project, and share the project.tags cache between all patch.project - # references. - qs = self.prefetch_related('project') - select = OrderedDict() - select_params = [] - - # All projects have the same tags, so we're good to go here - if project: - tags = project.tags - else: - tags = Tag.objects.all() - - for tag in tags: - select[tag.attr_name] = ( - "coalesce(" - "(SELECT count FROM patchwork_patchtag" - " WHERE patchwork_patchtag.patch_id=" - "patchwork_patch.submission_ptr_id" - " AND patchwork_patchtag.tag_id=%s), 0)") - select_params.append(tag.id) - - return qs.extra(select=select, select_params=select_params) - - -class PatchManager(models.Manager): - - def get_queryset(self): - return PatchQuerySet(self.model, using=self.db) - - def with_tag_counts(self, project): - return self.get_queryset().with_tag_counts(project) - - class EmailMixin(models.Model): """Mixin for models with an email-origin.""" # email metadata @@ -340,6 +296,16 @@ class EmailMixin(models.Model): return ''.join([match.group(0) + '\n' for match in self.response_re.finditer(self.content)]) + def _extract_tags(self, tags): + found_tags = {} + if not self.content: + return found_tags + + for tag in tags: + regex = re.compile(tag.pattern + r'\s*(.*)', re.M | re.I | re.U) + found_tags[tag] = regex.findall(self.content) + return found_tags + def save(self, *args, **kwargs): # Modifying a submission via admin interface changes '\n' newlines in # message content to '\r\n'. We need to fix them to avoid problems, @@ -371,6 +337,53 @@ class Submission(FilenameMixin, EmailMixin, models.Model): # submission metadata name = models.CharField(max_length=255) + tags = models.ManyToManyField(Tag, through=SubmissionTag) + + def add_tags(self, tag, values, comment=None): + if hasattr(self, 'patch'): + series = None + else: + series = self.coverletter.series + current_objs = SubmissionTag.objects.filter(submission=self, + comment=comment, + tag=tag, + series=series) + if not values: + current_objs.delete() + return + # In case the origin is modified, delete tags that were removed + current_objs.exclude(value__in=values).delete() + values_to_add = set(values) - set(current_objs.values_list('value', + flat=True)) + SubmissionTag.objects.bulk_create([SubmissionTag( + submission=self, + tag=tag, + value=value, + comment=comment, + series=series + ) for value in values_to_add]) + + def refresh_tags(self): + submission_tags = self._extract_tags(Tag.objects.all()) + for tag in submission_tags: + self.add_tags(tag, submission_tags[tag]) + + @property + def all_tags(self): + related_tags = {} + + for tag in self.project.tags: + if hasattr(self, 'patch'): + related_tags[tag] = SubmissionTag.objects.filter(( + Q(submission=self) | Q(series=self.series) + ) & Q(tag__name=tag.name)).values_list('value', + flat=True).distinct() + else: + related_tags[tag] = SubmissionTag.objects.filter( + Q(submission=self) & Q(tag__name=tag.name) + ).values_list('value', flat=True).distinct() + + return related_tags # patchwork metadata @@ -409,7 +422,6 @@ class Patch(Submission): diff = models.TextField(null=True, blank=True) commit_ref = models.CharField(max_length=255, null=True, blank=True) pull_url = models.CharField(max_length=255, null=True, blank=True) - tags = models.ManyToManyField(Tag, through=PatchTag) # patchwork metadata @@ -432,40 +444,6 @@ class Patch(Submission): default=None, null=True, help_text='The number assigned to this patch in the series') - objects = PatchManager() - - @staticmethod - def extract_tags(content, tags): - counts = Counter() - - for tag in tags: - regex = re.compile(tag.pattern, re.MULTILINE | re.IGNORECASE) - counts[tag] = len(regex.findall(content)) - - return counts - - def _set_tag(self, tag, count): - if count == 0: - self.patchtag_set.filter(tag=tag).delete() - return - patchtag, _ = PatchTag.objects.get_or_create(patch=self, tag=tag) - if patchtag.count != count: - patchtag.count = count - patchtag.save() - - def refresh_tag_counts(self): - tags = self.project.tags - counter = Counter() - - if self.content: - counter += self.extract_tags(self.content, tags) - - for comment in self.comments.all(): - counter = counter + self.extract_tags(comment.content, tags) - - for tag in tags: - self._set_tag(tag, counter[tag]) - def save(self, *args, **kwargs): if not hasattr(self, 'state') or not self.state: self.state = get_default_initial_patch_state() @@ -475,7 +453,7 @@ class Patch(Submission): super(Patch, self).save(**kwargs) - self.refresh_tag_counts() + self.refresh_tags() def is_editable(self, user): if not user.is_authenticated: @@ -610,13 +588,23 @@ class Comment(EmailMixin, models.Model): def save(self, *args, **kwargs): super(Comment, self).save(*args, **kwargs) - if hasattr(self.submission, 'patch'): - self.submission.patch.refresh_tag_counts() + self.refresh_tags() + + def refresh_tags(self): + comment_tags = self._extract_tags(Tag.objects.all()) + for tag in comment_tags: + self.submission.add_tags(tag, comment_tags[tag], comment=self) + + @property + def all_tags(self): + related_tags = {} + + for tag in self.submission.project.tags: + related_tags[tag] = SubmissionTag.objects.filter( + comment=self, tag__name=tag.name + ).values_list('value', flat=True).distinct() - def delete(self, *args, **kwargs): - super(Comment, self).delete(*args, **kwargs) - if hasattr(self.submission, 'patch'): - self.submission.patch.refresh_tag_counts() + return related_tags def is_editable(self, user): return False @@ -715,6 +703,7 @@ class Series(FilenameMixin, models.Model): self.name = self._format_name(cover) self.save() + cover.refresh_tags() def add_patch(self, patch, number): """Add a patch to the series.""" diff --git a/patchwork/templatetags/patch.py b/patchwork/templatetags/patch.py index 30ccc8e2..5be6b908 100644 --- a/patchwork/templatetags/patch.py +++ b/patchwork/templatetags/patch.py @@ -34,8 +34,9 @@ register = template.Library() def patch_tags(patch): counts = [] titles = [] + all_tags = patch.all_tags for tag in [t for t in patch.project.tags if t.show_column]: - count = getattr(patch, tag.attr_name) + count = len(all_tags[tag]) titles.append('%d %s' % (count, tag.name)) if count == 0: counts.append("-") diff --git a/patchwork/tests/test_parser.py b/patchwork/tests/test_parser.py index e99cf214..7fdceab3 100644 --- a/patchwork/tests/test_parser.py +++ b/patchwork/tests/test_parser.py @@ -802,12 +802,9 @@ class ParseInitialTagsTest(PatchTest): def test_tags(self): self.assertEqual(Patch.objects.count(), 1) patch = Patch.objects.all()[0] - self.assertEqual(patch.patchtag_set.filter( - tag__name='Acked-by').count(), 0) - self.assertEqual(patch.patchtag_set.get( - tag__name='Reviewed-by').count, 1) - self.assertEqual(patch.patchtag_set.get( - tag__name='Tested-by').count, 1) + self.assertEqual(patch.tags.filter(name='Acked-by').count(), 0) + self.assertEqual(patch.tags.filter(name='Reviewed-by').count(), 1) + self.assertEqual(patch.tags.filter(name='Tested-by').count(), 1) class ParseCommentTagsTest(PatchTest): @@ -830,12 +827,9 @@ class ParseCommentTagsTest(PatchTest): def test_tags(self): self.assertEqual(Patch.objects.count(), 1) patch = Patch.objects.all()[0] - self.assertEqual(patch.patchtag_set.filter( - tag__name='Acked-by').count(), 0) - self.assertEqual(patch.patchtag_set.get( - tag__name='Reviewed-by').count, 1) - self.assertEqual(patch.patchtag_set.get( - tag__name='Tested-by').count, 1) + self.assertEqual(patch.tags.filter(name='Acked-by').count(), 0) + self.assertEqual(patch.tags.filter(name='Reviewed-by').count(), 1) + self.assertEqual(patch.tags.filter(name='Tested-by').count(), 1) class SubjectTest(TestCase): diff --git a/patchwork/tests/test_tags.py b/patchwork/tests/test_tags.py index 4fd1bf23..f7a35f92 100644 --- a/patchwork/tests/test_tags.py +++ b/patchwork/tests/test_tags.py @@ -21,7 +21,7 @@ from django.test import TestCase from django.test import TransactionTestCase from patchwork.models import Patch -from patchwork.models import PatchTag +from patchwork.models import SubmissionTag from patchwork.models import Tag from patchwork.tests.utils import create_comment from patchwork.tests.utils import create_patch @@ -34,11 +34,14 @@ class ExtractTagsTest(TestCase): name_email = 'test name <' + email + '>' def assertTagsEqual(self, str, acks, reviews, tests): # noqa - counts = Patch.extract_tags(str, Tag.objects.all()) - self.assertEqual((acks, reviews, tests), - (counts[Tag.objects.get(name='Acked-by')], - counts[Tag.objects.get(name='Reviewed-by')], - counts[Tag.objects.get(name='Tested-by')])) + patch = create_patch(content=str) + extracted = patch._extract_tags(Tag.objects.all()) + self.assertEqual( + (acks, reviews, tests), + (len(extracted.get(Tag.objects.get(name='Acked-by'), [])), + len(extracted.get(Tag.objects.get(name='Reviewed-by'), [])), + len(extracted.get(Tag.objects.get(name='Tested-by'), []))) + ) def test_empty(self): self.assertTagsEqual('', 0, 0, 0) @@ -80,7 +83,7 @@ class ExtractTagsTest(TestCase): self.assertTagsEqual('> Acked-by: %s\n' % self.name_email, 0, 0, 0) -class PatchTagsTest(TransactionTestCase): +class SubmissionTagsTest(TransactionTestCase): fixtures = ['default_tags'] ACK = 1 @@ -95,16 +98,14 @@ class PatchTagsTest(TransactionTestCase): def assertTagsEqual(self, patch, acks, reviews, tests): # noqa patch = Patch.objects.get(pk=patch.pk) - def count(name): - try: - return patch.patchtag_set.get(tag__name=name).count - except PatchTag.DoesNotExist: - return 0 + def count(submission, name): + return SubmissionTag.objects.filter(submission=submission, + tag__name=name).count() counts = ( - count(name='Acked-by'), - count(name='Reviewed-by'), - count(name='Tested-by'), + count(patch, name='Acked-by'), + count(patch, name='Reviewed-by'), + count(patch, name='Tested-by'), ) self.assertEqual(counts, (acks, reviews, tests)) @@ -118,7 +119,12 @@ class PatchTagsTest(TransactionTestCase): if tagtype not in tags: return '' - return '%s-by: Test Tagger \n' % tags[tagtype] + index = SubmissionTag.objects.filter( + tag__name=tags[tagtype] + '-by' + ).count() + return '%s-by: Test Taggeri%d \n' % ( + tags[tagtype], index + 1 + ) def create_tag_comment(self, patch, tagtype=None): comment = create_comment( @@ -179,29 +185,3 @@ class PatchTagsTest(TransactionTestCase): c1.content += self.create_tag(self.REVIEW) c1.save() self.assertTagsEqual(self.patch, 1, 1, 0) - - -class PatchTagManagerTest(PatchTagsTest): - - def assertTagsEqual(self, patch, acks, reviews, tests): # noqa - tagattrs = {} - for tag in Tag.objects.all(): - tagattrs[tag.name] = tag.attr_name - - # force project.tags to be queried outside of the assertNumQueries - patch.project.tags - - # we should be able to do this with two queries: one for - # the patch table lookup, and the prefetch_related for the - # projects table. - with self.assertNumQueries(2): - patch = Patch.objects.with_tag_counts(project=patch.project) \ - .get(pk=patch.pk) - - counts = ( - getattr(patch, tagattrs['Acked-by']), - getattr(patch, tagattrs['Reviewed-by']), - getattr(patch, tagattrs['Tested-by']), - ) - - self.assertEqual(counts, (acks, reviews, tests)) diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py index 5942ded8..3ff4345c 100644 --- a/patchwork/views/__init__.py +++ b/patchwork/views/__init__.py @@ -274,9 +274,6 @@ def generic_list(request, project, view, view_args=None, filter_settings=None, if patches is None: patches = Patch.objects.filter(patch_project=project) - # annotate with tag counts - patches = patches.with_tag_counts(project) - patches = context['filters'].apply(patches) if not editable_order: patches = order.apply(patches) diff --git a/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml b/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml new file mode 100644 index 00000000..8a525532 --- /dev/null +++ b/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Tagging is completely reworked. Instead of counts, real values are + extracted. This fixes wrong counts when for example the same person + 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. +upgrade: + - | + The ``retag`` command (``python manage.py retag``) needs to be ran after + the upgrade. The migration only takes care of the database structure, while + the actual tag data will be created by the command, to make the migration + itself faster. Please note that this will take a lot of time and based on + the size of the data in question, might be useful to run in batches. 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 From patchwork Mon Sep 17 17:05:11 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Veronika Kabatova X-Patchwork-Id: 970741 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 ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 42DXZL6WPpz9sfR for ; Tue, 18 Sep 2018 03:05:46 +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 42DXZL5Hw8zF31F for ; Tue, 18 Sep 2018 03:05:46 +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 42DXYv49KTzDr4m for ; Tue, 18 Sep 2018 03:05:23 +1000 (AEST) Received: from smtp.corp.redhat.com (int-mx09.intmail.prod.int.phx2.redhat.com [10.5.11.24]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id AC00180464; Mon, 17 Sep 2018 17:05:21 +0000 (UTC) Received: from steamlocomotive (unknown [10.43.17.43]) by smtp.corp.redhat.com (Postfix) with ESMTP id 22040308BDAC; Mon, 17 Sep 2018 17:05:20 +0000 (UTC) From: vkabatov@redhat.com To: patchwork@lists.ozlabs.org Subject: [PATCH v2 3/4] tagging: use tag infrastructure to create tags in mboxes Date: Mon, 17 Sep 2018 19:05:11 +0200 Message-Id: <20180917170512.24405-2-vkabatov@redhat.com> In-Reply-To: <20180917170512.24405-1-vkabatov@redhat.com> References: <20180917170335.23838-1-vkabatov@redhat.com> <20180917170512.24405-1-vkabatov@redhat.com> X-Scanned-By: MIMEDefang 2.84 on 10.5.11.24 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.28]); Mon, 17 Sep 2018 17:05:21 +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/models.py | 12 ------------ patchwork/tests/test_mboxviews.py | 19 ++++++++++++++++--- patchwork/views/utils.py | 9 ++++++--- .../tagging-rework-9907e9dc3f835566.yaml | 3 +++ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/patchwork/models.py b/patchwork/models.py index 5caf7641..a7c75e63 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -284,18 +284,6 @@ class EmailMixin(models.Model): submitter = models.ForeignKey(Person, on_delete=models.CASCADE) content = models.TextField(null=True, blank=True) - response_re = re.compile( - r'^(Tested|Reviewed|Acked|Signed-off|Nacked|Reported)-by:.*$', - re.M | re.I) - - @property - def patch_responses(self): - if not self.content: - return '' - - return ''.join([match.group(0) + '\n' for match in - self.response_re.finditer(self.content)]) - def _extract_tags(self, tags): found_tags = {} if not self.content: diff --git a/patchwork/tests/test_mboxviews.py b/patchwork/tests/test_mboxviews.py index 9d941bf8..f7be7b0e 100644 --- a/patchwork/tests/test_mboxviews.py +++ b/patchwork/tests/test_mboxviews.py @@ -36,9 +36,10 @@ from patchwork.tests.utils import create_user class MboxPatchResponseTest(TestCase): - """Test that the mbox view appends the Acked-by from a patch comment.""" + fixtures = ['default_tags'] + def setUp(self): self.project = create_project() self.person = create_person() @@ -53,7 +54,9 @@ class MboxPatchResponseTest(TestCase): submitter=self.person, content='comment 2 text\nAcked-by: 2\n') response = self.client.get(reverse('patch-mbox', args=[patch.id])) - self.assertContains(response, 'Acked-by: 1\nAcked-by: 2\n') + # Can't guarantee the order in which the tags are returned + self.assertContains(response, 'Acked-by: 1\n') + self.assertContains(response, 'Acked-by: 2\n') def test_patch_utf8_nbsp(self): patch = create_patch( @@ -73,6 +76,8 @@ class MboxPatchSplitResponseTest(TestCase): """Test that the mbox view appends the Acked-by from a patch comment, and places it before an '---' update line.""" + fixtures = ['default_tags'] + def setUp(self): project = create_project() self.person = create_person() @@ -88,7 +93,15 @@ class MboxPatchSplitResponseTest(TestCase): def test_patch_response(self): response = self.client.get(reverse('patch-mbox', args=[self.patch.id])) - self.assertContains(response, 'Acked-by: 1\nAcked-by: 2\n') + # Can't guarantee the order in which the tags are returned + self.assertContains(response, 'Acked-by: 1\n') + self.assertContains(response, 'Acked-by: 2\n') + # We need to check for 3 Acked-by strings, one comes from the body of + # the patch and the other two are the tags themselves. + self.assertRegex( + response.content.decode(), + '(?s).*Acked-by: 1\n.*Acked-by.*Acked-by.*---\nupdate.*' + ) class MboxHeaderTest(TestCase): diff --git a/patchwork/views/utils.py b/patchwork/views/utils.py index 4644c621..0586265b 100644 --- a/patchwork/views/utils.py +++ b/patchwork/views/utils.py @@ -27,11 +27,13 @@ import email.utils import re from django.conf import settings +from django.db.models import Q from django.http import Http404 from django.utils import six from patchwork.models import Comment from patchwork.models import Patch +from patchwork.models import SubmissionTag if settings.ENABLE_REST_API: from rest_framework.authtoken.models import Token @@ -74,9 +76,10 @@ def _submission_to_mbox(submission): else: postscript = '' - # TODO(stephenfin): Make this use the tags infrastructure - for comment in Comment.objects.filter(submission=submission): - body += comment.patch_responses + for (tagname, value) in SubmissionTag.objects.filter( + Q(submission=submission) | Q(series=submission.series) + ).values_list('tag__name', 'value').distinct(): + body += '%s: %s\n' % (tagname, value) if postscript: body += '---\n' + postscript + '\n' diff --git a/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml b/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml index fdfd39f0..7fd64184 100644 --- a/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml +++ b/releasenotes/notes/tagging-rework-9907e9dc3f835566.yaml @@ -24,3 +24,6 @@ upgrade: the actual tag data will be created by the command, to make the migration itself faster. Please note that this will take a lot of time and based on the size of the data in question, might be useful to run in batches. +other: + - | + The tagging feature is now used to populate tags in mbox files. From patchwork Mon Sep 17 17:05:12 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Veronika Kabatova X-Patchwork-Id: 970740 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 ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 42DXZ006ZBz9sfR for ; Tue, 18 Sep 2018 03:05:28 +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 42DXYz60k4zF1RG for ; Tue, 18 Sep 2018 03:05:27 +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 42DXYs5GhBzDr4m for ; Tue, 18 Sep 2018 03:05:21 +1000 (AEST) Received: from smtp.corp.redhat.com (int-mx11.intmail.prod.int.phx2.redhat.com [10.5.11.26]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 0AACD88318; Mon, 17 Sep 2018 17:05:20 +0000 (UTC) Received: from steamlocomotive (unknown [10.43.17.43]) by smtp.corp.redhat.com (Postfix) with ESMTP id 9FFAA3091380; Mon, 17 Sep 2018 17:05:19 +0000 (UTC) From: vkabatov@redhat.com To: patchwork@lists.ozlabs.org Subject: [PATCH v2 4/4] tagging: change wording in documentation Date: Mon, 17 Sep 2018 19:05:12 +0200 Message-Id: <20180917170512.24405-3-vkabatov@redhat.com> In-Reply-To: <20180917170512.24405-1-vkabatov@redhat.com> References: <20180917170335.23838-1-vkabatov@redhat.com> <20180917170512.24405-1-vkabatov@redhat.com> X-Scanned-By: MIMEDefang 2.84 on 10.5.11.26 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.28]); Mon, 17 Sep 2018 17:05:20 +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 All submissions can have tags associated with them now and the documentation text should reflect it. Signed-off-by: Veronika Kabatova --- docs/usage/overview.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/usage/overview.rst b/docs/usage/overview.rst index e84e13d9..91d710c0 100644 --- a/docs/usage/overview.rst +++ b/docs/usage/overview.rst @@ -119,10 +119,11 @@ one delegate can be assigned to a patch. Tags ~~~~ -Tags are specially formatted metadata appended to the foot the body of a patch -or a comment on a patch. Patchwork extracts these tags at parse time and -associates them with the patch. You add extra tags to an email by replying to -the email. The following tags are available on a standard Patchwork install: +Tags are specially formatted metadata appended to the foot the body of a patch, +cover letter or a comment related to them. Patchwork extracts these tags at +parse time and associates them with the submissions. You add extra tags to an +email by replying to the email. The following tags are available on a standard +Patchwork install: ``Acked-by:`` For example::