Merge lp:~edwin-grubbs/launchpad/bug-676966-disallow-merging-people-with-ppas into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 12123
Proposed branch: lp:~edwin-grubbs/launchpad/bug-676966-disallow-merging-people-with-ppas
Merge into: lp:launchpad
Diff against target: 251 lines (+123/-12)
6 files modified
lib/lp/registry/browser/peoplemerge.py (+8/-0)
lib/lp/registry/browser/person.py (+4/-8)
lib/lp/registry/browser/tests/test_peoplemerge.py (+54/-3)
lib/lp/registry/doc/person-merge.txt (+16/-0)
lib/lp/registry/interfaces/person.py (+9/-0)
lib/lp/registry/model/person.py (+32/-1)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-676966-disallow-merging-people-with-ppas
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
j.c.sackett (community) code* Approve
Review via email: mp+43116@code.launchpad.net

Commit message

[r=jcsackett,sinzui][ui=none][bug=676966] Don't allow a person/team to be merged until its PPAs are deleted.

Description of the change

Summary
-------

To prevent oopses in the PPA publisher, a form error message is
displayed when the user tries to merge a person or team with a PPA
containing published packages.

Implementation details
----------------------

Since the merge views are using the same check as renaming a person with
a PPA, I have moved that check into the model and updated the rename
person view. Person.merge() also checks that the merged person does not
have a PPA with published packages.
    lib/lp/registry/browser/person.py
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/model/person.py

Added check to the AdminMergeBaseView so that the check applies to
+adminpeoplemerge and +adminteammerge.
    lib/lp/registry/browser/peoplemerge.py
    lib/lp/registry/browser/tests/test_peoplemerge.py
    lib/lp/registry/doc/person-merge.txt

Tests
-----

./bin/test -vv -t whatever

Demo and Q/A
------------

* Open http://launchpad.dev/...

Lint
----

Linting changed files:
  lib/lp/registry/browser/peoplemerge.py
  lib/lp/registry/browser/person.py
  lib/lp/registry/browser/tests/test_peoplemerge.py
  lib/lp/registry/doc/person-merge.txt
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/model/person.py

./lib/lp/registry/browser/peoplemerge.py
     143: E301 expected 1 blank line, found 2
./lib/lp/registry/doc/person-merge.txt
       1: narrative uses a moin header.
      22: narrative uses a moin header.
      32: narrative uses a moin header.
      61: narrative exceeds 78 characters.
      85: narrative exceeds 78 characters.
     105: narrative uses a moin header.
     122: narrative uses a moin header.
     339: narrative uses a moin header.
./lib/lp/registry/interfaces/person.py
     493: E302 expected 2 blank lines, found 1
./lib/lp/registry/model/person.py
    1480: W291 trailing whitespace
    1480: Line has trailing whitespace.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :
Download full text (4.7 KiB)

Hey Edwin--

I really like this branch. I just have some suggestions/questions on comments below.

> === modified file 'lib/lp/registry/browser/peoplemerge.py'
> --- lib/lp/registry/browser/peoplemerge.py 2010-12-03 16:33:03 +0000
> +++ lib/lp/registry/browser/peoplemerge.py 2010-12-08 18:32:00 +0000
> @@ -132,6 +132,13 @@
> if dupe_person == target_person and dupe_person is not None:
> self.addError(_("You can't merge ${name} into itself.",
> mapping=dict(name=dupe_person.name)))
> + # We cannot merge the teams if there is a PPA with published
> + # packages on the duplicate person, unless that PPA is removed.
> + if dupe_person.has_ppa_with_published_packages:
> + self.addError(_(
> + "${name} has a PPA with published packages; we "
> + "can't merge it.", mapping=dict(name=dupe_person.name)))

It might be good to direct someone to info on what they can do to get around this? In
other places where we throw an error in merging we indicate that a team needs to be removed or something; perhaps "the PPA must be deleted before we can merge ${name}, after the merge a new PPA can be recreated?

> === modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py'
> --- lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-03 16:33:03 +0000
> +++ lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-08 18:32:00 +0000
> @@ -152,3 +155,51 @@
> view = self.getView()
> self.assertEqual([], view.errors)
> self.assertEqual(self.target_team, self.dupe_team.merged)
> +
> + def test_cannot_merge_team_with_ppa_containing_published_packages(self):
> + # The PPA must be removed before the team can be merged.
> + login_celebrity('admin')
> + self.dupe_team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
> + archive = self.dupe_team.createPPA()
> + self.factory.makeSourcePackagePublishingHistory(archive=archive)
> + login_celebrity('registry_experts')
> + view = self.getView()
> + self.assertEqual(
> + [u"dupe-team has a PPA with published packages; "
> + "we can't merge it."],
> + view.errors)

I'd change the comment to just "A team cannot be merged with published PPA" since the test doesn't demonstrate removing the PPA to make it work.

> === modified file 'lib/lp/registry/doc/person-merge.txt'
> --- lib/lp/registry/doc/person-merge.txt 2010-12-01 23:39:05 +0000
> +++ lib/lp/registry/doc/person-merge.txt 2010-12-08 18:32:00 +0000
> @@ -425,6 +425,8 @@
> >>> test_team.deactivateAllMembers(comment, personset.getByName('name16'))
> >>> for team in test_team.super_teams:
> ... test_team.retractTeamMembership(team, test_team.teamowner)
> +
> +
> >>> personset.merge(test_team, landscape)
>
> # The resulting Landscape-developers no new super teams, has

Thanks for the cleanup.

> @@ -438,3 +440,18 @@
> []
> >>> list(IPollSubset(landscape).getAll())
> []
> +
> +A person with a PPA can't be merged until the PPA is deleted.
> +
> + >>> person_with_ppa = factory.makePerson()
> + >>> ot...

Read more...

review: Needs Information (code*)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (7.6 KiB)

Hi JC,

Thanks for the review. Here are the changes you asked for.

> Hey Edwin--
>
> I really like this branch. I just have some suggestions/questions on comments
> below.
>
> > === modified file 'lib/lp/registry/browser/peoplemerge.py'
> > --- lib/lp/registry/browser/peoplemerge.py 2010-12-03 16:33:03 +0000
> > +++ lib/lp/registry/browser/peoplemerge.py 2010-12-08 18:32:00 +0000
> > @@ -132,6 +132,13 @@
> > if dupe_person == target_person and dupe_person is not None:
> > self.addError(_("You can't merge ${name} into itself.",
> > mapping=dict(name=dupe_person.name)))
> > + # We cannot merge the teams if there is a PPA with published
> > + # packages on the duplicate person, unless that PPA is removed.
> > + if dupe_person.has_ppa_with_published_packages:
> > + self.addError(_(
> > + "${name} has a PPA with published packages; we "
> > + "can't merge it.", mapping=dict(name=dupe_person.name)))
>
> It might be good to direct someone to info on what they can do to get around
> this? In
> other places where we throw an error in merging we indicate that a team needs
> to be removed or something; perhaps "the PPA must be deleted before we can
> merge ${name}, after the merge a new PPA can be recreated?

I did something similar but a little bit shorter.

> > === modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py'
> > --- lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-03 16:33:03
> +0000
> > +++ lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-08 18:32:00
> +0000
> > @@ -152,3 +155,51 @@
> > view = self.getView()
> > self.assertEqual([], view.errors)
> > self.assertEqual(self.target_team, self.dupe_team.merged)
> > +
> > + def
> test_cannot_merge_team_with_ppa_containing_published_packages(self):
> > + # The PPA must be removed before the team can be merged.
> > + login_celebrity('admin')
> > + self.dupe_team.subscriptionpolicy =
> TeamSubscriptionPolicy.MODERATED
> > + archive = self.dupe_team.createPPA()
> > + self.factory.makeSourcePackagePublishingHistory(archive=archive)
> > + login_celebrity('registry_experts')
> > + view = self.getView()
> > + self.assertEqual(
> > + [u"dupe-team has a PPA with published packages; "
> > + "we can't merge it."],
> > + view.errors)
>
> I'd change the comment to just "A team cannot be merged with published PPA"
> since the test doesn't demonstrate removing the PPA to make it work.

