Hi Stephen, This doesn't have my fix for reducing DB queries: http://patchwork.ozlabs.org/patch/670292/
If you are respinning, please consider including it. Otherwise it'll need to be a follow-up patch - preferably merged at the same time. Regards, Daniel Stephen Finucane <step...@that.guru> writes: > Add a series model. This model is intentionally very minimal to allow > as much dynaminism as possible. It is expected that patches will be > migrated between series as new data is provided. > > Signed-off-by: Stephen Finucane <step...@that.guru> > --- > v5: > - Store cover letter name in SeriesRevision.name field > - Add warning about using the 'Patch.series' property, which causes a > new query each time > v4: > - Convert 'SeriesRevision'-'Patch' relationship from one-to-many to > many-to-many > - Remove 'Series' model, which is not used yet (revisioning is a > minefield that's being addressed separately) > - Add 'name' field to 'SeriesRevision' > v2: > - Resolve issue with REST API (Andrew Donnellan) > - Use more meaningful names for untitled series (Andrew Donnellan) > v1: > - Rename 'SeriesGroup' to 'Series' > - Rename 'Series' to 'SeriesRevision' > --- > patchwork/admin.py | 67 ++++++++++- > patchwork/migrations/0014_add_series_models.py | 67 +++++++++++ > patchwork/models.py | 158 > +++++++++++++++++++++++-- > 3 files changed, 274 insertions(+), 18 deletions(-) > create mode 100644 patchwork/migrations/0014_add_series_models.py > > diff --git a/patchwork/admin.py b/patchwork/admin.py > index 85ffecf..49bd55b 100644 > --- a/patchwork/admin.py > +++ b/patchwork/admin.py > @@ -21,9 +21,20 @@ from __future__ import absolute_import > > from django.contrib import admin > > -from patchwork.models import (Project, Person, UserProfile, State, > Submission, > - Patch, CoverLetter, Comment, Bundle, Tag, > Check, > - DelegationRule) > +from patchwork.models import Bundle > +from patchwork.models import Check > +from patchwork.models import Comment > +from patchwork.models import CoverLetter > +from patchwork.models import DelegationRule > +from patchwork.models import Patch > +from patchwork.models import Person > +from patchwork.models import Project > +from patchwork.models import SeriesReference > +from patchwork.models import SeriesRevision > +from patchwork.models import State > +from patchwork.models import Submission > +from patchwork.models import Tag > +from patchwork.models import UserProfile > > > class DelegationRuleInline(admin.TabularInline): > @@ -68,13 +79,22 @@ class SubmissionAdmin(admin.ModelAdmin): > search_fields = ('name', 'submitter__name', 'submitter__email') > date_hierarchy = 'date' > admin.site.register(Submission, SubmissionAdmin) > -admin.site.register(CoverLetter, SubmissionAdmin) > + > + > +class CoverLetterAdmin(admin.ModelAdmin): > + list_display = ('name', 'submitter', 'project', 'date', 'series') > + list_filter = ('project', ) > + readonly_fields = ('series', ) > + search_fields = ('name', 'submitter__name', 'submitter__email') > + date_hierarchy = 'date' > +admin.site.register(CoverLetter, CoverLetterAdmin) > > > class PatchAdmin(admin.ModelAdmin): > list_display = ('name', 'submitter', 'project', 'state', 'date', > - 'archived', 'is_pull_request') > + 'archived', 'is_pull_request', 'series') > list_filter = ('project', 'state', 'archived') > + readonly_fields = ('series', ) > search_fields = ('name', 'submitter__name', 'submitter__email') > date_hierarchy = 'date' > > @@ -94,6 +114,43 @@ class CommentAdmin(admin.ModelAdmin): > admin.site.register(Comment, CommentAdmin) > > > +class PatchInline(admin.StackedInline): > + model = SeriesRevision.patches.through > + extra = 0 > + > + > +class SeriesRevisionAdmin(admin.ModelAdmin): > + list_display = ('name', 'date', 'submitter', 'version', 'total', > + 'actual_total', 'complete') > + readonly_fields = ('actual_total', 'complete') > + search_fields = ('submitter_name', 'submitter_email') > + exclude = ('patches', ) > + inlines = (PatchInline, ) > + > + def complete(self, series): > + return series.complete > + complete.boolean = True > +admin.site.register(SeriesRevision, SeriesRevisionAdmin) > + > + > +class SeriesRevisionInline(admin.StackedInline): > + model = SeriesRevision > + readonly_fields = ('date', 'submitter', 'version', 'total', > + 'actual_total', 'complete') > + ordering = ('-date', ) > + show_change_link = True > + extra = 0 > + > + def complete(self, series): > + return series.complete > + complete.boolean = True > + > + > +class SeriesReferenceAdmin(admin.ModelAdmin): > + model = SeriesReference > +admin.site.register(SeriesReference, SeriesReferenceAdmin) > + > + > class CheckAdmin(admin.ModelAdmin): > list_display = ('patch', 'user', 'state', 'target_url', > 'description', 'context') > diff --git a/patchwork/migrations/0014_add_series_models.py > b/patchwork/migrations/0014_add_series_models.py > new file mode 100644 > index 0000000..8d0fffa > --- /dev/null > +++ b/patchwork/migrations/0014_add_series_models.py > @@ -0,0 +1,67 @@ > +# -*- 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', '0013_slug_check_context'), > + ] > + > + operations = [ > + migrations.CreateModel( > + name='SeriesReference', > + fields=[ > + ('id', models.AutoField(auto_created=True, primary_key=True, > serialize=False, verbose_name='ID')), > + ('msgid', models.CharField(max_length=255, unique=True)), > + ], > + ), > + migrations.CreateModel( > + name='SeriesRevision', > + fields=[ > + ('id', models.AutoField(auto_created=True, primary_key=True, > serialize=False, verbose_name='ID')), > + ('name', models.CharField(blank=True, help_text=b'An > optional name to associate with the series, e.g. "John\'s PCI series".', > max_length=255, null=True)), > + ('date', models.DateTimeField()), > + ('version', models.IntegerField(default=1, > help_text=b'Version of series revision as indicated by the subject > prefix(es)')), > + ('total', models.IntegerField(help_text=b'Number of patches > in series as indicated by the subject prefix(es)')), > + ('cover_letter', models.ForeignKey(blank=True, null=True, > on_delete=django.db.models.deletion.CASCADE, related_name='series_revisions', > to='patchwork.CoverLetter')), > + ], > + options={ > + 'ordering': ('date',), > + }, > + ), > + migrations.CreateModel( > + name='SeriesRevisionPatch', > + fields=[ > + ('id', models.AutoField(auto_created=True, primary_key=True, > serialize=False, verbose_name='ID')), > + ('number', models.PositiveSmallIntegerField(help_text=b'The > number assigned to this patch in the series revision')), > + ('patch', > models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, > to='patchwork.Patch')), > + ('revision', > models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, > to='patchwork.SeriesRevision')), > + ], > + options={ > + 'ordering': ['number'], > + }, > + ), > + migrations.AddField( > + model_name='seriesrevision', > + name='patches', > + field=models.ManyToManyField(related_name='series_revisions', > related_query_name=b'series_revision', > through='patchwork.SeriesRevisionPatch', to='patchwork.Patch'), > + ), > + migrations.AddField( > + model_name='seriesrevision', > + name='submitter', > + > field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, > to='patchwork.Person'), > + ), > + migrations.AddField( > + model_name='seriesreference', > + name='series', > + > field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, > related_name='references', related_query_name=b'reference', > to='patchwork.SeriesRevision'), > + ), > + migrations.AlterUniqueTogether( > + name='seriesrevisionpatch', > + unique_together=set([('revision', 'number'), ('revision', > 'patch')]), > + ), > + ] > diff --git a/patchwork/models.py b/patchwork/models.py > index 28e9861..4a55c1d 100644 > --- a/patchwork/models.py > +++ b/patchwork/models.py > @@ -293,7 +293,7 @@ class EmailMixin(models.Model): > > @python_2_unicode_compatible > class Submission(EmailMixin, models.Model): > - # parent > + # parents > > project = models.ForeignKey(Project) > > @@ -318,11 +318,27 @@ class Submission(EmailMixin, models.Model): > > > class CoverLetter(Submission): > - pass > + > + @property > + def series(self): > + """Get a simple series reference. > + > + Return the last series revision that (ordered by date) that > + this submission is a member of. > + > + .. warning:: > + Be judicious in your use of this. For example, do not use it > + in list templates as doing so will result in a new query for > + each item in the list. > + """ > + # NOTE(stephenfin): We don't use 'latest()' here, as this can raise > an > + # exception if no series revisions exist > + return self.series_revisions.order_by('-date').first() > > > @python_2_unicode_compatible > class Patch(Submission): > + > # patch metadata > > diff = models.TextField(null=True, blank=True) > @@ -419,17 +435,6 @@ class Patch(Submission): > 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() > - > - if self.hash is None and self.diff is not None: > - self.hash = self.hash_diff(self.diff).hexdigest() > - > - super(Patch, self).save(**kwargs) > - > - self.refresh_tag_counts() > - > def is_editable(self, user): > if not user.is_authenticated(): > return False > @@ -440,6 +445,22 @@ class Patch(Submission): > return self.project.is_editable(user) > > @property > + def series(self): > + """Get a simple series reference. > + > + Return the last series revision that (ordered by date) that > + this submission is a member of. > + > + .. warning:: > + Be judicious in your use of this. For example, do not use it > + in list templates as doing so will result in a new query for > + each item in the list. > + """ > + # NOTE(stephenfin): We don't use 'latest()' here, as this can raise > an > + # exception if no series revisions exist > + return self.series_revisions.order_by('-date').first() > + > + @property > def filename(self): > fname_re = re.compile(r'[^-_A-Za-z0-9\.]+') > str = fname_re.sub('-', self.name) > @@ -546,6 +567,17 @@ class Patch(Submission): > def __str__(self): > return self.name > > + def save(self, *args, **kwargs): > + if not hasattr(self, 'state') or not self.state: > + self.state = get_default_initial_patch_state() > + > + if self.hash is None and self.diff is not None: > + self.hash = self.hash_diff(self.diff).hexdigest() > + > + super(Patch, self).save(**kwargs) > + > + self.refresh_tag_counts() > + > class Meta: > verbose_name_plural = 'Patches' > > @@ -569,6 +601,106 @@ class Comment(EmailMixin, models.Model): > unique_together = [('msgid', 'submission')] > > > +@python_2_unicode_compatible > +class SeriesRevision(models.Model): > + """An individual revision of a series.""" > + > + # content > + cover_letter = models.ForeignKey(CoverLetter, > + related_name='series_revisions', > + null=True, blank=True) > + patches = models.ManyToManyField(Patch, through='SeriesRevisionPatch', > + related_name='series_revisions', > + related_query_name='series_revision') > + > + # metadata > + name = models.CharField(max_length=255, blank=True, null=True, > + help_text='An optional name to associate with ' > + 'the series, e.g. "John\'s PCI series".') > + date = models.DateTimeField() > + submitter = models.ForeignKey(Person) > + version = models.IntegerField(default=1, > + help_text='Version of series revision as ' > + 'indicated by the subject prefix(es)') > + total = models.IntegerField(help_text='Number of patches in series as ' > + 'indicated by the subject prefix(es)') > + > + @property > + def actual_total(self): > + return self.patches.count() > + > + @property > + def complete(self): > + return self.total == self.actual_total > + > + def add_cover_letter(self, cover): > + """Add a cover letter to the series revision. > + > + Helper method so we can use the same pattern to add both > + patches and cover letters. > + """ > + self.cover_letter = cover > + > + if not self.name: # don't override user-defined names > + self.name = cover.name.split(']')[-1] > + > + self.save() > + > + def add_patch(self, patch, number): > + """Add a patch to the series revision.""" > + # see if the patch is already in this series > + if SeriesRevisionPatch.objects.filter(revision=self, > + patch=patch).count(): > + return > + > + return SeriesRevisionPatch.objects.create(patch=patch, > + revision=self, > + number=number) > + > + def __str__(self): > + return self.name if self.name else 'Untitled series #%d' % self.id > + > + class Meta: > + ordering = ('date',) > + > + > +@python_2_unicode_compatible > +class SeriesRevisionPatch(models.Model): > + """A patch in a series revision. > + > + Patches can belong to many series revisions. This allows for things > + like auto-completion of partial series. > + """ > + patch = models.ForeignKey(Patch) > + revision = models.ForeignKey(SeriesRevision) > + number = models.PositiveSmallIntegerField( > + help_text='The number assigned to this patch in the series revision') > + > + def __str__(self): > + return self.patch.name > + > + class Meta: > + unique_together = [('revision', 'patch'), ('revision', 'number')] > + ordering = ['number'] > + > + > +@python_2_unicode_compatible > +class SeriesReference(models.Model): > + """A reference found in a series. > + > + Message IDs should be created for all patches in a series, > + including those of patches that have not yet been received. This is > + required to handle the case whereby one or more patches are > + received before the cover letter. > + """ > + series = models.ForeignKey(SeriesRevision, related_name='references', > + related_query_name='reference') > + msgid = models.CharField(max_length=255, unique=True) > + > + def __str__(self): > + return self.msgid > + > + > class Bundle(models.Model): > owner = models.ForeignKey(User) > project = models.ForeignKey(Project) > -- > 2.7.4 > > _______________________________________________ > Patchwork mailing list > Patchwork@lists.ozlabs.org > https://lists.ozlabs.org/listinfo/patchwork _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork