Skip to content

Commit

Permalink
Rework tagging infrastructure
Browse files Browse the repository at this point in the history
Solve getpatchwork#113 and getpatchwork#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 <vkabatov@redhat.com>
  • Loading branch information
veruu committed Sep 14, 2018
1 parent f0f8024 commit d401bfd
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 157 deletions.
15 changes: 9 additions & 6 deletions patchwork/management/commands/retag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '[<patch_id>...]'
help = 'Update tags on existing submissions and associated comments'
args = '[<submission_id>...]'

def handle(self, *args, **options):
query = Patch.objects
query = Submission.objects.prefetch_related('comments')

if args:
query = query.filter(id__in=args)
Expand All @@ -36,8 +36,11 @@ def handle(self, *args, **options):

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()
Expand Down
66 changes: 66 additions & 0 deletions patchwork/migrations/0034_rework_tagging.py
Original file line number Diff line number Diff line change
@@ -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')]),
),
]
175 changes: 82 additions & 93 deletions patchwork/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@

from __future__ import absolute_import

from collections import Counter
from collections import OrderedDict
import datetime
import random
import re
Expand All @@ -30,6 +28,7 @@
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
Expand Down Expand Up @@ -250,71 +249,28 @@ 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

class Meta:
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
Expand All @@ -340,6 +296,16 @@ def patch_responses(self):
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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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()
Expand All @@ -475,7 +453,7 @@ def save(self, *args, **kwargs):

super(Patch, self).save(**kwargs)

self.refresh_tag_counts()
self.refresh_tags()

def is_editable(self, user):
if not user.is_authenticated:
Expand Down Expand Up @@ -610,13 +588,23 @@ def get_absolute_url(self):

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
Expand Down Expand Up @@ -715,6 +703,7 @@ def add_cover_letter(self, cover):
self.name = self._format_name(cover)

self.save()
cover.refresh_tags()

def add_patch(self, patch, number):
"""Add a patch to the series."""
Expand Down
3 changes: 2 additions & 1 deletion patchwork/templatetags/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@
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("-")
Expand Down
Loading

0 comments on commit d401bfd

Please sign in to comment.