[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