Done.

> > === modified file 'lib/lp/registry/doc/person-merge.txt'
> > --- lib/lp/registry/doc/person-merge.txt 2010-12-01 23:39:05 +0000
> > +++ lib/lp/registry/doc/person-merge.txt 2010-12-08 18:32:00 +0000
> > @@ -425,6 +425,8 @@
> > >>> test_team.deactivateAllMembers(comment,
> personset.getByName('name16'))
> > >>> for team in test_team.super_teams:
> > ... test_team.retractTeamMembership(team, test_team.teamowner)
> > +
> > +
> > >>> personset.merge(test_team, landscape)
> >
> > # The resulting Landscape-developers no new...

Read more...

Revision history for this message
j.c.sackett (jcsackett) wrote :

Edwin--

Thanks for those changes.

review: Approve (code*)
Revision history for this message
Curtis Hovey (sinzui) wrote :

As we discussed on IRC "PPA that must be removed" should be "PPA that must be deleted" since that is the link the user must use.

has_ppa_with_published_packages() will allow users to merge active ppas without packages. I think these ppas will show up in searches. Can you talk to someone on the soyuz team

review: Needs Information (code)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (6.6 KiB)

> As we discussed on IRC "PPA that must be removed" should be "PPA that must be
> deleted" since that is the link the user must use.
>
> has_ppa_with_published_packages() will allow users to merge active ppas
> without packages. I think these ppas will show up in searches. Can you talk to
> someone on the soyuz team

Hi Curtis,

Thanks for the review.

I have changed the condition to not allow merges on a person or team with a PPA in the ACTIVE or DELETING status whether or not it has published packages, so that links to merged persons don't show up in https://launchpad.net/~PERSON/+archivesubscriptions

Incremental Diff:

=== modified file 'lib/lp/registry/browser/peoplemerge.py'
--- lib/lp/registry/browser/peoplemerge.py 2010-12-15 20:17:39 +0000
+++ lib/lp/registry/browser/peoplemerge.py 2010-12-17 04:08:34 +0000
@@ -134,11 +134,12 @@
                   mapping=dict(name=dupe_person.name)))
         # We cannot merge the teams if there is a PPA with published
         # packages on the duplicate person, unless that PPA is removed.
- if dupe_person.has_ppa_with_published_packages:
+ if dupe_person.has_existing_ppa:
             self.addError(_(
- "${name} has a PPA that must be removed before it "
- "can be merged.", mapping=dict(name=dupe_person.name)))
-
+ "${name} has a PPA that must be deleted before it "
+ "can be merged. It may take ten minutes to remove the "
+ "deleted PPA's files.",
+ mapping=dict(name=dupe_person.name)))

     def render(self):
         # Subclasses may define other actions that they will render manually

=== modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py'
--- lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-15 20:17:39 +0000
+++ lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-17 04:07:09 +0000
@@ -156,17 +156,17 @@
         self.assertEqual([], view.errors)
         self.assertEqual(self.target_team, self.dupe_team.merged)

- def test_cannot_merge_team_with_ppa_containing_published_packages(self):
- # A team with a published PPA cannot be merged.
+ def test_cannot_merge_team_with_ppa(self):
+ # A team with a PPA cannot be merged.
         login_celebrity('admin')
         self.dupe_team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
         archive = self.dupe_team.createPPA()
- self.factory.makeSourcePackagePublishingHistory(archive=archive)
         login_celebrity('registry_experts')
         view = self.getView()
         self.assertEqual(
- [u'dupe-team has a PPA that must be removed before '
- 'it can be merged.'],
+ [u"dupe-team has a PPA that must be deleted before it can be "
+ "merged. It may take ten minutes to remove the deleted PPA's "
+ "files."],
             view.errors)

     def test_registry_delete_team_with_super_teams(self):
