[PATCH v5 2/7] models: Add 'Series' model and related models
Daniel Axtens
dja at axtens.net
Thu Oct 13 10:26:48 AEDT 2016
Hi Stephen,
This doesn't have my fix for reducing DB queries:
http://patchwork.ozlabs.org/patch/670292/
If you are respinning, please consider including it. Otherwise it'll
need to be a follow-up patch - preferably merged at the same time.
Regards,
Daniel
Stephen Finucane <stephen at that.guru> writes:
> 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 at that.guru>
> ---
> 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/0014_add_series_models.py | 67 +++++++++++
> patchwork/models.py | 158 +++++++++++++++++++++++--
> 3 files changed, 274 insertions(+), 18 deletions(-)
> create mode 100644 patchwork/migrations/0014_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/0014_add_series_models.py b/patchwork/migrations/0014_add_series_models.py
> new file mode 100644
> index 0000000..8d0fffa
> --- /dev/null
> +++ b/patchwork/migrations/0014_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', '0013_slug_check_context'),
> + ]
> +
> + 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 28e9861..4a55c1d 100644
> --- a/patchwork/models.py
> +++ b/patchwork/models.py
> @@ -293,7 +293,7 @@ class EmailMixin(models.Model):
>
> @python_2_unicode_compatible
> class Submission(EmailMixin, models.Model):
> - # parent
> + # parents
>
> project = models.ForeignKey(Project)
>
> @@ -318,11 +318,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)
> @@ -419,17 +435,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
> @@ -440,6 +445,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)
> @@ -546,6 +567,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'
>
> @@ -569,6 +601,106 @@ 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.
> + """
> + self.cover_letter = cover
> +
> + if not self.name: # don't override user-defined names
> + self.name = cover.name.split(']')[-1]
> +
> + 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():
> + return
> +
> + 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
>
> _______________________________________________
> Patchwork mailing list
> Patchwork at lists.ozlabs.org
> https://lists.ozlabs.org/listinfo/patchwork
More information about the Patchwork
mailing list