Skip to content

Commit 76505e9

Browse files
committed
models: Convert Series-Patch relationship to 1:N
Late in the development of the series feature, it was decided that there were advantages to allowing an N:M relationship between series and patches. This would allow us to do things like create complete series where a sole vN patch was sent to a list rather than the full series. After some time using series in the wild, it's apparent that such features are very difficult to implement correctly and will likely never be implemented. As such, it's time to start cleaning up the mess, paving the way for things like an improved tagging feature. There are some significant changes to the model required: - models.py, migrations/0027, migrations/0028, migrations/0029 The migrations make the following changes: 1. - Add 'Patch.series_alt' and 'Patch.number' fields. 2. - Populate the 'Patch.series_alt' and 'Patch.number' fields from their 'SeriesPatch' equivalents. 3. - Remove the 'SeriesPatch' model. - Rename 'Patch.series_alt' to 'Patch.series'. - Change 'Series.cover_letter' to a 'OneToOneField' since a cover letter can no longer be assigned to multiple series. Note that the migrations have to be split into multiple parts as the combined migration raises an OperationalError as below. (1072, "Key column 'series_alt_id' doesn't exist in table") This is due to Django's penchant for creating indexes for newly created fields, as noted here: https://stackoverflow.com/q/35158530/ Aside from the model changes, there are numerous other changes required: - admin.py Reflect model changes for the 'PatchInline' inline used by 'SeriesAdmin' - api/cover.py, api/patch.py Update the 'series' field for the cover letter and patch resources to reflect the model changes. A 'to_representation' function is added in both cases to post-process this field and make it look like a list again. This is necessary to avoid breaking clients. - parser.py Update to reflect the replacement of 'SeriesPatch' with 'Patch'. - signals.py Update to filter on changes to 'Patch' instead of 'SeriesPatch'. This requires some reworking due to how we set these fields now, as we can no longer receive on 'post_save' signals for 'SeriesPatch' and must instead watch for 'pre_save' on 'Patch', which is what we do for delegate and state changes on same. - templates/patchwork/*.html Remove logic that handled multiple series in favour of the (simpler) single series logic. - tests/* Modify the 'create_series_patch' helper to reflect the removal of the 'SeriesPatch' model. This entire helper will be removed in a future change. Improve some tests to cover edge cases that were highlighted during development Unfortunately, all of the above changes must go in at the same time, otherwise we end up with either (a) broken views, API etc. or (b) split brain because we need to keep the new single-series fields alongside the older multi-series fields and models while we rework the views. It's unfortunate but there's not much to be done here. Signed-off-by: Stephen Finucane <stephen@that.guru> Tested-by: Daniel Axtens <dja@axtens.net>
1 parent 8883380 commit 76505e9

21 files changed

+263
-174
lines changed

lib/sql/grant-all.mysql.sql

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_patchtag TO 'www-data'@localho
2727
GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_person TO 'www-data'@localhost;
2828
GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_project TO 'www-data'@localhost;
2929
GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_series TO 'www-data'@localhost;
30-
GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_seriespatch TO 'www-data'@localhost;
3130
GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_seriesreference TO 'www-data'@localhost;
3231
GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_state TO 'www-data'@localhost;
3332
GRANT SELECT, UPDATE, INSERT, DELETE ON patchwork_submission TO 'www-data'@localhost;
@@ -42,7 +41,6 @@ GRANT INSERT, SELECT ON patchwork_coverletter TO 'nobody'@localhost;
4241
GRANT INSERT, SELECT ON patchwork_patch TO 'nobody'@localhost;
4342
GRANT INSERT, SELECT ON patchwork_person TO 'nobody'@localhost;
4443
GRANT INSERT, SELECT ON patchwork_series TO 'nobody'@localhost;
45-
GRANT INSERT, SELECT ON patchwork_seriespatch TO 'nobody'@localhost;
4644
GRANT INSERT, SELECT ON patchwork_seriesreference TO 'nobody'@localhost;
4745
GRANT INSERT, SELECT ON patchwork_submission TO 'nobody'@localhost;
4846
GRANT INSERT, SELECT, UPDATE, DELETE ON patchwork_patchtag TO 'nobody'@localhost;

