From patchwork Tue Mar 10 15:35:18 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stephen Finucane X-Patchwork-Id: 1252261 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [203.11.71.2]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 48cK1p6l6Pz9s3x for ; Wed, 11 Mar 2020 02:36:14 +1100 (AEDT) Authentication-Results: ozlabs.org; dmarc=none (p=none dis=none) header.from=that.guru Authentication-Results: ozlabs.org; dkim=fail reason="key not found in DNS" header.d=that.guru header.i=@that.guru header.a=rsa-sha256 header.s=default header.b=CWc1tYZi; dkim-atps=neutral Received: from lists.ozlabs.org (unknown [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 48cK1m6F6RzDqQg for ; Wed, 11 Mar 2020 02:36:12 +1100 (AEDT) X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=none (no SPF record) smtp.mailfrom=that.guru (client-ip=172.82.139.126; helo=qrelay126.mxroute.com; envelope-from=stephen@that.guru; receiver=) Authentication-Results: lists.ozlabs.org; dmarc=none (p=none dis=none) header.from=that.guru Received: from qrelay126.mxroute.com (qrelay126.mxroute.com [172.82.139.126]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 48cK1B5xZKzDqPb for ; Wed, 11 Mar 2020 02:35:38 +1100 (AEDT) Received: from filter004.mxroute.com ([149.28.56.236] 149.28.56.236.vultr.com) (Authenticated sender: mN4UYu2MZsgR) by qrelay126.mxroute.com (ZoneMTA) with ESMTPA id 170c516215b000f8af.001 for ; Tue, 10 Mar 2020 15:35:26 +0000 X-Zone-Loop: 92337956adf9eaf12daafe108800356caac0910cf575 X-Originating-IP: [149.28.56.236] Received: from one.mxroute.com (one.mxroute.com [195.201.59.211]) by filter004.mxroute.com (Postfix) with ESMTPS id C99233EA19; Tue, 10 Mar 2020 15:35:25 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=that.guru; s=default; h=Content-Transfer-Encoding:MIME-Version:Message-Id:Date:Subject: Cc:To:From:Sender:Reply-To:Content-Type:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=wcVzbaXncm+NkcfPYQKmQ1x0ShY2h4lbPi4P0M2qmHg=; b=CWc1tYZitgi+3Rx3YjJ+NTUxU1 w9X19+p67wgCu3sCPXEYoK4ZUzPTmECjWZD9dhsSlVmIgeJyT+B+lm1d+ba1lQi7k617FlIDoKuCH r20GwZY6zoZnK18/eeyoEJIDcwosVAfgLy0kEdBaNl6zjg5LPDzCJnVaxsqpSbEKo9DTFAkJnzGvb 7Y23+VcV0KMZ00LeKAJztSRerWmE966o7T2ClJlMwp4g8TaNL6isZ5UNcEjF+diJ2c80QHJnyiYro z8cnhcbjkBTcXp84zMcl/tqhYDzIdagDR11oolgGIgmFhIZ3sjBenswCihhMbliXOfiaUlnPLkat/ YFtXzpAQ==; From: Stephen Finucane To: patchwork@lists.ozlabs.org Subject: [PATCH v3 1/2] parser: Don't group patches with different versions in a series Date: Tue, 10 Mar 2020 15:35:18 +0000 Message-Id: <20200310153519.14380-1-stephen@that.guru> X-Mailer: git-send-email 2.24.1 MIME-Version: 1.0 X-AuthUser: stephen@that.guru X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" As noted in #340 [1], if a patch from a series is dropped or miscategorised, patches from a later revision of that series can end up included in the earlier series rather than in their own series. This was actually intentional as part of the fix for #105 [2]. However, completely ignoring this information can be problematic. Refine things by checking for versions and, if they don't match, using timeboxing to try guess if they should be kept together. This would resolve the issue seen in #340 while preventing a regression for #105. [1] https://github.com/getpatchwork/patchwork/issues/340 [1] https://github.com/getpatchwork/patchwork/issues/105 Signed-off-by: Stephen Finucane Related: #340 Related: #105 Tested-by: Ali Alnubani --- patchwork/parser.py | 18 +++ .../tests/series/bugs-mixed-versions.mbox | 110 ++++++++++++++++++ patchwork/tests/test_series.py | 17 +++ 3 files changed, 145 insertions(+) create mode 100644 patchwork/tests/series/bugs-mixed-versions.mbox diff --git a/patchwork/parser.py b/patchwork/parser.py index 0e88d934..45930b45 100644 --- a/patchwork/parser.py +++ b/patchwork/parser.py @@ -250,14 +250,32 @@ def _find_series_by_references(project, mail): Returns: The matching ``Series`` instance, if any """ + subject = mail.get('Subject') + name, prefixes = clean_subject(subject, [project.linkname]) + version = parse_version(name, prefixes) + refs = find_references(mail) h = clean_header(mail.get('Message-Id')) if h: refs = [h] + refs + for ref in refs: try: series = SeriesReference.objects.get( msgid=ref[:255], project=project).series + + if series.version != version: + # if the versions don't match, at least make sure these were + # sent around the same time + date = find_date(mail) + delta = datetime.timedelta(minutes=SERIES_DELAY_INTERVAL) + start_date = date - delta + end_date = date + delta + + # ...and if they don't, this probably isn't our series + if start_date > series.date > end_date: + continue + # we want to return a queryset like '_find_series_by_markers' return Series.objects.filter(id=series.id) except SeriesReference.DoesNotExist: diff --git a/patchwork/tests/series/bugs-mixed-versions.mbox b/patchwork/tests/series/bugs-mixed-versions.mbox new file mode 100644 index 00000000..1aa639be --- /dev/null +++ b/patchwork/tests/series/bugs-mixed-versions.mbox @@ -0,0 +1,110 @@ +From stephenfinucane@gmail.com Sun Sep 11 23:22:13 2016 +Return-Path: +From: Stephen Finucane +To: stephenfinucane@hotmail.com +Subject: [PATCH 1/2] test: Add some lorem ipsum +Date: Sun, 11 Sep 2016 23:22:03 +0100 +Message-ID: <1473632524-8585-2-git-send-email-stephenfinucane@gmail.com> +X-Mailer: git-send-email 2.7.4 +MIME-Version: 1.0 +Content-Type: text/plain +Content-Length: 670 +Lines: 22 + +From: Stephen Finucane + +--- + test.txt | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/test.txt b/test.txt +index f75ba05..a6c61c0 100644 +--- a/test.txt ++++ b/test.txt +@@ -1 +1,8 @@ + Hello, world. ++ ++Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eget eleifend ++augue. Nullam at metus venenatis, laoreet neque nec, convallis mauris. ++Pellentesque aliquam at nisi et laoreet. Duis non nisl venenatis, rhoncus risus ++id, elementum felis. In hac habitasse platea dictumst. Nam sit amet maximus ++eros. Nam quis ligula ut tortor egestas bibendum. Nunc sed purus sit amet ++tellus commodo bibendum ut vel dolor. +-- +2.7.4 + + +From stephenfinucane@gmail.com Sun Sep 18 23:31:07 2016 +Return-Path: +From: Stephen Finucane +To: stephenfinucane@hotmail.com +Subject: [PATCH v2 2/2] test: Convert to Markdown +Date: Sun, 18 Sep 2016 23:30:55 +0100 +Message-ID: <1473633055-10316-3-git-send-email-stephenfinucane@gmail.com> +X-Mailer: git-send-email 2.7.4 +In-Reply-To: <1473633055-10316-2-git-send-email-stephenfinucane@gmail.com> +References: <1473633055-10316-2-git-send-email-stephenfinucane@gmail.com> +MIME-Version: 1.0 +Content-Type: text/plain +Content-Length: 2589 +Lines: 58 + +From: Stephen Finucane + +--- + test.md | 17 +++++++++++++++++ + test.txt | 17 ----------------- + 2 files changed, 17 insertions(+), 17 deletions(-) + create mode 100644 test.md + delete mode 100644 test.txt + +diff --git a/test.md b/test.md +new file mode 100644 +index 0000000..201bfe7 +--- /dev/null ++++ b/test.md +@@ -0,0 +1,17 @@ ++# Hello, world ++ ++Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eget eleifend ++augue. Nullam at metus venenatis, laoreet neque nec, convallis mauris. ++Pellentesque aliquam at nisi et laoreet. Duis non nisl venenatis, rhoncus risus ++id, elementum felis. In hac habitasse platea dictumst. Nam sit amet maximus ++eros. Nam quis ligula ut tortor egestas bibendum. Nunc sed purus sit amet ++tellus commodo bibendum ut vel dolor. ++ ++Curabitur scelerisque tempus efficitur. Maecenas aliquet elementum quam, ac ++pharetra nisi gravida a. Nam vitae malesuada leo. Nam accumsan facilisis metus ++sed vulputate. Ut libero odio, tempor ac posuere vel, malesuada quis ex. Donec ++interdum ipsum et nibh molestie auctor eget in neque. Sed nec hendrerit enim. ++Etiam scelerisque non magna eu molestie. Maecenas dui quam, tempor quis ipsum ++eu, dapibus congue dui. Integer tincidunt accumsan tortor sed maximus. Praesent ++eu magna accumsan, consequat turpis quis, fringilla ante. Vestibulum nec ++maximus nulla. Donec vehicula nisl at pharetra eleifend. +diff --git a/test.txt b/test.txt +deleted file mode 100644 +index 4478ac2..0000000 +--- a/test.txt ++++ /dev/null +@@ -1,17 +0,0 @@ +-Hello, world. +- +-Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eget eleifend +-augue. Nullam at metus venenatis, laoreet neque nec, convallis mauris. +-Pellentesque aliquam at nisi et laoreet. Duis non nisl venenatis, rhoncus risus +-id, elementum felis. In hac habitasse platea dictumst. Nam sit amet maximus +-eros. Nam quis ligula ut tortor egestas bibendum. Nunc sed purus sit amet +-tellus commodo bibendum ut vel dolor. +- +-Curabitur scelerisque tempus efficitur. Maecenas aliquet elementum quam, ac +-pharetra nisi gravida a. Nam vitae malesuada leo. Nam accumsan facilisis metus +-sed vulputate. Ut libero odio, tempor ac posuere vel, malesuada quis ex. Donec +-interdum ipsum et nibh molestie auctor eget in neque. Sed nec hendrerit enim. +-Etiam scelerisque non magna eu molestie. Maecenas dui quam, tempor quis ipsum +-eu, dapibus congue dui. Integer tincidunt accumsan tortor sed maximus. Praesent +-eu magna accumsan, consequat turpis quis, fringilla ante. Vestibulum nec +-maximus nulla. Donec vehicula nisl at pharetra eleifend. +-- +2.7.4 + + diff --git a/patchwork/tests/test_series.py b/patchwork/tests/test_series.py index 295a049d..2e86e47c 100644 --- a/patchwork/tests/test_series.py +++ b/patchwork/tests/test_series.py @@ -538,6 +538,23 @@ class RevisedSeriesTest(_BaseTestCase): self.assertSerialized(patches, [1, 1, 1]) + def test_mixed_versions(self): + """Series with a revision sent in reply to an incompleted series. + + Parse a series with two patches, one of which has been lost or + miscategorized, followed by a second revision of the missing patch. + None of the patches of the second revision should be included in the + first revision. + + - [PATCH 1/2] test: Add some lorem ipsum + - [PATCH v2 2/2] test: Convert to Markdown + """ + _, patches, _ = self._parse_mbox( + 'bugs-mixed-versions.mbox', [0, 2, 0], + ) + + self.assertSerialized(patches, [1, 1]) + class SeriesTotalTest(_BaseTestCase): From patchwork Tue Mar 10 15:35:19 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stephen Finucane X-Patchwork-Id: 1252271 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [203.11.71.2]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 48cKPG5KPjz9sNg for ; Wed, 11 Mar 2020 02:53:06 +1100 (AEDT) Authentication-Results: ozlabs.org; dmarc=none (p=none dis=none) header.from=that.guru Authentication-Results: ozlabs.org; dkim=fail reason="key not found in DNS" header.d=that.guru header.i=@that.guru header.a=rsa-sha256 header.s=default header.b=XbldU2qj; dkim-atps=neutral Received: from lists.ozlabs.org (unknown [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 48cKPG52VSzDqVW for ; Wed, 11 Mar 2020 02:53:06 +1100 (AEDT) X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=none (no SPF record) smtp.mailfrom=that.guru (client-ip=160.202.107.212; helo=q2relay212.mxroute.com; envelope-from=stephen@that.guru; receiver=) Authentication-Results: lists.ozlabs.org; dmarc=none (p=none dis=none) header.from=that.guru Authentication-Results: lists.ozlabs.org; dkim=fail reason="key not found in DNS" header.d=that.guru header.i=@that.guru header.a=rsa-sha256 header.s=default header.b=XbldU2qj; dkim-atps=neutral X-Greylist: delayed 306 seconds by postgrey-1.36 at bilbo; Wed, 11 Mar 2020 02:52:58 AEDT Received: from q2relay212.mxroute.com (q2relay212.mxroute.com [160.202.107.212]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 48cKP656YbzDqTh for ; Wed, 11 Mar 2020 02:52:58 +1100 (AEDT) Received: from chi-d045 ([191.96.97.158] chicago.mxroute.com) (Authenticated sender: mN4UYu2MZsgR) by q2relay212.mxroute.com (ZoneMTA) with ESMTPSA id 170c5216255000b5df.001 for (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256); Tue, 10 Mar 2020 15:47:44 +0000 X-Zone-Loop: b9575591c864ec49d2b9b9bbee847156da74c769eba1 Received: from filter004.mxroute.com ([149.28.56.236] 149.28.56.236.vultr.com) (Authenticated sender: mN4UYu2MZsgR) by chi-d045 (ZoneMTA) with ESMTPSA id 170c516236100072d4.001 for (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256); Tue, 10 Mar 2020 15:35:27 +0000 X-Zone-Loop: 46a023b519e111b668538fa7e01dcf74d7e92e6ffa0d X-Originating-IP: [191.96.97.158] Received: from one.mxroute.com (one.mxroute.com [195.201.59.211]) by filter004.mxroute.com (Postfix) with ESMTPS id EAA0B3ED98; Tue, 10 Mar 2020 15:35:25 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=that.guru; s=default; h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To: Message-Id:Date:Subject:Cc:To:From:Sender:Reply-To:Content-Type:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=zxJJWQxHSKnLeVHtkCVPiaK45VVEkV8JvC8mfkFPoPE=; b=XbldU2qjRR+4ZIxbR9l2+82U3x kp1gU5CZXC9Czb8rGT9v4LUz2vp2vZ5xKqSQEIucyOrIa0yPBsMX/Q7CfJlD4OXAPtIFJc3OkU/wX qnxr8mao3qpB75xQ1SQo3thHloy/lObOQuGWTiJ1q/AH9JXkabBa7UlD5snDfFl6UE72hSj4vaktg 3cxcHTjIPM17axigvWjJoz4ZBWEn/5EKJfiNSZQgRSiUSd7M/+2CPKqpCNiPVDA6XdT9lUYm0iLcf j3JVuS5p1Md0aRqhFKMTf51nmNjcebmh7hhbIPDhSAVJJnO0pMCCshAqDBRLVwOLgq4nnxO90s4tv eK8VAFsA==; From: Stephen Finucane To: patchwork@lists.ozlabs.org Subject: [PATCH v3 2/2] migrations: Don't attempt to rehome patches Date: Tue, 10 Mar 2020 15:35:19 +0000 Message-Id: <20200310153519.14380-2-stephen@that.guru> X-Mailer: git-send-email 2.24.1 In-Reply-To: <20200310153519.14380-1-stephen@that.guru> References: <20200310153519.14380-1-stephen@that.guru> MIME-Version: 1.0 X-AuthUser: stephen@that.guru X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" Migration 0039 attempts to move patches that have ended up in an arbitrary series due to race conditions into the correct series. However, there are a number of race conditions that can occur here that make this particularly tricky to do. Given that series are really just arbitary metadata, it's not really necessary to do this...so don't. Instead, just delete the series references that identical message IDs and below to the same project, allowing us to add the uniqueness constraint and prevent the issue bubbling up in the future. This means we're still left with orphaned series but these could be fixed manually, if necessary. Signed-off-by: Stephen Finucane Closes: #340 --- .../0039_unique_series_references.py | 110 ++++++++---------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/patchwork/migrations/0039_unique_series_references.py b/patchwork/migrations/0039_unique_series_references.py index 99b10fcc..3a101728 100644 --- a/patchwork/migrations/0039_unique_series_references.py +++ b/patchwork/migrations/0039_unique_series_references.py @@ -3,62 +3,6 @@ from django.db.models import Count import django.db.models.deletion -def merge_duplicate_series(apps, schema_editor): - SeriesReference = apps.get_model('patchwork', 'SeriesReference') - Patch = apps.get_model('patchwork', 'Patch') - - msgid_seriesrefs = {} - - # find all SeriesReference that share a msgid but point to different series - # and decide which of the series is going to be the authoritative one - msgid_counts = ( - SeriesReference.objects.values('msgid') - .annotate(count=Count('msgid')) - .filter(count__gt=1) - ) - for msgid_count in msgid_counts: - msgid = msgid_count['msgid'] - chosen_ref = None - for series_ref in SeriesReference.objects.filter(msgid=msgid): - if series_ref.series.cover_letter: - if chosen_ref: - # I don't think this can happen, but explode if it does - raise Exception( - "Looks like you've got two or more series that share " - "some patches but do not share a cover letter. Unable " - "to auto-resolve." - ) - - # if a series has a cover letter, that's the one we'll group - # everything under - chosen_ref = series_ref - - if not chosen_ref: - # if none of the series have cover letters, simply use the last - # one (hint: this relies on Python's weird scoping for for loops - # where 'series_ref' is still accessible outside the loop) - chosen_ref = series_ref - - msgid_seriesrefs[msgid] = chosen_ref - - # reassign any patches referring to non-authoritative series to point to - # the authoritative one, and delete the other series; we do this separately - # to allow us a chance to raise the exception above if necessary - for msgid, chosen_ref in msgid_seriesrefs.items(): - for series_ref in SeriesReference.objects.filter(msgid=msgid): - if series_ref == chosen_ref: - continue - - # update the patches to point to our chosen series instead, on the - # assumption that all other metadata is correct - for patch in Patch.objects.filter(series=series_ref.series): - patch.series = chosen_ref.series - patch.save() - - # delete the other series (which will delete the series ref) - series_ref.series.delete() - - def copy_project_field(apps, schema_editor): if connection.vendor == 'postgresql': schema_editor.execute( @@ -67,7 +11,7 @@ def copy_project_field(apps, schema_editor): SET project_id = patchwork_series.project_id FROM patchwork_series WHERE patchwork_seriesreference.series_id = patchwork_series.id - """ + """ ) elif connection.vendor == 'mysql': schema_editor.execute( @@ -75,7 +19,7 @@ def copy_project_field(apps, schema_editor): UPDATE patchwork_seriesreference, patchwork_series SET patchwork_seriesreference.project_id = patchwork_series.project_id WHERE patchwork_seriesreference.series_id = patchwork_series.id - """ # noqa + """ # noqa ) else: SeriesReference = apps.get_model('patchwork', 'SeriesReference') @@ -87,14 +31,49 @@ def copy_project_field(apps, schema_editor): series_ref.save() +def delete_duplicate_series(apps, schema_editor): + if connection.vendor == 'postgresql': + schema_editor.execute( + """ + DELETE + FROM + patchwork_seriesreference a + USING patchwork_seriesreference b + WHERE + a.id < b.id + AND a.project_id = b.project_id + AND a.msgid = b.msgid + """ + ) + elif connection.vendor == 'mysql': + schema_editor.execute( + """ + DELETE a FROM patchwork_seriesreference a + INNER JOIN patchwork_seriesreference b + WHERE + a.id < b.id + AND a.project_id = b.project_id + AND a.msgid = b.msgid + """ + ) + else: + Project = apps.get_model('patchwork', 'Project') + SeriesReference = apps.get_model('patchwork', 'SeriesReference') + + for project in Project.objects.all(): + ( + SeriesReference.objects.filter(project=project) + .annotate(count=Count('msgid')) + .filter(count__gt=1) + .delete() + ) + + class Migration(migrations.Migration): dependencies = [('patchwork', '0038_state_slug')] operations = [ - migrations.RunPython( - merge_duplicate_series, migrations.RunPython.noop, atomic=False - ), migrations.AddField( model_name='seriesreference', name='project', @@ -104,12 +83,12 @@ class Migration(migrations.Migration): to='patchwork.Project', ), ), - migrations.AlterUniqueTogether( - name='seriesreference', unique_together={('project', 'msgid')} - ), migrations.RunPython( copy_project_field, migrations.RunPython.noop, atomic=False ), + migrations.RunPython( + delete_duplicate_series, migrations.RunPython.noop, atomic=False + ), migrations.AlterField( model_name='seriesreference', name='project', @@ -118,4 +97,7 @@ class Migration(migrations.Migration): to='patchwork.Project', ), ), + migrations.AlterUniqueTogether( + name='seriesreference', unique_together={('project', 'msgid')} + ), ]