@@ -205,13 +205,13 @@
         return create_initialized_view(
             self.person_set, '+adminpeoplemerge', form=form)

- def test_cannot_merge_person_with_ppa_containing_published_packages(self):
- # The PPA must be r...

Read more...

Revision history for this message
Curtis Hovey (sinzui) wrote :

This branch looks good to land.

review: Approve (code)
Revision history for this message
Julian Edwards (julian-edwards) wrote :

I think that the restriction on merging people with PPAs with or without publications is wrong. I originally suggested that we allow the merge to take place even though there's a PPA as long as it has no packages (i.e. it's a brand new one). Deleting these types of PPAs has zero affect other than to change its status; there's nothing to actually delete on disk.

The +archivesubscriptions problem should be solved differently.

Also, has_existing_ppa and has_ppa_with_published_packages
should not be properties on IPerson, they should be on IArchiveSet so
that Soyuz-specific queries are kept in the lp.soyuz domain.

The Bugs team made this mistake with one of their bits of code that knows too
much about Soyuz publishing tables and caused circular imports when running
bin/test. It also makes future changes to soyuz code harder because the coder will not expect this sort of code to be outside the soyuz domain.

I recommend that this be fixed ASAP as tech debt. I filed bug 693357 about this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/peoplemerge.py'
2--- lib/lp/registry/browser/peoplemerge.py 2010-12-13 16:28:55 +0000
3+++ lib/lp/registry/browser/peoplemerge.py 2010-12-20 20:45:28 +0000
4@@ -132,6 +132,14 @@
5 if dupe_person == target_person and dupe_person is not None:
6 self.addError(_("You can't merge ${name} into itself.",
7 mapping=dict(name=dupe_person.name)))
8+ # We cannot merge the teams if there is a PPA with published
9+ # packages on the duplicate person, unless that PPA is removed.
10+ if dupe_person.has_existing_ppa:
11+ self.addError(_(
12+ "${name} has a PPA that must be deleted before it "
13+ "can be merged. It may take ten minutes to remove the "
14+ "deleted PPA's files.",
15+ mapping=dict(name=dupe_person.name)))
16
17 def render(self):
18 # Subclasses may define other actions that they will render manually
19
20=== modified file 'lib/lp/registry/browser/person.py'
21--- lib/lp/registry/browser/person.py 2010-12-17 17:49:50 +0000
22+++ lib/lp/registry/browser/person.py 2010-12-20 20:45:28 +0000
23@@ -327,7 +327,6 @@
24 from lp.soyuz.browser.archivesubscription import (
25 traverse_archive_subscription_for_subscriber,
26 )
27-from lp.soyuz.enums import ArchiveStatus
28 from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
29 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
30 from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
31@@ -4138,16 +4137,13 @@
32
33 When a user has a PPA renames are prohibited.
34 """
35- active_ppas = [
36- ppa for ppa in self.context.ppas
37- if ppa.status in (ArchiveStatus.ACTIVE, ArchiveStatus.DELETING)]
38- num_packages = sum(
39- ppa.getPublishedSources().count() for ppa in active_ppas)
40- if num_packages > 0:
41+ has_ppa_with_published_packages = (
42+ self.context.has_ppa_with_published_packages)
43+ if has_ppa_with_published_packages:
44 # This makes the field's widget display (i.e. read) only.
45 self.form_fields['name'].for_display = True
46 super(PersonEditView, self).setUpWidgets()
47- if num_packages > 0:
48+ if has_ppa_with_published_packages:
49 self.widgets['name'].hint = _(
50 'This user has an active PPA with packages published and '
51 'may not be renamed.')
52
53=== modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py'
54--- lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-13 16:28:55 +0000
55+++ lib/lp/registry/browser/tests/test_peoplemerge.py 2010-12-20 20:45:28 +0000
56@@ -12,8 +12,11 @@
57 )
58 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
59 from canonical.testing.layers import DatabaseFunctionalLayer
60-from lp.registry.interfaces.person import IPersonSet
61 from lp.registry.interfaces.mailinglist import MailingListStatus
62+from lp.registry.interfaces.person import (
63+ IPersonSet,
64+ TeamSubscriptionPolicy,
65+ )
66 from lp.testing import (
67 celebrity_logged_in,
68 login_celebrity,
69@@ -92,8 +95,8 @@
70 def setUp(self):
71 super(TestAdminTeamMergeView, self).setUp()
72 self.person_set = getUtility(IPersonSet)
73- self.dupe_team = self.factory.makeTeam()
74- self.target_team = self.factory.makeTeam()
75+ self.dupe_team = self.factory.makeTeam(name='dupe-team')
76+ self.target_team = self.factory.makeTeam(name='target-team')
77 login_celebrity('registry_experts')
78
79 def getView(self, form=None):
80@@ -153,6 +156,19 @@
81 self.assertEqual([], view.errors)
82 self.assertEqual(self.target_team, self.dupe_team.merged)
83
84+ def test_cannot_merge_team_with_ppa(self):
85+ # A team with a PPA cannot be merged.
86+ login_celebrity('admin')
87+ self.dupe_team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
88+ archive = self.dupe_team.createPPA()
89+ login_celebrity('registry_experts')
90+ view = self.getView()
91+ self.assertEqual(
92+ [u"dupe-team has a PPA that must be deleted before it can be "
93+ "merged. It may take ten minutes to remove the deleted PPA's "
94+ "files."],
95+ view.errors)
96+
97 def test_registry_delete_team_with_super_teams(self):
98 # Registry admins can delete teams with super team memberships.
99 self.target_team = getUtility(ILaunchpadCelebrities).registry_experts
100@@ -164,3 +180,38 @@
101 view = self.getView()
102 self.assertEqual([], view.errors)
103 self.assertEqual(self.target_team, self.dupe_team.merged)
104+
105+
106+class TestAdminPeopleMergeView(TestCaseWithFactory):
107+ """Test the AdminPeopleMergeView rules."""
108+
109+ layer = DatabaseFunctionalLayer
110+
111+ def setUp(self):
112+ super(TestAdminPeopleMergeView, self).setUp()
113+ self.person_set = getUtility(IPersonSet)
114+ self.dupe_person = self.factory.makePerson(name='dupe-person')
115+ self.target_person = self.factory.makePerson()
116+ login_celebrity('registry_experts')
117+
118+ def getView(self, form=None):
119+ if form is None:
120+ form = {
121+ 'field.dupe_person': self.dupe_person.name,
122+ 'field.target_person': self.target_person.name,
123+ 'field.actions.reassign_emails_and_merge':
124+ 'Reassign E-mails and Merge',
125+ }
126+ return create_initialized_view(
127+ self.person_set, '+adminpeoplemerge', form=form)
128+
129+ def test_cannot_merge_person_with_ppa(self):
130+ # A person with a PPA cannot be merged.
131+ login_celebrity('admin')
132+ archive = self.dupe_person.createPPA()
133+ view = self.getView()
134+ self.assertEqual(
135+ [u"dupe-person has a PPA that must be deleted before it can be "
136+ "merged. It may take ten minutes to remove the deleted PPA's "
137+ "files."],
138+ view.errors)
139
140=== modified file 'lib/lp/registry/doc/person-merge.txt'
141--- lib/lp/registry/doc/person-merge.txt 2010-12-15 22:11:11 +0000
142+++ lib/lp/registry/doc/person-merge.txt 2010-12-20 20:45:28 +0000
143@@ -422,6 +422,8 @@
144 >>> test_team.deactivateAllMembers(comment, personset.getByName('name16'))
145 >>> for team in test_team.super_teams:
146 ... test_team.retractTeamMembership(team, test_team.teamowner)
147+
148+
149 >>> personset.merge(test_team, landscape)
150
151 # The resulting Landscape-developers no new super teams and its
152@@ -432,3 +434,17 @@
153 [u'salgado', u'name12']
154 >>> [team.name for team in landscape.super_teams]
155 []
156+
157+A person with a PPA can't be merged.
158+
159+ >>> person_with_ppa = factory.makePerson()
160+ >>> other_person = factory.makePerson()
161+ >>> archive = person_with_ppa.createPPA()
162+ >>> email = IMasterObject(person_with_ppa.preferredemail)
163+ >>> email.status = EmailAddressStatus.VALIDATED
164+ >>> email.destroySelf()
165+ >>> transaction.commit()
166+ >>> personset.merge(person_with_ppa, other_person)
167+ Traceback (most recent call last):
168+ ...
169+ AssertionError: from_person has a ppa in ACTIVE or DELETING status
170
171=== modified file 'lib/lp/registry/interfaces/person.py'
172--- lib/lp/registry/interfaces/person.py 2010-12-16 18:56:02 +0000
173+++ lib/lp/registry/interfaces/person.py 2010-12-20 20:45:28 +0000
174@@ -862,6 +862,15 @@
175 # Really IArchive, see archive.py
176 value_type=Reference(schema=Interface)))
177
178+ has_existing_ppa = Bool(
179+ title=_("Does this person have a ppa that is active or not fully "
180+ "deleted?"),
181+ readonly=True, required=False)
182+
183+ has_ppa_with_published_packages = Bool(
184+ title=_("Does this person have a ppa with published packages?"),
185+ readonly=True, required=False)
186+
187 entitlements = Attribute("List of Entitlements for this person or team.")
188
189 structural_subscriptions = Attribute(
190
191=== modified file 'lib/lp/registry/model/person.py'
192--- lib/lp/registry/model/person.py 2010-12-17 04:48:01 +0000
193+++ lib/lp/registry/model/person.py 2010-12-20 20:45:28 +0000
194@@ -268,11 +268,15 @@
195 VOUCHER_STATUSES,
196 )
197 from lp.services.worlddata.model.language import Language
198-from lp.soyuz.enums import ArchivePurpose
199+from lp.soyuz.enums import (
200+ ArchivePurpose,
201+ ArchiveStatus,
202+ )
203 from lp.soyuz.interfaces.archive import IArchiveSet
204 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
205 from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
206 from lp.soyuz.model.archive import Archive
207+from lp.soyuz.model.publishing import SourcePackagePublishingHistory
208 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
209 from lp.translations.model.hastranslationimports import (
210 HasTranslationImportsMixin,
211@@ -2659,6 +2663,29 @@
212 return Archive.selectBy(
213 owner=self, purpose=ArchivePurpose.PPA, orderBy='name')
214
215+ @property
216+ def has_existing_ppa(self):
217+ """See `IPerson`."""
218+ result = Store.of(self).find(
219+ Archive,
220+ Archive.owner == self.id,
221+ Archive.purpose == ArchivePurpose.PPA,
222+ Archive.status.is_in(
223+ [ArchiveStatus.ACTIVE, ArchiveStatus.DELETING]))
224+ return not result.is_empty()
225+
226+ @property
227+ def has_ppa_with_published_packages(self):
228+ """See `IPerson`."""
229+ result = Store.of(self).find(
230+ Archive,
231+ SourcePackagePublishingHistory.archive == Archive.id,
232+ Archive.owner == self.id,
233+ Archive.purpose == ArchivePurpose.PPA,
234+ Archive.status.is_in(
235+ [ArchiveStatus.ACTIVE, ArchiveStatus.DELETING]))
236+ return not result.is_empty()
237+
238 def getPPAByName(self, name):
239 """See `IPerson`."""
240 return getUtility(IArchiveSet).getPPAOwnedByPerson(self, name)
241@@ -3759,6 +3786,10 @@
242 if getUtility(IEmailAddressSet).getByPerson(from_person).count() > 0:
243 raise AssertionError('from_person still has email addresses.')
244
245+ if from_person.has_existing_ppa:
246+ raise AssertionError(
247+ 'from_person has a ppa in ACTIVE or DELETING status')
248+
249 if from_person.is_team and from_person.allmembers.count() > 0:
250 raise AssertionError(
251 "Only teams without active members can be merged")