lib/sql/grant-all.postgres.sql

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ GRANT SELECT, UPDATE, INSERT, DELETE ON
2727
patchwork_person,
2828
patchwork_project,
2929
patchwork_series,
30-
patchwork_seriespatch,
3130
patchwork_seriesreference,
3231
patchwork_state,
3332
patchwork_submission,
@@ -56,7 +55,6 @@ GRANT SELECT, UPDATE ON
5655
patchwork_person_id_seq,
5756
patchwork_project_id_seq,
5857
patchwork_series_id_seq,
59-
patchwork_seriespatch_id_seq,
6058
patchwork_seriesreference_id_seq,
6159
patchwork_state_id_seq,
6260
patchwork_tag_id_seq,
@@ -70,7 +68,6 @@ GRANT INSERT, SELECT ON
7068
patchwork_comment,
7169
patchwork_coverletter,
7270
patchwork_event
73-
patchwork_seriespatch,
7471
patchwork_seriesreference,
7572
patchwork_submission,
7673
TO "nobody";
@@ -93,7 +90,6 @@ GRANT UPDATE, SELECT ON
9390
patchwork_patchtag_id_seq,
9491
patchwork_person_id_seq,
9592
patchwork_series_id_seq,
96-
patchwork_seriespatch_id_seq,
9793
patchwork_seriesreference_id_seq,
9894
TO "nobody";
9995

patchwork/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class CommentAdmin(admin.ModelAdmin):
114114

115115

116116
class PatchInline(admin.StackedInline):
117-
model = Series.patches.through
117+
model = Patch
118118
extra = 0
119119

120120

patchwork/api/cover.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CoverLetterListSerializer(BaseHyperlinkedModelSerializer):
2424
project = ProjectSerializer(read_only=True)
2525
submitter = PersonSerializer(read_only=True)
2626
mbox = SerializerMethodField()
27-
series = SeriesSerializer(many=True, read_only=True)
27+
series = SeriesSerializer(read_only=True)
2828
comments = SerializerMethodField()
2929

3030
def get_web_url(self, instance):
@@ -39,6 +39,15 @@ def get_comments(self, cover):
3939
return self.context.get('request').build_absolute_uri(
4040
reverse('api-cover-comment-list', kwargs={'pk': cover.id}))
4141

42+
def to_representation(self, instance):
43+
# NOTE(stephenfin): This is here to ensure our API looks the same even
44+
# after we changed the series-patch relationship from M:N to 1:N. It
45+
# will be removed in API v2
46+
data = super(CoverLetterListSerializer, self).to_representation(
47+
instance)
48+
data['series'] = [data['series']]
49+
return data
50+
4251
class Meta:
4352
model = CoverLetter
4453
fields = ('id', 'url', 'web_url', 'project', 'msgid', 'date', 'name',
@@ -89,8 +98,8 @@ class CoverLetterList(ListAPIView):
8998
ordering = 'id'
9099

91100
def get_queryset(self):
92-
return CoverLetter.objects.all().prefetch_related('series')\
93-
.select_related('project', 'submitter')\
101+
return CoverLetter.objects.all()\
102+
.select_related('project', 'submitter', 'series')\
94103
.defer('content', 'headers')
95104

96105

@@ -100,5 +109,5 @@ class CoverLetterDetail(RetrieveAPIView):
100109
serializer_class = CoverLetterDetailSerializer
101110

102111
def get_queryset(self):
103-
return CoverLetter.objects.all().prefetch_related('series')\
104-
.select_related('project', 'submitter')
112+
return CoverLetter.objects.all()\
113+
.select_related('project', 'submitter', 'series')

patchwork/api/patch.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class PatchListSerializer(BaseHyperlinkedModelSerializer):
7070
submitter = PersonSerializer(read_only=True)
7171
delegate = UserSerializer(allow_null=True)
7272
mbox = SerializerMethodField()
73-
series = SeriesSerializer(many=True, read_only=True)
73+
series = SeriesSerializer(read_only=True)
7474
comments = SerializerMethodField()
7575
check = SerializerMethodField()
7676
checks = SerializerMethodField()
@@ -111,6 +111,14 @@ def validate_delegate(self, value):
111111
"'%s'" % (value, self.instance.project))
112112
return value
113113

114+
def to_representation(self, instance):
115+
# NOTE(stephenfin): This is here to ensure our API looks the same even
116+
# after we changed the series-patch relationship from M:N to 1:N. It
117+
# will be removed in API v2
118+
data = super(PatchListSerializer, self).to_representation(instance)
119+
data['series'] = [data['series']]
120+
return data
121+
114122
class Meta:
115123
model = Patch
116124
fields = ('id', 'url', 'web_url', 'project', 'msgid', 'date', 'name',
@@ -173,8 +181,9 @@ class PatchList(ListAPIView):
173181

174182
def get_queryset(self):
175183
return Patch.objects.all()\
176-
.prefetch_related('series', 'check_set')\
177-
.select_related('project', 'state', 'submitter', 'delegate')\
184+
.prefetch_related('check_set')\
185+
.select_related('project', 'state', 'submitter', 'delegate',
186+
'series')\
178187
.defer('content', 'diff', 'headers')
179188

180189

@@ -186,5 +195,6 @@ class PatchDetail(RetrieveUpdateAPIView):
186195

187196
def get_queryset(self):
188197
return Patch.objects.all()\
189-
.prefetch_related('series', 'check_set')\
190-
.select_related('project', 'state', 'submitter', 'delegate')
198+
.prefetch_related('check_set')\
199+
.select_related('project', 'state', 'submitter', 'delegate',
200+
'series')
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
from django.db.models import Count
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('patchwork', '0030_add_submission_covering_index'),
13+
]
14+
15+
operations = [
16+
# Add Patch.series_alt, Patch.number fields. This will store the fields
17+
# currently stored in SeriesPatch
18+
migrations.AddField(
19+
model_name='patch',
20+
name='number',
21+
field=models.PositiveSmallIntegerField(default=None, help_text=b'The number assigned to this patch in the series', null=True),
22+
),
23+
migrations.AddField(
24+
model_name='patch',
25+
name='series_alt',
26+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='patchwork.Series'),
27+
),
28+
migrations.AlterUniqueTogether(
29+
name='patch',
30+
unique_together=set([('series_alt', 'number')]),
31+
),
32+
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
from django.db.models import Count
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('patchwork', '0031_add_patch_series_fields'),
13+
]
14+
15+
operations = [
16+
# Copy SeriesPatch.series, SeriesPatch.number to Patch.series_alt,
17+
# Patch.number. Note that there is no uniqueness check here because no
18+
# code actually allowed us to save multiple series
19+
migrations.RunSQL(
20+
"""UPDATE patchwork_patch SET series_alt_id =
21+
(SELECT series_id from patchwork_seriespatch
22+
WHERE patchwork_seriespatch.patch_id =
23+
patchwork_patch.submission_ptr_id);
24+
UPDATE patchwork_patch SET number =
25+
(SELECT number from patchwork_seriespatch
26+
WHERE patchwork_seriespatch.patch_id =
27+
patchwork_patch.submission_ptr_id);
28+
""",
29+
"""INSERT INTO patchwork_seriespatch
30+
(patch_id, series_id, number)
31+
SELECT submission_ptr_id, series_alt_id, number
32+
FROM patchwork_patch;
33+
""",
34+
),
35+
]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('patchwork', '0032_migrate_data_from_series_patch_to_patch'),
12+
]
13+
14+
operations = [
15+
# Remove SeriesPatch
16+
migrations.AlterUniqueTogether(
17+
name='seriespatch',
18+
unique_together=set([]),
19+
),
20+
migrations.RemoveField(
21+
model_name='seriespatch',
22+
name='patch',
23+
),
24+
migrations.RemoveField(
25+
model_name='seriespatch',
26+
name='series',
27+
),
28+
migrations.RemoveField(
29+
model_name='series',
30+
name='patches',
31+
),
32+
migrations.DeleteModel(
33+
name='SeriesPatch',
34+
),
35+
# Now that SeriesPatch has been removed, we can use the now-unused
36+
# Patch.series field and add a backreference
37+
migrations.RenameField(
38+
model_name='patch',
39+
old_name='series_alt',
40+
new_name='series',
41+
),
42+
migrations.AlterField(
43+
model_name='patch',
44+
name='series',
45+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='patches', related_query_name='patch', to='patchwork.Series'),
46+
),
47+
migrations.AlterUniqueTogether(
48+
name='patch',
49+
unique_together=set([('series', 'number')]),
50+
),
51+
# Migrate CoverLetter to OneToOneField as a cover letter can no longer
52+
# be assigned to multiple series
53+
migrations.AlterField(
54+
model_name='series',
55+
name='cover_letter',
56+
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='series', to='patchwork.CoverLetter'),
57+
),
58+
]

