diff options
author | Stephen Finucane <stephen@that.guru> | 2016-10-29 14:13:34 +0100 |
---|---|---|
committer | Stephen Finucane <stephen@that.guru> | 2016-10-31 16:06:23 +0000 |
commit | d67d859f40f49b3c10efe5fdbff54797bdd7ccec (patch) | |
tree | c7bbd50dcbfe72e0a0301e67e1bd5f000ead3764 | |
parent | 08407e9c2058120e5db068b801c758b5434e00a0 (diff) | |
download | patchwork-d67d859f40f49b3c10efe5fdbff54797bdd7ccec.tar patchwork-d67d859f40f49b3c10efe5fdbff54797bdd7ccec.tar.gz |
models: Add 'Series' model
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>
-rw-r--r-- | patchwork/admin.py | 54 | ||||
-rw-r--r-- | patchwork/migrations/0015_add_series_models.py | 67 | ||||
-rw-r--r-- | patchwork/models.py | 160 |
3 files changed, 276 insertions, 5 deletions
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')] +@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) |