[PATCH v5 2/7] models: Add 'Series' model and related models

Stephen Finucane stephen at that.guru
Mon Oct 10 09:25:16 AEDT 2016


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 <stephen at 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')]
 
 
+ at 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',)
+
+
+ at 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']
+
+
+ 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(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



More information about the Patchwork mailing list