patchwork/models.py

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,15 @@ class Patch(Submission):
407407
# patches in a project without needing to do a JOIN.
408408
patch_project = models.ForeignKey(Project, on_delete=models.CASCADE)
409409

410+
# series metadata
411+
412+
series = models.ForeignKey(
413+
'Series', null=True, blank=True, on_delete=models.CASCADE,
414+
related_name='patches', related_query_name='patch')
415+
number = models.PositiveSmallIntegerField(
416+
default=None, null=True,
417+
help_text='The number assigned to this patch in the series')
418+
410419
objects = PatchManager()
411420

412421
@staticmethod
@@ -563,6 +572,7 @@ def __str__(self):
563572
class Meta:
564573
verbose_name_plural = 'Patches'
565574
base_manager_name = 'objects'
575+
unique_together = [('series', 'number')]
566576

567577
indexes = [
568578
# This is a covering index for the /list/ query
@@ -606,19 +616,16 @@ class Meta:
606616

607617
@python_2_unicode_compatible
608618
class Series(FilenameMixin, models.Model):
609-
"""An collection of patches."""
619+
"""A collection of patches."""
610620

611621
# parent
612622
project = models.ForeignKey(Project, related_name='series', null=True,
613623
blank=True, on_delete=models.CASCADE)
614624

615625
# content
616-
cover_letter = models.ForeignKey(CoverLetter,
617-
related_name='series',
618-
null=True, blank=True,
619-
on_delete=models.CASCADE)
620-
patches = models.ManyToManyField(Patch, through='SeriesPatch',
621-
related_name='series')
626+
cover_letter = models.OneToOneField(CoverLetter, related_name='series',
627+
null=True,
628+
on_delete=models.CASCADE)
622629

623630
# metadata
624631
name = models.CharField(max_length=255, blank=True, null=True,
@@ -684,9 +691,8 @@ def add_cover_letter(self, cover):
684691
self.name = self._format_name(cover)
685692
else:
686693
try:
687-
name = SeriesPatch.objects.get(series=self,
688-
number=1).patch.name
689-
except SeriesPatch.DoesNotExist:
694+
name = Patch.objects.get(series=self, number=1).name
695+
except Patch.DoesNotExist:
690696
name = None
691697

692698
if self.name == name:
@@ -696,20 +702,16 @@ def add_cover_letter(self, cover):
696702

697703
def add_patch(self, patch, number):
698704
"""Add a patch to the series."""
699-
# see if the patch is already in this series
700-
if SeriesPatch.objects.filter(series=self, patch=patch).count():
701-
# TODO(stephenfin): We may wish to raise an exception here in the
702-
# future
703-
return
704-
705705
# both user defined names and cover letter-based names take precedence
706706
if not self.name and number == 1:
707707
self.name = patch.name # keep the prefixes for patch-based names
708708
self.save()
709709

710-
return SeriesPatch.objects.create(series=self,
711-
patch=patch,
712-
number=number)
710+
patch.series = self
711+
patch.number = number
712+
patch.save()
713+
714+
return patch
713715

714716
def get_absolute_url(self):
715717
# TODO(stephenfin): We really need a proper series view
@@ -727,26 +729,6 @@ class Meta:
727729
verbose_name_plural = 'Series'
728730

729731

730-
@python_2_unicode_compatible
731-
class SeriesPatch(models.Model):
732-
"""A patch in a series.
733-
734-
Patches can belong to many series. This allows for things like
735-
auto-completion of partial series.
736-
"""
737-
patch = models.ForeignKey(Patch, on_delete=models.CASCADE)
738-
series = models.ForeignKey(Series, on_delete=models.CASCADE)
739-
number = models.PositiveSmallIntegerField(
740-
help_text='The number assigned to this patch in the series')
741-
742-
def __str__(self):
743-
return self.patch.name
744-
745-
class Meta:
746-
unique_together = [('series', 'patch'), ('series', 'number')]
747-
ordering = ['number']
748-
749-
750732
@python_2_unicode_compatible
751733
class SeriesReference(models.Model):
752734
"""A reference found in a series.

0 commit comments

Comments
 (0)