]> git.ipfire.org Git - thirdparty/patchwork.git/commitdiff
models: Add 'Series' model
authorStephen Finucane <stephen@that.guru>
Sat, 29 Oct 2016 13:13:34 +0000 (14:13 +0100)
committerStephen Finucane <stephen@that.guru>
Mon, 31 Oct 2016 16:06:23 +0000 (16:06 +0000)
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@that.guru>
Reviewed-by: Andy Doan <andy.doan@linaro.org>
Reviewed-by: Daniel Axtens <dja@axtens.net>
Reviewed-by: Andrew Donnellan <andrew.donnellan@au1.ibm.com>
Tested-by: Russell Currey <ruscur@russell.cc>
patchwork/admin.py
patchwork/migrations/0015_add_series_models.py [new file with mode: 0644]
patchwork/models.py

index 85ffecfc768d09c5a7940310ec7a6b116ce3ad9a..ef041c4a38320716a0dbb8bca8287a8760bf1d3f 100644 (file)
@@ -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 (file)
index 0000000..b7c3dc7
--- /dev/null
@@ -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')]),
+        ),
+    ]
index 8a9762a2754b71a22fe8b44fe9b3e8bc6aaaf502..a27dda65649adf8258045b063bf0811410780cef 100644 (file)
@@ -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')]
 
 
+@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',)
+
+
+@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']
+
+
+@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)