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

Stephen Finucane stephenfinucane at hotmail.com
Tue Sep 13 07:53:28 AEST 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 <stephenfinucane at hotmail.com>
Co-authored-by: Andrew Donnellan <andrew.donnellan at au1.ibm.com>
Reviewed-by: Andy Doan <andy.doan at linaro.org>
---
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                             |  70 ++++++++++++++--
 patchwork/migrations/0014_add_series_models.py |  56 +++++++++++++
 patchwork/models.py                            | 108 ++++++++++++++++++++++++-
 3 files changed, 227 insertions(+), 7 deletions(-)
 create mode 100644 patchwork/migrations/0014_add_series_models.py

diff --git a/patchwork/admin.py b/patchwork/admin.py
index 0f217d7..89b074f 100644
--- a/patchwork/admin.py
+++ b/patchwork/admin.py
@@ -21,9 +21,21 @@ 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 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):
@@ -76,8 +88,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 +109,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 SeriesRevisionAdmin(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(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 SeriesAdmin(admin.ModelAdmin):
+    list_display = ('name', )
+    readonly_fields = ('name', )
+    inlines = [SeriesRevisionInline]
+admin.site.register(Series, SeriesAdmin)
+
+
+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..ea801b1
--- /dev/null
+++ b/patchwork/migrations/0014_add_series_models.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('patchwork', '0013_slug_check_context'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Series',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+            ],
+            options={
+                'verbose_name_plural': 'Series',
+            },
+        ),
+        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)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='SeriesRevision',
+            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)')),
+                ('group', models.ForeignKey(related_query_name=b'revision', related_name='revisions', blank=True, to='patchwork.Series', null=True)),
+                ('submitter', models.ForeignKey(to='patchwork.Person')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='seriesreference',
+            name='series',
+            field=models.ForeignKey(related_query_name=b'reference', related_name='references', to='patchwork.SeriesRevision'),
+        ),
+        migrations.AddField(
+            model_name='coverletter',
+            name='series',
+            field=models.OneToOneField(related_name='cover_letter', null=True, blank=True, to='patchwork.SeriesRevision'),
+        ),
+        migrations.AddField(
+            model_name='patch',
+            name='series',
+            field=models.ForeignKey(related_query_name=b'patch', related_name='unique_patches', blank=True, to='patchwork.SeriesRevision', null=True),
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index 3ab90e1..2875369 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -182,6 +182,101 @@ models.signals.post_save.connect(_user_saved_callback, sender=User)
 
 
 @python_2_unicode_compatible
+class Series(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 '[Series #%d]' % self.id
+
+    @property
+    def latest_revision(self):
+        # TODO(stephenfin) Can this raise an exception?
+        return self.revisions.latest('date')
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name_plural = 'Series'
+
+
+ at python_2_unicode_compatible
+class SeriesRevision(models.Model):
+
+    group = models.ForeignKey(Series, 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 #%d, revision #%d]' % (self.group.id,
+                                                   self.version)
+
+    @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
+
+
+ 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
+
+
+ at python_2_unicode_compatible
 class State(models.Model):
     name = models.CharField(max_length=100)
     ordering = models.IntegerField(unique=True)
@@ -295,7 +390,7 @@ class EmailMixin(models.Model):
 
 @python_2_unicode_compatible
 class Submission(EmailMixin, models.Model):
-    # parent
+    # parents
 
     project = models.ForeignKey(Project)
 
@@ -320,11 +415,20 @@ class Submission(EmailMixin, models.Model):
 
 
 class CoverLetter(Submission):
-    pass
+    # parents
+
+    series = models.OneToOneField(SeriesRevision, related_name='cover_letter',
+                                  null=True, blank=True)
 
 
 @python_2_unicode_compatible
 class Patch(Submission):
+    # parents
+
+    series = models.ForeignKey(SeriesRevision, related_name='unique_patches',
+                               related_query_name='patch',
+                               null=True, blank=True)
+
     # patch metadata
 
     diff = models.TextField(null=True, blank=True)
-- 
2.7.4



More information about the Patchwork mailing list