Merge ~andrey-fedoseev/launchpad:close-account into launchpad:master

Proposed by Andrey Fedoseev
Status: Merged
Approved by: Andrey Fedoseev
Approved revision: 7f4c454f3aec41e3ee0690c081ea7bd0291e2978
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~andrey-fedoseev/launchpad:close-account
Merge into: launchpad:master
Diff against target: 334 lines (+241/-16)
2 files modified
lib/lp/registry/scripts/closeaccount.py (+72/-16)
lib/lp/registry/scripts/tests/test_closeaccount.py (+169/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+428085@code.launchpad.net

Commit message

close-account script: skip references related to inactive products

announcement.registrant
milestonetag.created_by
productrelease.owner
productreleasefile.uploader

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/registry/scripts/closeaccount.py b/lib/lp/registry/scripts/closeaccount.py
index c4e59fd..708bd5b 100644
--- a/lib/lp/registry/scripts/closeaccount.py
+++ b/lib/lp/registry/scripts/closeaccount.py
@@ -8,9 +8,11 @@ __all__ = [
8 "CloseAccountScript",8 "CloseAccountScript",
9]9]
1010
11from typing import List, Tuple
12
11import six13import six
12from storm.exceptions import IntegrityError14from storm.exceptions import IntegrityError
13from storm.expr import LeftJoin, Lower, Or15from storm.expr import And, LeftJoin, Lower, Or
14from zope.component import getUtility16from zope.component import getUtility
15from zope.security.proxy import removeSecurityProxy17from zope.security.proxy import removeSecurityProxy
1618
@@ -19,8 +21,12 @@ from lp.answers.model.question import Question
19from lp.app.interfaces.launchpad import ILaunchpadCelebrities21from lp.app.interfaces.launchpad import ILaunchpadCelebrities
20from lp.bugs.model.bugtask import BugTask22from lp.bugs.model.bugtask import BugTask
21from lp.registry.interfaces.person import PersonCreationRationale23from lp.registry.interfaces.person import PersonCreationRationale
24from lp.registry.model.announcement import Announcement
25from lp.registry.model.milestone import Milestone
26from lp.registry.model.milestonetag import MilestoneTag
22from lp.registry.model.person import Person, PersonSettings27from lp.registry.model.person import Person, PersonSettings
23from lp.registry.model.product import Product28from lp.registry.model.product import Product
29from lp.registry.model.productrelease import ProductRelease, ProductReleaseFile
24from lp.registry.model.productseries import ProductSeries30from lp.registry.model.productseries import ProductSeries
25from lp.services.database import postgresql31from lp.services.database import postgresql
26from lp.services.database.constants import DEFAULT32from lp.services.database.constants import DEFAULT
@@ -419,7 +425,7 @@ def close_account(username, log):
419 "Can't delete non-trivial PPAs for user %s" % person_name425 "Can't delete non-trivial PPAs for user %s" % person_name
420 )426 )
421427
422 has_references = False428 reference_counts = [] # type: List[Tuple[str, int]]
423429
424 # Check for active related projects, and skip inactive ones.430 # Check for active related projects, and skip inactive ones.
425 for col in "bug_supervisor", "driver", "owner":431 for col in "bug_supervisor", "driver", "owner":
@@ -434,11 +440,7 @@ def close_account(username, log):
434 )440 )
435 count = result.get_one()[0]441 count = result.get_one()[0]
436 if count:442 if count:
437 log.error(443 reference_counts.append(("product.{}".format(col), count))
438 "User %s is still referenced by %d product.%s values"
439 % (person_name, count, col)
440 )
441 has_references = True
442 skip.add(("product", col))444 skip.add(("product", col))
443 for col in "driver", "owner":445 for col in "driver", "owner":
444 count = store.find(446 count = store.find(
@@ -448,13 +450,65 @@ def close_account(username, log):
448 getattr(ProductSeries, col) == person,450 getattr(ProductSeries, col) == person,
449 ).count()451 ).count()
450 if count:452 if count:
451 log.error(453 reference_counts.append(("productseries.{}".format(col), count))
452 "User %s is still referenced by %d productseries.%s values"
453 % (person_name, count, col)
454 )
455 has_references = True
456 skip.add(("productseries", col))454 skip.add(("productseries", col))
457455
456 # Check announcements, skipping the ones
457 # that are related to inactive products.
458 count = store.find(
459 Announcement,
460 Or(
461 And(Announcement.product == Product.id, Product.active),
462 Announcement.product == None,
463 ),
464 Announcement.registrant == person,
465 ).count()
466 if count:
467 reference_counts.append(("announcement.registrant", count))
468 skip.add(("announcement", "registrant"))
469
470 # Check MilestoneTags, skipping the ones
471 # that are related to inactive products / product series.
472 count = store.find(
473 MilestoneTag,
474 MilestoneTag.milestone_id == Milestone.id,
475 Or(
476 And(Milestone.product == Product.id, Product.active),
477 Milestone.product == None,
478 ),
479 MilestoneTag.created_by_id == person.id,
480 ).count()
481 if count:
482 reference_counts.append(("milestonetag.created_by", count))
483 skip.add(("milestonetag", "created_by"))
484
485 # Check ProductReleases, skipping the ones
486 # that are related to inactive products / product series.
487 count = store.find(
488 ProductRelease,
489 ProductRelease.milestone == Milestone.id,
490 Milestone.product == Product.id,
491 Product.active,
492 ProductRelease.owner == person.id,
493 ).count()
494 if count:
495 reference_counts.append(("productrelease.owner", count))
496 skip.add(("productrelease", "owner"))
497
498 # Check ProductReleases, skipping the ones
499 # that are related to inactive products / product series.
500 count = store.find(
501 ProductReleaseFile,
502 ProductReleaseFile.productrelease == ProductRelease.id,
503 ProductRelease.milestone == Milestone.id,
504 Milestone.product == Product.id,
505 Product.active,
506 ProductReleaseFile.uploader == person.id,
507 ).count()
508 if count:
509 reference_counts.append(("productreleasefile.uploader", count))
510 skip.add(("productreleasefile", "uploader"))
511
458 # Closing the account will only work if all references have been handled512 # Closing the account will only work if all references have been handled
459 # by this point. If not, it's safer to bail out. It's OK if this513 # by this point. If not, it's safer to bail out. It's OK if this
460 # doesn't work in all conceivable situations, since some of them may514 # doesn't work in all conceivable situations, since some of them may
@@ -474,12 +528,14 @@ def close_account(username, log):
474 )528 )
475 count = result.get_one()[0]529 count = result.get_one()[0]
476 if count:530 if count:
531 reference_counts.append(("{}.{}".format(src_tab, src_col), count))
532
533 if reference_counts:
534 for reference, count in reference_counts:
477 log.error(535 log.error(
478 "User %s is still referenced by %d %s.%s values"536 "User %s is still referenced by %d %s values"
479 % (person_name, count, src_tab, src_col)537 % (person_name, count, reference)
480 )538 )
481 has_references = True
482 if has_references:
483 raise LaunchpadScriptFailure(539 raise LaunchpadScriptFailure(
484 "User %s is still referenced" % person_name540 "User %s is still referenced" % person_name
485 )541 )
diff --git a/lib/lp/registry/scripts/tests/test_closeaccount.py b/lib/lp/registry/scripts/tests/test_closeaccount.py
index 348b747..b78ff60 100644
--- a/lib/lp/registry/scripts/tests/test_closeaccount.py
+++ b/lib/lp/registry/scripts/tests/test_closeaccount.py
@@ -2,7 +2,9 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test the close-account script."""4"""Test the close-account script."""
5from datetime import datetime
56
7import pytz
6import transaction8import transaction
7from storm.store import Store9from storm.store import Store
8from testtools.matchers import (10from testtools.matchers import (
@@ -25,6 +27,7 @@ from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
25from lp.code.tests.helpers import GitHostingFixture27from lp.code.tests.helpers import GitHostingFixture
26from lp.registry.interfaces.person import IPersonSet28from lp.registry.interfaces.person import IPersonSet
27from lp.registry.interfaces.teammembership import ITeamMembershipSet29from lp.registry.interfaces.teammembership import ITeamMembershipSet
30from lp.registry.model.productrelease import ProductRelease
28from lp.registry.scripts.closeaccount import CloseAccountScript31from lp.registry.scripts.closeaccount import CloseAccountScript
29from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache32from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache
30from lp.services.database.interfaces import IStore33from lp.services.database.interfaces import IStore
@@ -1005,3 +1008,169 @@ class TestCloseAccount(TestCaseWithFactory):
1005 self.assertRemoved(account_id, person_id)1008 self.assertRemoved(account_id, person_id)
1006 self.assertIsNone(store.get(Archive, ppa_id))1009 self.assertIsNone(store.get(Archive, ppa_id))
1007 self.assertEqual(other_ppa, store.get(Archive, other_ppa_id))1010 self.assertEqual(other_ppa, store.get(Archive, other_ppa_id))
1011
1012 def test_skips_announcements_in_deactivated_products(self):
1013 person = self.factory.makePerson()
1014 person_id = person.id
1015 account_id = person.account.id
1016
1017 product = self.factory.makeProduct()
1018 product.announce(person, "announcement")
1019
1020 script = self.makeScript([person.name])
1021 with dbuser("launchpad"):
1022 self.assertRaisesWithContent(
1023 LaunchpadScriptFailure,
1024 "User %s is still referenced" % person.name,
1025 self.runScript,
1026 script,
1027 )
1028 self.assertNotRemoved(account_id, person_id)
1029
1030 product.active = False
1031 with dbuser("launchpad"):
1032 self.runScript(script)
1033
1034 self.assertRemoved(account_id, person_id)
1035
1036 def test_non_product_announcements_are_not_skipped(self):
1037 person = self.factory.makePerson()
1038 person_id = person.id
1039 account_id = person.account.id
1040
1041 distro = self.factory.makeDistribution()
1042 distro.announce(person, "announcement")
1043
1044 script = self.makeScript([person.name])
1045 with dbuser("launchpad"):
1046 self.assertRaisesWithContent(
1047 LaunchpadScriptFailure,
1048 "User %s is still referenced" % person.name,
1049 self.runScript,
1050 script,
1051 )
1052 self.assertNotRemoved(account_id, person_id)
1053
1054 def test_skip_milestone_tags_from_inactive_products(self):
1055
1056 active_product = self.factory.makeProduct()
1057 inactive_product = self.factory.makeProduct()
1058 inactive_product.active = False
1059
1060 active_product_series = self.factory.makeProductSeries(
1061 product=active_product
1062 )
1063 inactive_product_series = self.factory.makeProductSeries(
1064 product=inactive_product
1065 )
1066
1067 for milestone_target, expected_to_be_removed in (
1068 ({"product": active_product}, False),
1069 ({"product": inactive_product}, True),
1070 ({"productseries": active_product_series}, False),
1071 ({"productseries": inactive_product_series}, True),
1072 ({"distribution": self.factory.makeDistribution()}, False),
1073 ({"distroseries": self.factory.makeDistroSeries()}, False),
1074 ):
1075 person = self.factory.makePerson()
1076 person_id = person.id
1077 account_id = person.account.id
1078
1079 milestone = self.factory.makeMilestone(**milestone_target)
1080 milestone.setTags(["tag"], person)
1081 script = self.makeScript([person.name])
1082 with dbuser("launchpad"):
1083 if not expected_to_be_removed:
1084 self.assertRaisesWithContent(
1085 LaunchpadScriptFailure,
1086 "User %s is still referenced" % person.name,
1087 self.runScript,
1088 script,
1089 )
1090 self.assertNotRemoved(account_id, person_id)
1091 else:
1092 self.runScript(script)
1093 self.assertRemoved(account_id, person_id)
1094
1095 def test_skip_product_releases_from_inactive_products(self):
1096
1097 active_product = self.factory.makeProduct()
1098 inactive_product = self.factory.makeProduct()
1099 inactive_product.active = False
1100
1101 active_product_series = self.factory.makeProductSeries(
1102 product=active_product
1103 )
1104 inactive_product_series = self.factory.makeProductSeries(
1105 product=inactive_product
1106 )
1107
1108 for milestone_target, expected_to_be_removed in (
1109 ({"product": active_product}, False),
1110 ({"product": inactive_product}, True),
1111 ({"productseries": active_product_series}, False),
1112 ({"productseries": inactive_product_series}, True),
1113 ):
1114 person = self.factory.makePerson()
1115 person_id = person.id
1116 account_id = person.account.id
1117
1118 milestone = self.factory.makeMilestone(**milestone_target)
1119 milestone.createProductRelease(person, datetime.now(pytz.UTC))
1120 script = self.makeScript([person.name])
1121 with dbuser("launchpad"):
1122 if not expected_to_be_removed:
1123 self.assertRaisesWithContent(
1124 LaunchpadScriptFailure,
1125 "User %s is still referenced" % person.name,
1126 self.runScript,
1127 script,
1128 )
1129 self.assertNotRemoved(account_id, person_id)
1130 else:
1131 self.runScript(script)
1132 self.assertRemoved(account_id, person_id)
1133
1134 def test_skip_product_releases_files_from_inactive_products(self):
1135
1136 active_product = self.factory.makeProduct()
1137 inactive_product = self.factory.makeProduct()
1138 inactive_product.active = False
1139
1140 active_product_series = self.factory.makeProductSeries(
1141 product=active_product
1142 )
1143 inactive_product_series = self.factory.makeProductSeries(
1144 product=inactive_product
1145 )
1146
1147 for milestone_target, expected_to_be_removed in (
1148 ({"product": active_product}, False),
1149 ({"product": inactive_product}, True),
1150 ({"productseries": active_product_series}, False),
1151 ({"productseries": inactive_product_series}, True),
1152 ):
1153 person = self.factory.makePerson()
1154 person_id = person.id
1155 account_id = person.account.id
1156
1157 milestone = self.factory.makeMilestone(**milestone_target)
1158 product_release = milestone.createProductRelease(
1159 milestone.product.owner, datetime.now(pytz.UTC)
1160 ) # type: ProductRelease
1161 product_release.addReleaseFile(
1162 "test.txt", b"test", "text/plain", person
1163 )
1164 script = self.makeScript([person.name])
1165 with dbuser("launchpad"):
1166 if not expected_to_be_removed:
1167 self.assertRaisesWithContent(
1168 LaunchpadScriptFailure,
1169 "User %s is still referenced" % person.name,
1170 self.runScript,
1171 script,
1172 )
1173 self.assertNotRemoved(account_id, person_id)
1174 else:
1175 self.runScript(script)
1176 self.assertRemoved(account_id, person_id)

Subscribers

People subscribed via source and target branches

to status/vote changes: