[PATCH v7 2/8] models: Add 'Series' model

Stephen Finucane stephen at that.guru
Sun Oct 30 00:13:34 AEDT 2016


Add a series model. This model is expected to act like a collection for
patches, similar to bundles but thread-orientated.

Signed-off-by: Stephen Finucane <stephen at that.guru>
Reviewed-by: Andy Doan <andy.doan at linaro.org>
Reviewed-by: Daniel Axtens <dja at axtens.net>
Reviewed-by: Andrew Donnellan <andrew.donnellan at au1.ibm.com>
Tested-by: Russell Currey <ruscur at russell.cc>
---
v7:
- Rename 'SeriesRevision' to 'Series'
- Rename 'Series.actual_total' to 'Series.received_total'
- Rename 'Series.complete' to 'Series.received_all'
- Move 'Patch.series' and 'CoverLetter.series' properties into a mixin
  and rename 'latest_series'
- Don't call the 'latest_series' property in admin, as this results in
  excessive database queries
- Clarify precendence of names
v6:
- Store first patch names in 'SeriesRevision.name' field, if cover
  a name is not already set
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                             |  54 ++++++++-
 patchwork/migrations/0015_add_series_models.py |  67 +++++++++++
 patchwork/models.py                            | 160 ++++++++++++++++++++++++-
 3 files changed, 276 insertions(+), 5 deletions(-)
 create mode 100644 patchwork/migrations/0015_add_series_models.py

diff --git a/patchwork/admin.py b/patchwork/admin.py
index 85ffecf..ef041c4 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 Series
+from patchwork.models import SeriesReference
+from patchwork.models import State
+from patchwork.models import Submission
+from patchwork.models import Tag
+from patchwork.models import UserProfile
 
 
 class DelegationRuleInline(admin.TabularInline):
@@ -94,6 +105,43 @@ class CommentAdmin(admin.ModelAdmin):
 admin.site.register(Comment, CommentAdmin)
 
 
+class PatchInline(admin.StackedInline):
+    model = Series.patches.through
+    extra = 0
+
+
+class SeriesAdmin(admin.ModelAdmin):
+    list_display = ('name', 'date', 'submitter', 'version', 'total',
+                    'received_total', 'received_all')
+    readonly_fields = ('received_total', 'received_all')
+    search_fields = ('submitter_name', 'submitter_email')
+    exclude = ('patches', )
+    inlines = (PatchInline, )
+
+    def received_all(self, series):
+        return series.received_all
+    received_all.boolean = True
+admin.site.register(Series, SeriesAdmin)
+
+
+class SeriesInline(admin.StackedInline):
+    model = Series
+    readonly_fields = ('date', 'submitter', 'version', 'total',
+                       'received_total', 'received_all')
+    ordering = ('-date', )
+    show_change_link = True
+    extra = 0
+
+    def received_all(self, series):
+        return series.received_all
+    received_all.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/0015_add_series_models.py b/patchwork/migrations/0015_add_series_models.py
new file mode 100644
index 0000000..b7c3dc7
--- /dev/null
+++ b/patchwork/migrations/0015_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', '0014_remove_userprofile_primary_project'),
+    ]
+
+    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='Series',
+            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 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', to='patchwork.CoverLetter')),
+            ],
+            options={
+                'ordering': ('date',),
+            },
+        ),
+        migrations.CreateModel(
+            name='SeriesPatch',
+            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')),
+                ('patch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patchwork.Patch')),
+                ('series', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patchwork.Series')),
+            ],
+            options={
+                'ordering': ['number'],
+            },
+        ),
+        migrations.AddField(
+            model_name='series',
+            name='patches',
+            field=models.ManyToManyField(related_name='series', through='patchwork.SeriesPatch', to='patchwork.Patch'),
+        ),
+        migrations.AddField(
+            model_name='series',
+            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.Series'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='seriespatch',
+            unique_together=set([('series', 'number'), ('series', 'patch')]),
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index 8a9762a..a27dda6 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -314,12 +314,31 @@ class Submission(EmailMixin, models.Model):
         unique_together = [('msgid', 'project')]
 
 
-class CoverLetter(Submission):
+class SeriesMixin(object):
+
+    @property
+    def latest_series(self):
+        """Get the latest series this is a member of.
+
+        Return the last series 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 exist
+        return self.series.order_by('-date').first()
+
+
+class CoverLetter(SeriesMixin, Submission):
     pass
 
 
 @python_2_unicode_compatible
-class Patch(Submission):
+class Patch(SeriesMixin, Submission):
     # patch metadata
 
     diff = models.TextField(null=True, blank=True)
@@ -566,6 +585,143 @@ class Comment(EmailMixin, models.Model):
         unique_together = [('msgid', 'submission')]
 
 
+ at python_2_unicode_compatible
+class Series(models.Model):
+    """An collection of patches."""
+
+    # content
+    cover_letter = models.ForeignKey(CoverLetter,
+                                     related_name='series',
+                                     null=True, blank=True)
+    patches = models.ManyToManyField(Patch, through='SeriesPatch',
+                                     related_name='series')
+
+    # 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 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 received_total(self):
+        return self.patches.count()
+
+    @property
+    def received_all(self):
+        return self.total == self.received_total
+
+    def add_cover_letter(self, cover):
+        """Add a cover letter to the series.
+
+        Helper method so we can use the same pattern to add both
+        patches and cover letters.
+        """
+
+        def _format_name(obj):
+            return obj.name.split(']')[-1]
+
+        if self.cover_letter:
+            # TODO(stephenfin): We may wish to raise an exception here in the
+            # future
+            return
+
+        self.cover_letter = cover
+
+        # we allow "upgrading of series names. Names from different
+        # sources are prioritized:
+        #
+        # 1. user-provided names
+        # 2. cover letter-based names
+        # 3. first patch-based (i.e. 01/nn) names
+        #
+        # Names are never "downgraded" - a cover letter received after
+        # the first patch will result in the name being upgraded to a
+        # cover letter-based name, but receiving the first patch after
+        # the cover letter will not change the name of the series.
+        #
+        # If none of the above are available, the name will be null.
+
+        if not self.name:
+            self.name = _format_name(cover)
+        else:
+            try:
+                name = SeriesPatch.objects.get(series=self,
+                                               number=1).patch.name
+            except SeriesPatch.DoesNotExist:
+                name = None
+
+            if self.name == name:
+                self.name = _format_name(cover)
+
+        self.save()
+
+    def add_patch(self, patch, number):
+        """Add a patch to the series."""
+        # see if the patch is already in this series
+        if SeriesPatch.objects.filter(series=self, patch=patch).count():
+            # TODO(stephenfin): We may wish to raise an exception here in the
+            # future
+            return
+
+        # both user defined names and cover letter-based names take precedence
+        if not self.name and number == 1:
+            self.name = patch.name  # keep the prefixes for patch-based names
+            self.save()
+
+        return SeriesPatch.objects.create(series=self,
+                                          patch=patch,
+                                          number=number)
+
+    def __str__(self):
+        return self.name if self.name else 'Untitled series #%d' % self.id
+
+    class Meta:
+        ordering = ('date',)
+
+
+ at python_2_unicode_compatible
+class SeriesPatch(models.Model):
+    """A patch in a series.
+
+    Patches can belong to many series. This allows for things like
+    auto-completion of partial series.
+    """
+    patch = models.ForeignKey(Patch)
+    series = models.ForeignKey(Series)
+    number = models.PositiveSmallIntegerField(
+        help_text='The number assigned to this patch in the series')
+
+    def __str__(self):
+        return self.patch.name
+
+    class Meta:
+        unique_together = [('series', 'patch'), ('series', 'number')]
+        ordering = ['number']
+
+
+ at 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(Series, 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



More information about the Patchwork mailing list