[PATCH v3 3/5] models: Convert Series-Patch relationship to 1:N

Daniel Axtens dja at axtens.net
Tue Oct 2 16:28:41 AEST 2018


Stephen Finucane <stephen at that.guru> writes:

> On Tue, 2018-10-02 at 00:18 +1000, Daniel Axtens wrote:
>> Hi Stephen,
>> 
>> > +class Migration(migrations.Migration):
>> > +
>> > +    dependencies = [
>> > +        ('patchwork', '0031_add_patch_series_fields'),
>> > +    ]
>> > +
>> > +    operations = [
>> > +        # Copy SeriesPatch.series, SeriesPatch.number to Patch.series_alt,
>> > +        # Patch.number. Note that there is no uniqueness check here because no
>> > +        # code actually allowed us to save multiple series
>> > +        migrations.RunSQL(
>> > +            """UPDATE patchwork_patch SET series_alt_id =
>> > +                  (SELECT series_id from patchwork_seriespatch
>> > +                   WHERE patchwork_seriespatch.patch_id =
>> > +                            patchwork_patch.submission_ptr_id);
>> > +               UPDATE patchwork_patch SET number =
>> > +                   (SELECT number from patchwork_seriespatch
>> > +                    WHERE patchwork_seriespatch.patch_id =
>> > +                             patchwork_patch.submission_ptr_id);
>> > +            """,
>> 
>> I get these two statements, but
>> 
>> > +            """INSERT INTO patchwork_seriespatch
>> > +                  (patch_id, series_id, number)
>> > +                SELECT submission_ptr_id, series_alt_id, number
>> > +                FROM patchwork_patch;
>> > +            """,
>> 
>> I don't understand this. What is the goal of inserting stuff into
>> patchwork_seriespatch? Aren't we just about to delete it?
>
> This is the reverse statement which allows us to undo the migration
> (it's a second argument). Where possible, I try to include these as
> they're very useful during testing.

Ah. D'oh. Silly me. Carry on.

>> 
>> > +        ),
>> > +    ]
>> > diff --git a/patchwork/migrations/0033_remove_patch_series_model.py b/patchwork/migrations/0033_remove_patch_series_model.py
>> > new file mode 100644
>> > index 00000000..a9ea2e20
>> > --- /dev/null
>> > +++ b/patchwork/migrations/0033_remove_patch_series_model.py
>> > @@ -0,0 +1,58 @@
>> > +# -*- 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', '0032_migrate_data_from_series_patch_to_patch'),
>> > +    ]
>> > +
>> > +    operations = [
>> > +        # Remove SeriesPatch
>> > +        migrations.AlterUniqueTogether(
>> > +            name='seriespatch',
>> > +            unique_together=set([]),
>> > +        ),
>> > +        migrations.RemoveField(
>> > +            model_name='seriespatch',
>> > +            name='patch',
>> > +        ),
>> > +        migrations.RemoveField(
>> > +            model_name='seriespatch',
>> > +            name='series',
>> > +        ),
>> 
>> Are these 3 migrations required given that we're about to delete the
>> model? I would have thought Django would be smart enough to delete the
>> parts of the SeriesPatch model itself...?
>
> To be clear, this bit was autogenerated. From the looks of this and
> previous migrations though, it seems Django requires all references to
> be removed one by one. I don't really want to twiddle with this lest I
> break something, heh.

Ok. Fair enough.

>> 
>> > +        migrations.RemoveField(
>> > +            model_name='series',
>> > +            name='patches',
>> > +        ),
>> > +        migrations.DeleteModel(
>> > +            name='SeriesPatch',
>> > +        ),
>> > +        # Now that SeriesPatch has been removed, we can use the now-unused
>> > +        # Patch.series field and add a backreference
>> > +        migrations.RenameField(
>> > +            model_name='patch',
>> > +            old_name='series_alt',
>> > +            new_name='series',
>> > +        ),
>> > +        migrations.AlterField(
>> > +            model_name='patch',
>> > +            name='series',
>> > +            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='patches', related_query_name='patch', to='patchwork.Series'),
>> 
>> When you created series_alt, it was defined as
>> > +            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='patchwork.Series'),
>> 
>> So it looks like you're addig related_name and related_query_name
>> here. Would there be any issue with:
>>  - moving the deletion of 'patches' from Series up to migration 1, then
>>  - defining series_alt with the full related_[query_]name in migration
>>     1, and
>>  - just having the rename here?
>
> Hmm, I'm not sure. If I recall correctly, removing the attribute in one
> migration would break references to this and therefore prevent us
> applying the other two migrations. I'll try it though, to be sure.
>

