--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def copy_series_field(apps, schema_editor):
+ """Populate the project field from child cover letter/patches."""
+ # TODO(stephenfin): Perhaps we'd like to include an SQL variant of the
+ # below though I'd imagine it would be rather tricky
+ Series = apps.get_model('patchwork', 'Series')
+
+ for series in Series.objects.all():
+ if series.cover_letter:
+ series.project = series.cover_letter.project
+ series.save()
+ elif series.patches:
+ series.project = series.patches.first().project
+ series.save()
+ else:
+ # a series without patches or cover letters should not exist.
+ # Delete it.
+ series.delete()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('patchwork', '0015_add_series_models'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='series',
+ name='project',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='series', to='patchwork.Project'),
+ ),
+ migrations.RunPython(copy_series_field, migrations.RunPython.noop),
+ migrations.AlterField(
+ model_name='seriesreference',
+ name='msgid',
+ field=models.CharField(max_length=255),
+ ),
+ migrations.AlterUniqueTogether(
+ name='seriesreference',
+ unique_together=set([('series', 'msgid')]),
+ ),
+ ]
class Series(models.Model):
"""An collection of patches."""
+ # parent
+ project = models.ForeignKey(Project, related_name='series')
+
# content
cover_letter = models.ForeignKey(CoverLetter,
related_name='series',
"""
series = models.ForeignKey(Series, related_name='references',
related_query_name='reference')
- msgid = models.CharField(max_length=255, unique=True)
+ msgid = models.CharField(max_length=255)
def __str__(self):
return self.msgid
+ class Meta:
+ unique_together = [('series', 'msgid')]
+
class Bundle(models.Model):
owner = models.ForeignKey(User)
return project
-def find_series(mail):
+def find_series(project, mail):
"""Find a patch's series.
Traverse RFC822 headers, starting with most recent first, to find
recent (the patch's closest ancestor) to least recent
Args:
+ project (patchwork.Project): The project that the series
+ belongs to
mail (email.message.Message): The mail to extract series from
Returns:
The matching ``Series`` instance, if any
"""
for ref in [mail.get('Message-Id')] + find_references(mail):
- # try parsing by RFC5322 fields first
try:
- return SeriesReference.objects.get(msgid=ref).series
+ return SeriesReference.objects.get(
+ msgid=ref, series__project=project).series
except SeriesReference.DoesNotExist:
pass
filenames = find_filenames(diff)
delegate = find_delegate_by_filename(project, filenames)
- series = find_series(mail)
+ series = find_series(project, mail)
# We will create a new series if:
# - we have a patch number (x of n), and
# - either:
(series.version != version) or
(SeriesPatch.objects.filter(series=series, number=x).count()
)):
- series = Series(date=date,
+ series = Series(project=project,
+ date=date,
submitter=author,
version=version,
total=n)
# series.) That should not create a series ref
# for this series, so check for the msg-id only,
# not the msg-id/series pair.
- SeriesReference.objects.get(msgid=ref)
+ SeriesReference.objects.get(msgid=ref,
+ series__project=project)
except SeriesReference.DoesNotExist:
SeriesReference.objects.create(series=series, msgid=ref)
# could only point to a different series or unrelated
# message
try:
- series = SeriesReference.objects.get(msgid=msgid).series
+ series = SeriesReference.objects.get(
+ msgid=msgid, series__project=project).series
except SeriesReference.DoesNotExist:
series = None
if not series:
- series = Series(date=date,
+ series = Series(project=project,
+ date=date,
submitter=author,
version=version,
total=n)
from patchwork.parser import subject_check
from patchwork.tests import TEST_MAIL_DIR
from patchwork.tests.utils import create_project
+from patchwork.tests.utils import create_series
from patchwork.tests.utils import create_series_reference
from patchwork.tests.utils import create_state
from patchwork.tests.utils import create_user
def test_new_series(self):
msgid = make_msgid()
email = self._create_email(msgid)
+ project = create_project()
- self.assertIsNone(find_series(email))
+ self.assertIsNone(find_series(project, email))
def test_first_reply(self):
msgid_a = make_msgid()
# assume msgid_a was already handled
ref = create_series_reference(msgid=msgid_a)
- series = find_series(email)
+ series = find_series(ref.series.project, email)
self.assertEqual(series, ref.series)
def test_nested_series(self):
"""Handle a series sent in-reply-to an existing series."""
# create an old series with a "cover letter"
msgids = [make_msgid()]
- ref_v1 = create_series_reference(msgid=msgids[0])
+ project = create_project()
+ series_v1 = create_series(project=project)
+ create_series_reference(msgid=msgids[0], series=series_v1)
# ...and three patches
for i in range(3):
msgids.append(make_msgid())
- create_series_reference(msgid=msgids[-1],
- series=ref_v1.series)
+ create_series_reference(msgid=msgids[-1], series=series_v1)
# now create a new series with "cover letter"
msgids.append(make_msgid())
- ref_v2 = create_series_reference(msgid=msgids[-1])
+ series_v2 = create_series(project=project)
+ ref_v2 = create_series_reference(msgid=msgids[-1], series=series_v2)
# ...and the "first patch" of this new series
msgid = make_msgid()
email = self._create_email(msgid, msgids)
- series = find_series(email)
+ series = find_series(project, email)
# this should link to the second series - not the first
self.assertEqual(len(msgids), 4 + 1) # old series + new cover
self.assertEqual(status.HTTP_200_OK, resp.status_code)
self.assertSerialized(series, resp.data)
- patch = create_patch()
+ patch = create_patch(project=series.project)
series.add_patch(patch, 1)
resp = self.client.get(self.api_url(series.id))
self.assertSerialized(series, resp.data)
- cover_letter = create_cover()
+ cover_letter = create_cover(project=series.project)
series.add_cover_letter(cover_letter)
resp = self.client.get(self.api_url(series.id))
self.assertSerialized(series, resp.data)
class _BaseTestCase(TestCase):
def setUp(self):
- self.project = utils.create_project()
utils.create_state()
- def _parse_mbox(self, name, counts):
+ def _parse_mbox(self, name, counts, project=None):
"""Parse an mbox file and return the results.
:param name: Name of mbox file
letters, patches and replies parsed
"""
results = [[], [], []]
+ project = project or utils.create_project()
mbox = mailbox.mbox(os.path.join(TEST_SERIES_DIR, name))
for msg in mbox:
- obj = parser.parse_mail(msg, self.project.listid)
+ obj = parser.parse_mail(msg, project.listid)
if type(obj) == models.CoverLetter:
results[0].append(obj)
elif type(obj) == models.Patch:
self.assertSerialized(patches, [2])
self.assertSerialized(covers, [1])
+ def test_duplicated(self):
+ """Series received on multiple mailing lists.
+
+ Parse a series with a two patches sent to two mailing lists
+ at the same time.
+
+ Input:
+
+ - [PATCH 1/2] test: Add some lorem ipsum
+ - [PATCH 2/2] test: Convert to Markdown
+ - [PATCH 1/2] test: Add some lorem ipsum
+ - [PATCH 2/2] test: Convert to Markdown
+ """
+ project_a = utils.create_project()
+ project_b = utils.create_project()
+
+ _, patches_a, _ = self._parse_mbox(
+ 'base-no-cover-letter.mbox', [0, 2, 0], project=project_a)
+ _, patches_b, _ = self._parse_mbox(
+ 'base-no-cover-letter.mbox', [0, 2, 0], project=project_b)
+
+ self.assertSerialized(patches_a + patches_b, [2, 2])
+
class RevisedSeriesTest(_BaseTestCase):
"""Tests for a series plus a single revision.
def create_series(**kwargs):
"""Create 'Series' object."""
values = {
+ 'project': create_project() if 'project' not in kwargs else None,
'date': dt.now(),
'submitter': create_person() if 'submitter' not in kwargs else None,
'total': 1,