[PATCH v6 2/8] models: Add 'SeriesRevision' model
Stephen Finucane
stephen at that.guru
Sun Oct 16 23:50:48 AEDT 2016
Add a series revision 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>
---
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 | 67 ++++++++-
patchwork/migrations/0015_add_series_models.py | 67 +++++++++
patchwork/models.py | 186 +++++++++++++++++++++++--
3 files changed, 302 insertions(+), 18 deletions(-)
create mode 100644 patchwork/migrations/0015_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/0015_add_series_models.py b/patchwork/migrations/0015_add_series_models.py
new file mode 100644
index 0000000..4d4598e
--- /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='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 d0ef44d..49572ec 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -292,7 +292,7 @@ class EmailMixin(models.Model):
@python_2_unicode_compatible
class Submission(EmailMixin, models.Model):
- # parent
+ # parents
project = models.ForeignKey(Project)
@@ -317,11 +317,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)
@@ -418,17 +434,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
@@ -439,6 +444,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)
@@ -545,6 +566,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'
@@ -568,6 +600,134 @@ 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.
+ """
+
+ 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
+
+ # don't override user-defined names
+ if not self.name:
+ self.name = _format_name(cover)
+ # ...but give cover letter-based names precedence over patch-based
+ # names
+ else:
+ try:
+ name = SeriesRevisionPatch.objects.get(revision=self,
+ number=1).patch.name
+ except SeriesRevisionPatch.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 revision."""
+ # see if the patch is already in this series
+ if SeriesRevisionPatch.objects.filter(revision=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 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