Sure, I thought that might be the case, just thought it would be worth seeing.

>> I haven't tried this, I'm just trying to get the migrations straight in
>> my head - both for my own satisfaction and in hopes that anyone who has
>> to apply them manually to a complicated setup will be able to get it right.
>> 
>> Apart from that I think this mostly looks good - I haven't properly
>> looked at the event handling code or the view changes yet but from a
>> quick skim they seem in order. I'll give the series a spin on my
>> instance next.

This is still my plan.

Thanks,
Daniel

>> 
>> Regards,
>> Daniel
>> 
>> > +        ),
>> > +        migrations.AlterUniqueTogether(
>> > +            name='patch',
>> > +            unique_together=set([('series', 'number')]),
>> > +        ),
>> > +        # Migrate CoverLetter to OneToOneField as a cover letter can no longer
>> > +        # be assigned to multiple series
>> > +        migrations.AlterField(
>> > +            model_name='series',
>> > +            name='cover_letter',
>> > +            field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='series', to='patchwork.CoverLetter'),
>> > +        ),
>> > +    ]
>> > diff --git a/patchwork/models.py b/patchwork/models.py
>> > index a043844d..a483dc64 100644
>> > --- a/patchwork/models.py
>> > +++ b/patchwork/models.py
>> > @@ -407,6 +407,15 @@ class Patch(Submission):
>> >      # patches in a project without needing to do a JOIN.
>> >      patch_project = models.ForeignKey(Project, on_delete=models.CASCADE)
>> >  
>> > +    # series metadata
>> > +
>> > +    series = models.ForeignKey(
>> > +        'Series', null=True, blank=True, on_delete=models.CASCADE,
>> > +        related_name='patches', related_query_name='patch')
>> > +    number = models.PositiveSmallIntegerField(
>> > +        default=None, null=True,
>> > +        help_text='The number assigned to this patch in the series')
>> > +
>> >      objects = PatchManager()
>> >  
>> >      @staticmethod
>> > @@ -563,6 +572,7 @@ class Patch(Submission):
>> >      class Meta:
>> >          verbose_name_plural = 'Patches'
>> >          base_manager_name = 'objects'
>> > +        unique_together = [('series', 'number')]
>> >  
>> >          indexes = [
>> >              # This is a covering index for the /list/ query
>> > @@ -606,19 +616,16 @@ class Comment(EmailMixin, models.Model):
>> >  
>> >  @python_2_unicode_compatible
>> >  class Series(FilenameMixin, models.Model):
>> > -    """An collection of patches."""
>> > +    """A collection of patches."""
>> >  
>> >      # parent
>> >      project = models.ForeignKey(Project, related_name='series', null=True,
>> >                                  blank=True, on_delete=models.CASCADE)
>> >  
>> >      # content
>> > -    cover_letter = models.ForeignKey(CoverLetter,
>> > -                                     related_name='series',
>> > -                                     null=True, blank=True,
>> > -                                     on_delete=models.CASCADE)
>> > -    patches = models.ManyToManyField(Patch, through='SeriesPatch',
>> > -                                     related_name='series')
>> > +    cover_letter = models.OneToOneField(CoverLetter, related_name='series',
>> > +                                        null=True,
>> > +                                        on_delete=models.CASCADE)
>> >  
>> >      # metadata
>> >      name = models.CharField(max_length=255, blank=True, null=True,
>> > @@ -684,9 +691,8 @@ class Series(FilenameMixin, models.Model):
>> >              self.name = self._format_name(cover)
>> >          else:
>> >              try:
>> > -                name = SeriesPatch.objects.get(series=self,
>> > -                                               number=1).patch.name
>> > -            except SeriesPatch.DoesNotExist:
>> > +                name = Patch.objects.get(series=self, number=1).name
>> > +            except Patch.DoesNotExist:
>> >                  name = None
>> >  
>> >              if self.name == name:
>> > @@ -696,20 +702,16 @@ class Series(FilenameMixin, models.Model):
>> >  
>> >      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)
>> > +        patch.series = self
>> > +        patch.number = number
>> > +        patch.save()
>> > +
>> > +        return patch
>> > 
>> 
>> I get a bit lost on who throws and who catches exceptions, but do we
>> need to be catching an integrity error here if I send out for example 2
>> patches with the same number? (I've done this in real life before by
>> mistake.)
>
> In theory, yes, but I remove this whole thing later in the series. If I
> need to respin though, I'll add the check to be safe.
>
> Cheers,
> Stephen
>
>> 
>> >      def get_absolute_url(self):
>> >          # TODO(stephenfin): We really need a proper series view
>> > @@ -727,26 +729,6 @@ class Series(FilenameMixin, models.Model):
>> >          verbose_name_plural = 'Series'
>> >  
>> >  
>> > - 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, on_delete=models.CASCADE)
>> > -    series = models.ForeignKey(Series, on_delete=models.CASCADE)
>> > -    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.
>> > diff --git a/patchwork/parser.py b/patchwork/parser.py
>> > index 2ba1db74..bc9dae2f 100644
>> > --- a/patchwork/parser.py
>> > +++ b/patchwork/parser.py
>> > @@ -27,7 +27,6 @@ from patchwork.models import Person
>> >  from patchwork.models import Project
>> >  from patchwork.models import Series
>> >  from patchwork.models import SeriesReference
>> > -from patchwork.models import SeriesPatch
>> >  from patchwork.models import State
>> >  from patchwork.models import Submission
>> >  
>> > @@ -1034,8 +1033,7 @@ def parse_mail(mail, list_id=None):
>> >          # - there is no existing series to assign this patch to, or
>> >          # - there is an existing series, but it already has a patch with this
>> >          #   number in it
>> > -        if not series or (
>> > -                SeriesPatch.objects.filter(series=series, number=x).count()):
>> > +        if not series or Patch.objects.filter(series=series, number=x).count():
>> >              series = Series(project=project,
>> >                              date=date,
>> >                              submitter=author,
>> > @@ -1069,6 +1067,8 @@ def parse_mail(mail, list_id=None):
>> >          # patch. Don't add unnumbered patches (for example diffs sent
>> >          # in reply, or just messages with random refs/in-reply-tos)
>> >          if series and x:
>> > +            # TODO(stephenfin): Remove 'series' from the conditional as we will
>> > +            # always have a series
>> >              series.add_patch(patch, x)
>> >  
>> >          return patch
>> > diff --git a/patchwork/signals.py b/patchwork/signals.py
>> > index b7b8e6f5..536b177e 100644
>> > --- a/patchwork/signals.py
>> > +++ b/patchwork/signals.py
>> > @@ -15,7 +15,6 @@ from patchwork.models import Event
>> >  from patchwork.models import Patch
>> >  from patchwork.models import PatchChangeNotification
>> >  from patchwork.models import Series
>> > -from patchwork.models import SeriesPatch
>> >  
>> >  
>> >  @receiver(pre_save, sender=Patch)
>> > @@ -133,39 +132,46 @@ def create_patch_delegated_event(sender, instance, raw, **kwargs):
>> >      create_event(instance, orig_patch.delegate, instance.delegate)
>> >  
>> >  
>> > - at receiver(post_save, sender=SeriesPatch)
>> > -def create_patch_completed_event(sender, instance, created, raw, **kwargs):
>> > -    """Create patch completed event for patches with series."""
>> > + at receiver(pre_save, sender=Patch)
>> > +def create_patch_completed_event(sender, instance, raw, **kwargs):
>> >  
>> > -    def create_event(patch, series):
>> > +    def create_event(patch):
>> >          return Event.objects.create(
>> >              category=Event.CATEGORY_PATCH_COMPLETED,
>> >              project=patch.project,
>> >              patch=patch,
>> > -            series=series)
>> > +            series=patch.series)
>> >  
>> > -    # don't trigger for items loaded from fixtures or existing items
>> > -    if raw or not created:
>> > +    # don't trigger for items loaded from fixtures, new items or items that
>> > +    # (still) don't have a series
>> > +    if raw or not instance.pk or not instance.series:
>> > +        return
>> > +
>> > +    orig_patch = Patch.objects.get(pk=instance.pk)
>> > +
>> > +    # we don't currently allow users to change a series, though this might
>> > +    # change in the future. However, we handle that here nonetheless
>> > +    if orig_patch.series == instance.series:
>> >          return
>> >  
>> >      # if dependencies not met, don't raise event. There's also no point raising
>> >      # events for successors since they'll have the same issue
>> > -    predecessors = SeriesPatch.objects.filter(
>> > +    predecessors = Patch.objects.filter(
>> >          series=instance.series, number__lt=instance.number)
>> >      if predecessors.count() != instance.number - 1:
>> >          return
>> >  
>> > -    create_event(instance.patch, instance.series)
>> > +    create_event(instance)
>> >  
>> >      # if this satisfies dependencies for successor patch, raise events for
>> >      # those
>> >      count = instance.number + 1
>> > -    for successor in SeriesPatch.objects.filter(
>> > +    for successor in Patch.objects.order_by('number').filter(
>> >              series=instance.series, number__gt=instance.number):
>> >          if successor.number != count:
>> >              break
>> >  
>> > -        create_event(successor.patch, successor.series)
>> > +        create_event(successor)
>> >          count += 1
>> >  
>> >  
>> > @@ -204,15 +210,9 @@ def create_series_created_event(sender, instance, created, raw, **kwargs):
>> >      create_event(instance)
>> >  
>> >  
>> > - at receiver(post_save, sender=SeriesPatch)
>> > + at receiver(post_save, sender=Patch)
>> >  def create_series_completed_event(sender, instance, created, raw, **kwargs):
>> >  
>> > -    # NOTE(stephenfin): We subscribe to the SeriesPatch.post_save signal
>> > -    # instead of Series.m2m_changed to minimize the amount of times this is
>> > -    # fired. The m2m_changed signal doesn't support a 'changed' parameter,
>> > -    # which we could use to quick skip the signal when a patch is merely
>> > -    # updated instead of added to the series.
>> > -
>> >      # NOTE(stephenfin): It's actually possible for this event to be fired
>> >      # multiple times for a given series. To trigger this case, you would need
>> >      # to send an additional patch to already exisiting series. This pattern
>> > @@ -229,5 +229,5 @@ def create_series_completed_event(sender, instance, created, raw, **kwargs):
>> >      if raw or not created:
>> >          return
>> >  
>> > -    if instance.series.received_all:
>> > +    if instance.series and instance.series.received_all:
>> >          create_event(instance.series)
>> > diff --git a/patchwork/templates/patchwork/download_buttons.html b/patchwork/templates/patchwork/download_buttons.html
>> > index 32acf26b..21933bd2 100644
>> > --- a/patchwork/templates/patchwork/download_buttons.html
>> > +++ b/patchwork/templates/patchwork/download_buttons.html
>> > @@ -15,22 +15,9 @@
>> >     class="btn btn-default" role="button" title="Download cover mbox"
>> >     >mbox</a>
>> >    {% endif %}
>> > -  {% if all_series|length == 1 %}
>> > -  {% with all_series|first as series %}
>> > -  <a href="{% url 'series-mbox' series_id=series.id %}"
>> > +  {% if submission.series %}
>> > +  <a href="{% url 'series-mbox' series_id=submission.series.id %}"
>> >     class="btn btn-default" role="button"
>> >     title="Download patch mbox with dependencies">series</a>
>> > -  {% endwith %}
>> > -  {% elif all_series|length > 1 %}
>> > -  <button type="button" class="btn btn-default dropdown-toggle"
>> > -   data-toggle="dropdown">
>> > -   series <span class="caret"></span>
>> > -  </button>
>> > -  <ul class="dropdown-menu" role="menu">
>> > -  {% for series in all_series %}
>> > -   <li><a href="{% url 'series-mbox' series_id=series.id %}"
>> > -    >{{ series }}</a></li>
>> > -  {% endfor %}
>> > -  </ul>
>> >    {% endif %}
>> >  </div>
>> > diff --git a/patchwork/templates/patchwork/patch-list.html b/patchwork/templates/patchwork/patch-list.html
>> > index 71c1ba92..9dcee1c8 100644
>> > --- a/patchwork/templates/patchwork/patch-list.html
>> > +++ b/patchwork/templates/patchwork/patch-list.html
>> > @@ -194,13 +194,11 @@ $(document).ready(function() {
>> >      </a>
>> >     </td>
>> >     <td>
>> > -    {% with patch.series.all.0 as series %}
>> > -     {% if series %}
>> > -     <a href="?series={{series.id}}">
>> > -      {{ series|truncatechars:100 }}
>> > -     </a>
>> > -     {% endif %}
>> > -    {% endwith %}
>> > +    {% if patch.series %}
>> > +    <a href="?series={{patch.series.id}}">
>> > +     {{ patch.series|truncatechars:100 }}
>> > +    </a>
>> > +    {% endif %}
>> >     </td>
>> >     <td class="text-nowrap">{{ patch|patch_tags }}</td>
>> >     <td class="text-nowrap">{{ patch|patch_checks }}</td>
>> > diff --git a/patchwork/templates/patchwork/submission.html b/patchwork/templates/patchwork/submission.html
>> > index f03e1408..ceb88af3 100644
>> > --- a/patchwork/templates/patchwork/submission.html
>> > +++ b/patchwork/templates/patchwork/submission.html
>> > @@ -64,25 +64,13 @@ function toggle_div(link_id, headers_id)
>> >     </div>
>> >    </td>
>> >   </tr>
>> > -{% if all_series %}
>> > +{% if submission.series %}
>> >   <tr>
>> >    <th>Series</th>
>> >    <td>
>> > -   <div class="patchrelations">
>> > -    <ul>
>> > -     {% for series in all_series %}
>> > -     <li>
>> > -      {% if forloop.first %}
>> > -       {{ series }}
>> > -      {% else %}
>> > -       <a href="{% url 'patch-list' project_id=project.linkname %}?series={{ series.id }}">
>> > -       {{ series }}
>> > -       </a>
>> > -      {% endif %}
>> > -     </li>
>> > -    {% endfor %}
>> > -    </ul>
>> > -   </div>
>> > +   <a href="{% url 'patch-list' project_id=project.linkname %}?series={{ submission.series.id }}">
>> > +    {{ submission.series }}
>> > +   </a>
>> >    </td>
>> >   </tr>
>> >   <tr>
>> > @@ -92,9 +80,8 @@ function toggle_div(link_id, headers_id)
>> >        href="javascript:toggle_div('togglepatchrelations', 'patchrelations')"
>> >     >show</a>
>> >     <div id="patchrelations" class="patchrelations" style="display:none;">
>> > -    {% for series in all_series %}
>> >      <ul>
>> > -    {% with series.cover_letter as cover %}
>> > +    {% with submission.series.cover_letter as cover %}
>> >       <li>
>> >       {% if cover %}
>> >        {% if cover == submission %}
>> > @@ -107,7 +94,7 @@ function toggle_div(link_id, headers_id)
>> >       {% endif %}
>> >       </li>
>> >      {% endwith %}
>> > -    {% for sibling in series.patches.all %}
>> > +    {% for sibling in submission.series.patches.all %}
>> >       <li>
>> >        {% if sibling == submission %}
>> >         {{ sibling.name|default:"[no subject]"|truncatechars:100 }}
>> > @@ -119,7 +106,6 @@ function toggle_div(link_id, headers_id)
>> >       </li>
>> >      {% endfor %}
>> >      </ul>
>> > -    {% endfor %}
>> >     </div>
>> >    </td>
>> >   </tr>
>> > diff --git a/patchwork/tests/test_detail.py b/patchwork/tests/test_detail.py
>> > index 9c44779a..4ca1c9cd 100644
>> > --- a/patchwork/tests/test_detail.py
>> > +++ b/patchwork/tests/test_detail.py
>> > @@ -9,7 +9,6 @@ from django.urls import reverse
>> >  from patchwork.tests.utils import create_comment
>> >  from patchwork.tests.utils import create_cover
>> >  from patchwork.tests.utils import create_patch
>> > -from patchwork.tests.utils import create_series
>> >  
>> >  
>> >  class CoverLetterViewTest(TestCase):
>> > @@ -35,21 +34,6 @@ class PatchViewTest(TestCase):
>> >          response = self.client.get(requested_url)
>> >          self.assertRedirects(response, redirect_url)
>> >  
>> > -    def test_series_dropdown(self):
>> > -        patch = create_patch()
>> > -        series = [create_series() for x in range(5)]
>> > -
>> > -        for series_ in series:
>> > -            series_.add_patch(patch, 1)
>> > -
>> > -        response = self.client.get(
>> > -            reverse('patch-detail', kwargs={'patch_id': patch.id}))
>> > -
>> > -        for series_ in series:
>> > -            self.assertContains(
>> > -                response,
>> > -                reverse('series-mbox', kwargs={'series_id': series_.id}))
>> > -
>> >  
>> >  class CommentRedirectTest(TestCase):
>> >  
>> > diff --git a/patchwork/tests/test_events.py b/patchwork/tests/test_events.py
>> > index b9952f64..bd4f9be1 100644
>> > --- a/patchwork/tests/test_events.py
>> > +++ b/patchwork/tests/test_events.py
>> > @@ -34,8 +34,8 @@ class PatchCreateTest(_BaseTestCase):
>> >          """No series, so patch dependencies implicitly exist."""
>> >          patch = utils.create_patch()
>> >  
>> > -        # This should raise both the CATEGORY_PATCH_CREATED and
>> > -        # CATEGORY_PATCH_COMPLETED events as there are no specific dependencies
>> > +        # This should raise the CATEGORY_PATCH_CREATED event only as there is
>> > +        # no series
>> >          events = _get_events(patch=patch)
>> >          self.assertEqual(events.count(), 1)
>> >          self.assertEqual(events[0].category, Event.CATEGORY_PATCH_CREATED)
>> > @@ -57,6 +57,13 @@ class PatchCreateTest(_BaseTestCase):
>> >          self.assertEventFields(events[0])
>> >          self.assertEventFields(events[1])
>> >  
>> > +        # This shouldn't be affected by another update to the patch
>> > +        series_patch.patch.commit_ref = 'aac76f0b0f8dd657ff07bb'
>> > +        series_patch.patch.save()
>> > +
>> > +        events = _get_events(patch=series_patch.patch)
>> > +        self.assertEqual(events.count(), 2)
>> > +
>> >      def test_patch_dependencies_out_of_order(self):
>> >          series = utils.create_series()
>> >          series_patch_3 = utils.create_series_patch(series=series, number=3)
>> > diff --git a/patchwork/tests/test_series.py b/patchwork/tests/test_series.py
>> > index ff3412e6..f5e6b5b0 100644
>> > --- a/patchwork/tests/test_series.py
>> > +++ b/patchwork/tests/test_series.py
>> > @@ -73,7 +73,15 @@ class _BaseTestCase(TestCase):
>> >  
>> >              patches_ = patches[start_idx:end_idx]
>> >              for patch in patches_:
>> > -                self.assertEqual(patch.series.first(), series[idx])
>> > +                self.assertEqual(patch.series, series[idx])
>> > +
>> > +                # TODO(stephenfin): Rework this function into two different
>> > +                # functions - we're clearly not always testing patches here
>> > +                if isinstance(patch, models.Patch):
>> > +                    self.assertEqual(series[idx].patches.get(id=patch.id),
>> > +                                     patch)
>> > +                else:
>> > +                    self.assertEqual(series[idx].cover_letter, patch)
>> >  
>> >              start_idx = end_idx
>> >  
>> > @@ -517,7 +525,7 @@ class SeriesTotalTest(_BaseTestCase):
>> >          self.assertSerialized(patches, [1])
>> >          self.assertSerialized(covers, [1])
>> >  
>> > -        series = patches[0].series.first()
>> > +        series = patches[0].series
>> >          self.assertFalse(series.received_all)
>> >  
>> >      def test_complete(self):
>> > @@ -537,7 +545,7 @@ class SeriesTotalTest(_BaseTestCase):
>> >          self.assertSerialized(covers, [1])
>> >          self.assertSerialized(patches, [2])
>> >  
>> > -        series = patches[0].series.first()
>> > +        series = patches[0].series
>> >          self.assertTrue(series.received_all)
>> >  
>> >      def test_extra_patches(self):
>> > @@ -558,7 +566,7 @@ class SeriesTotalTest(_BaseTestCase):
>> >          self.assertSerialized(covers, [1])
>> >          self.assertSerialized(patches, [3])
>> >  
>> > -        series = patches[0].series.first()
>> > +        series = patches[0].series
>> >          self.assertTrue(series.received_all)
>> >  
>> >  
>> > @@ -639,13 +647,13 @@ class SeriesNameTestCase(TestCase):
>> >  
>> >          cover = self._parse_mail(mbox[0])
>> >          cover_name = 'A sample series'
>> > -        self.assertEqual(cover.series.first().name, cover_name)
>> > +        self.assertEqual(cover.series.name, cover_name)
>> >  
>> >          self._parse_mail(mbox[1])
>> > -        self.assertEqual(cover.series.first().name, cover_name)
>> > +        self.assertEqual(cover.series.name, cover_name)
>> >  
>> >          self._parse_mail(mbox[2])
>> > -        self.assertEqual(cover.series.first().name, cover_name)
>> > +        self.assertEqual(cover.series.name, cover_name)
>> >  
>> >          mbox.close()
>> >  
>> > @@ -663,7 +671,7 @@ class SeriesNameTestCase(TestCase):
>> >          mbox = self._get_mbox('base-no-cover-letter.mbox')
>> >  
>> >          patch = self._parse_mail(mbox[0])
>> > -        series = patch.series.first()
>> > +        series = patch.series
>> >          self.assertEqual(series.name, patch.name)
>> >  
>> >          self._parse_mail(mbox[1])
>> > @@ -687,13 +695,13 @@ class SeriesNameTestCase(TestCase):
>> >          mbox = self._get_mbox('base-out-of-order.mbox')
>> >  
>> >          patch = self._parse_mail(mbox[0])
>> > -        self.assertIsNone(patch.series.first().name)
>> > +        self.assertIsNone(patch.series.name)
>> >  
>> >          patch = self._parse_mail(mbox[1])
>> > -        self.assertEqual(patch.series.first().name, patch.name)
>> > +        self.assertEqual(patch.series.name, patch.name)
>> >  
>> >          cover = self._parse_mail(mbox[2])
>> > -        self.assertEqual(cover.series.first().name, 'A sample series')
>> > +        self.assertEqual(cover.series.name, 'A sample series')
>> >  
>> >          mbox.close()
>> >  
>> > @@ -712,7 +720,7 @@ class SeriesNameTestCase(TestCase):
>> >          """
>> >          mbox = self._get_mbox('base-out-of-order.mbox')
>> >  
>> > -        series = self._parse_mail(mbox[0]).series.first()
>> > +        series = self._parse_mail(mbox[0]).series
>> >          self.assertIsNone(series.name)
>> >  
>> >          series_name = 'My custom series name'
>> > diff --git a/patchwork/tests/utils.py b/patchwork/tests/utils.py
>> > index d280fd69..0c3bbff2 100644
>> > --- a/patchwork/tests/utils.py
>> > +++ b/patchwork/tests/utils.py
>> > @@ -19,7 +19,6 @@ from patchwork.models import Patch
>> >  from patchwork.models import Person
>> >  from patchwork.models import Project
>> >  from patchwork.models import Series
>> > -from patchwork.models import SeriesPatch
>> >  from patchwork.models import SeriesReference
>> >  from patchwork.models import State
>> >  from patchwork.tests import TEST_PATCH_DIR
>> > @@ -228,17 +227,24 @@ def create_series(**kwargs):
>> >  
>> >  
>> >  def create_series_patch(**kwargs):
>> > -    """Create 'SeriesPatch' object."""
>> > +    """Create 'Patch' object and associate with a series."""
>> > +    # TODO(stephenfin): Remove this and all callers
>> >      num = 1 if 'series' not in kwargs else kwargs['series'].patches.count() + 1
>> > +    if 'number' in kwargs:
>> > +        num = kwargs['number']
>> >  
>> > -    values = {
>> > -        'series': create_series() if 'series' not in kwargs else None,
>> > -        'number': num,
>> > -        'patch': create_patch() if 'patch' not in kwargs else None,
>> > -    }
>> > -    values.update(**kwargs)
>> > +    series = create_series() if 'series' not in kwargs else kwargs['series']
>> > +    patch = create_patch() if 'patch' not in kwargs else kwargs['patch']
>> > +
>> > +    series.add_patch(patch, num)
>> > +
>> > +    class SeriesPatch(object):
>> > +        """Simple wrapper to avoid needing to update all tests at once."""
>> > +        def __init__(self, series, patch):
>> > +            self.series = series
>> > +            self.patch = patch
>> >  
>> > -    return SeriesPatch.objects.create(**values)
>> > +    return SeriesPatch(series=series, patch=patch)
>> >  
>> >  
>> >  def create_series_reference(**kwargs):
>> > diff --git a/patchwork/views/cover.py b/patchwork/views/cover.py
>> > index cb8fd3ca..08b633a1 100644
>> > --- a/patchwork/views/cover.py
>> > +++ b/patchwork/views/cover.py
>> > @@ -36,7 +36,6 @@ def cover_detail(request, cover_id):
>> >      comments = comments.only('submitter', 'date', 'id', 'content',
>> >                               'submission')
>> >      context['comments'] = comments
>> > -    context['all_series'] = cover.series.all().order_by('-date')
>> >  
>> >      return render(request, 'patchwork/submission.html', context)
>> >  
>> > diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py
>> > index 862dc83d..277b2816 100644
>> > --- a/patchwork/views/patch.py
>> > +++ b/patchwork/views/patch.py
>> > @@ -105,7 +105,6 @@ def patch_detail(request, patch_id):
>> >                               'submission')
>> >  
>> >      context['comments'] = comments
>> > -    context['all_series'] = patch.series.all().order_by('-date')
>> >      context['checks'] = patch.check_set.all().select_related('user')
>> >      context['submission'] = patch
>> >      context['patchform'] = form
>> > @@ -132,7 +131,7 @@ def patch_mbox(request, patch_id):
>> >  
>> >      response = HttpResponse(content_type='text/plain')
>> >      if series_id:
>> > -        if not patch.series.count():
>> > +        if not patch.series:
>> >              raise Http404('Patch does not have an associated series. This is '
>> >                            'because the patch was processed with an older '
>> >                            'version of Patchwork. It is not possible to '
>> > diff --git a/patchwork/views/utils.py b/patchwork/views/utils.py
>> > index a2be2c88..3c5d2982 100644
>> > --- a/patchwork/views/utils.py
>> > +++ b/patchwork/views/utils.py
>> > @@ -18,7 +18,6 @@ from django.utils import six
>> >  
>> >  from patchwork.models import Comment
>> >  from patchwork.models import Patch
>> > -from patchwork.models import Series
>> >  
>> >  if settings.ENABLE_REST_API:
>> >      from rest_framework.authtoken.models import Token
>> > @@ -128,26 +127,22 @@ def series_patch_to_mbox(patch, series_id):
>> >      Returns:
>> >          A string for the mbox file.
>> >      """
>> > -    if series_id == '*':
>> > -        series = patch.series.order_by('-date').first()
>> > -    else:
>> > +    if series_id != '*':
>> >          try:
>> >              series_id = int(series_id)
>> >          except ValueError:
>> >              raise Http404('Expected integer series value or *. Received: %r' %
>> >                            series_id)
>> >  
>> > -        try:
>> > -            series = patch.series.get(id=series_id)
>> > -        except Series.DoesNotExist:
>> > +        if patch.series.id != series_id:
>> >              raise Http404('Patch does not belong to series %d' % series_id)
>> >  
>> >      mbox = []
>> >  
>> >      # get the series-ified patch
>> > -    number = series.seriespatch_set.get(patch=patch).number
>> > -    for dep in series.seriespatch_set.filter(number__lt=number):
>> > -        mbox.append(patch_to_mbox(dep.patch))
>> > +    for dep in patch.series.patches.filter(
>> > +            number__lt=patch.number).order_by('number'):
>> > +        mbox.append(patch_to_mbox(dep))
>> >  
>> >      mbox.append(patch_to_mbox(patch))
>> >  
>> > @@ -165,7 +160,7 @@ def series_to_mbox(series):
>> >      """
>> >      mbox = []
>> >  
>> > -    for dep in series.seriespatch_set.all():
>> > +    for dep in series.patches.all().order_by('number'):
>> >          mbox.append(patch_to_mbox(dep.patch))
>> >  
>> >      return '\n'.join(mbox)
>> > -- 
>> > 2.17.1


More information about the Patchwork mailing list