[RFC 3/3] models: Add 'Series' model and related models

Stephen Finucane stephen.finucane at intel.com
Sat Apr 2 03:14:46 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.finucane at intel.com>
---
 patchwork/admin.py                             |  55 ++++++++++++-
 patchwork/migrations/0013_add_series_models.py |  60 ++++++++++++++
 patchwork/models.py                            | 110 ++++++++++++++++++++++++-
 3 files changed, 220 insertions(+), 5 deletions(-)
 create mode 100644 patchwork/migrations/0013_add_series_models.py

diff --git a/patchwork/admin.py b/patchwork/admin.py
index 0f217d7..87cd631 100644
--- a/patchwork/admin.py
+++ b/patchwork/admin.py
@@ -23,7 +23,8 @@ from django.contrib import admin
 
 from patchwork.models import (Project, Person, UserProfile, State, Submission,
                               Patch, CoverLetter, Comment, Bundle, Tag, Check,
-                              DelegationRule)
+                              DelegationRule, SeriesGroup, Series,
+                              SeriesReference)
 
 
 class DelegationRuleInline(admin.TabularInline):
@@ -76,8 +77,8 @@ admin.site.register(CoverLetter, CoverLetterAdmin)
 
 class PatchAdmin(admin.ModelAdmin):
     list_display = ('name', 'submitter', 'project', 'state', 'date',
-                    'archived', 'is_pull_request')
-    list_filter = ('project', 'state', 'archived')
+                    'archived', 'is_pull_request', 'series')
+    list_filter = ('project', 'state', 'archived', 'series')
     search_fields = ('name', 'submitter__name', 'submitter__email')
     date_hierarchy = 'date'
 
@@ -97,6 +98,54 @@ class CommentAdmin(admin.ModelAdmin):
 admin.site.register(Comment, CommentAdmin)
 
 
+class CoverLetterInline(admin.StackedInline):
+    model = CoverLetter
+    extra = 0
+
+
+class PatchInline(admin.StackedInline):
+    model = Patch
+    extra = 0
+
+
+class SeriesAdmin(admin.ModelAdmin):
+    list_display = ('name', 'group', 'date', 'submitter', 'version', 'total',
+                    'actual_total', 'complete')
+    readonly_fields = ('actual_total', 'complete')
+    search_fields = ('submitter_name', 'submitter_email')
+    inlines = [CoverLetterInline, PatchInline]
+
+    def complete(self, series):
+        return series.complete
+    complete.boolean = True
+admin.site.register(Series, SeriesAdmin)
+
+
+class SeriesInline(admin.StackedInline):
+    model = Series
+    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 SeriesGroupAdmin(admin.ModelAdmin):
+    list_display = ('name', )
+    readonly_fields = ('name', )
+    inlines = [SeriesInline]
+admin.site.register(SeriesGroup, SeriesGroupAdmin)
+
+
+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/0013_add_series_models.py b/patchwork/migrations/0013_add_series_models.py
new file mode 100644
index 0000000..b9add68
--- /dev/null
+++ b/patchwork/migrations/0013_add_series_models.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('patchwork', '0012_add_coverletter_model'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Series',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=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)')),
+            ],
+            options={
+                'verbose_name_plural': 'Series',
+            },
+        ),
+        migrations.CreateModel(
+            name='SeriesGroup',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='SeriesReference',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('msgid', models.CharField(unique=True, max_length=255)),
+                ('series', models.ForeignKey(related_query_name=b'reference', related_name='references', to='patchwork.Series')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='series',
+            name='group',
+            field=models.ForeignKey(related_query_name=b'revision', related_name='revisions', blank=True, to='patchwork.SeriesGroup', null=True),
+        ),
+        migrations.AddField(
+            model_name='series',
+            name='submitter',
+            field=models.ForeignKey(to='patchwork.Person'),
+        ),
+        migrations.AddField(
+            model_name='coverletter',
+            name='series',
+            field=models.OneToOneField(related_name='cover_letter', null=True, blank=True, to='patchwork.Series'),
+        ),
+        migrations.AddField(
+            model_name='patch',
+            name='series',
+            field=models.ForeignKey(related_query_name=b'patch', related_name='unique_patches', blank=True, to='patchwork.Series', null=True),
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index 4ec2fe8..e0a3e3a 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -37,6 +37,9 @@ from django.utils.six.moves import filter
 from patchwork.fields import HashField
 from patchwork.parser import extract_tags, hash_patch
 
+SERIES_DEFAULT_NAME = 'Unnamed Series'
+SERIESGROUP_DEFAULT_NAME = 'Unnamed Series Group'
+
 
 @python_2_unicode_compatible
 class Person(models.Model):
@@ -182,6 +185,100 @@ models.signals.post_save.connect(_user_saved_callback, sender=User)
 
 
 @python_2_unicode_compatible
+class SeriesGroup(models.Model):
+    """A collection of series.
+
+    Provides a way to group series revisions such that finding a series
+    revisions won't require a large amount of computation each time.
+    The model itself should never be exposed to users - instead expose
+    the individual revisions.
+    """
+
+    @property
+    def name(self):
+        # TODO(stephenfin) If there is a non-empty revision name we
+        # should use that instead
+        if self.latest_revision:
+            return self.latest_revision.name
+        return SERIESGROUP_DEFAULT_NAME
+
+    @property
+    def latest_revision(self):
+        # TODO(stephenfin) Can this raise an exception?
+        return self.revisions.latest('date')
+
+    def __str__(self):
+        return self.name
+
+
+ at python_2_unicode_compatible
+class Series(models.Model):
+
+    group = models.ForeignKey(SeriesGroup, related_name='revisions',
+                              related_query_name='revision', null=True,
+                              blank=True)
+    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)')
+
+    @cached_property
+    def name(self):
+        try:
+            return self.cover_letter.name
+        except CoverLetter.DoesNotExist:
+            return SERIES_DEFAULT_NAME
+
+    @property
+    def actual_total(self):
+        return Patch.objects.filter(series=self).count()
+
+    @property
+    def complete(self):
+        return self.total == self.actual_total
+
+    @property
+    def patches(self):
+        """Return patches associated with this series .
+
+        Eventually this will "autofill" a series revision by pulling in
+        missing patches from prior revisions, where possible. For now,
+        however, this just lets us retrieve the patches created for
+        this given revision.
+
+        Returns:
+            The patches in the revision.
+        """
+        return self.unique_patches.all()
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name_plural = 'Series'
+
+
+ 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
+
+
+ at python_2_unicode_compatible
 class State(models.Model):
     name = models.CharField(max_length=100)
     ordering = models.IntegerField(unique=True)
@@ -295,7 +392,7 @@ class EmailMixin(models.Model):
 
 @python_2_unicode_compatible
 class Submission(EmailMixin, models.Model):
-    # parent
+    # parents
 
     project = models.ForeignKey(Project)
 
@@ -320,11 +417,20 @@ class Submission(EmailMixin, models.Model):
 
 
 class CoverLetter(Submission):
-    pass
+    # parents
+
+    series = models.OneToOneField(Series, related_name='cover_letter',
+                                  null=True, blank=True)
 
 
 @python_2_unicode_compatible
 class Patch(Submission):
+    # parents
+
+    series = models.ForeignKey(Series, related_name='unique_patches',
+                               related_query_name='patch',
+                               null=True, blank=True)
+
     # patch metadata
 
     diff = models.TextField(null=True, blank=True)
-- 
2.0.0



More information about the Patchwork mailing list