Merge lp:~cjwatson/launchpad/archive-subscriber-refactor-cancel into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18901
Proposed branch: lp:~cjwatson/launchpad/archive-subscriber-refactor-cancel
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/close-account-more-2
Diff against target: 235 lines (+83/-21)
6 files modified
lib/lp/registry/scripts/closeaccount.py (+8/-10)
lib/lp/security.py (+5/-0)
lib/lp/soyuz/configure.zcml (+5/-2)
lib/lp/soyuz/doc/archivesubscriber.txt (+20/-0)
lib/lp/soyuz/interfaces/archivesubscriber.py (+24/-4)
lib/lp/soyuz/model/archivesubscriber.py (+21/-5)
To merge this branch: bzr merge lp:~cjwatson/launchpad/archive-subscriber-refactor-cancel
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+364169@code.launchpad.net

Commit message

Factor out some duplication in ArchiveSubscriber cancellation.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/scripts/closeaccount.py'
2--- lib/lp/registry/scripts/closeaccount.py 2019-03-11 14:51:16 +0000
3+++ lib/lp/registry/scripts/closeaccount.py 2019-03-12 10:25:44 +0000
4@@ -25,10 +25,7 @@
5 PersonSettings,
6 )
7 from lp.services.database import postgresql
8-from lp.services.database.constants import (
9- DEFAULT,
10- UTC_NOW,
11- )
12+from lp.services.database.constants import DEFAULT
13 from lp.services.database.interfaces import IMasterStore
14 from lp.services.database.sqlbase import cursor
15 from lp.services.identity.interfaces.account import (
16@@ -42,6 +39,7 @@
17 LaunchpadScriptFailure,
18 )
19 from lp.soyuz.enums import ArchiveSubscriberStatus
20+from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
21 from lp.soyuz.model.archivesubscriber import ArchiveSubscriber
22
23
24@@ -320,13 +318,13 @@
25 # corresponding ArchiveSubscriber rows; but even once PPA authorisation
26 # is handled dynamically, we probably still want to have the per-person
27 # audit trail here.
28- store.find(
29- ArchiveSubscriber,
30+ archive_subscriber_ids = set(store.find(
31+ ArchiveSubscriber.id,
32 ArchiveSubscriber.subscriber_id == person.id,
33- ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT).set(
34- date_cancelled=UTC_NOW,
35- cancelled_by_id=janitor.id,
36- status=ArchiveSubscriberStatus.CANCELLED)
37+ ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT))
38+ if archive_subscriber_ids:
39+ getUtility(IArchiveSubscriberSet).cancel(
40+ archive_subscriber_ids, janitor)
41 skip.add(('archivesubscriber', 'subscriber'))
42 skip.add(('archiveauthtoken', 'person'))
43
44
45=== modified file 'lib/lp/security.py'
46--- lib/lp/security.py 2019-02-07 10:17:43 +0000
47+++ lib/lp/security.py 2019-03-12 10:25:44 +0000
48@@ -2923,6 +2923,11 @@
49 super(EditArchiveSubscriber, self).checkAuthenticated(user))
50
51
52+class AdminArchiveSubscriberSet(AdminByCommercialTeamOrAdmins):
53+ """Only (commercial) admins can manipulate archive subscribers in bulk."""
54+ usedfor = IArchiveSubscriberSet
55+
56+
57 class ViewSourcePackageRecipe(DelegatedAuthorization):
58
59 permission = "launchpad.View"
60
61=== modified file 'lib/lp/soyuz/configure.zcml'
62--- lib/lp/soyuz/configure.zcml 2018-11-18 18:30:00 +0000
63+++ lib/lp/soyuz/configure.zcml 2019-03-12 10:25:44 +0000
64@@ -1,4 +1,4 @@
65-<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the
66+<!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the
67 GNU Affero General Public License version 3 (see the file LICENSE).
68 -->
69
70@@ -515,7 +515,10 @@
71 class="lp.soyuz.model.archivesubscriber.ArchiveSubscriberSet"
72 provides="lp.soyuz.interfaces.archivesubscriber.IArchiveSubscriberSet">
73 <allow
74- interface="lp.soyuz.interfaces.archivesubscriber.IArchiveSubscriberSet"/>
75+ interface="lp.soyuz.interfaces.archivesubscriber.IArchiveSubscriberSetView"/>
76+ <require
77+ permission="launchpad.Admin"
78+ interface="lp.soyuz.interfaces.archivesubscriber.IArchiveSubscriberSetAdmin"/>
79 </securedutility>
80
81 <!-- PersonalArchiveSubscription -->
82
83=== modified file 'lib/lp/soyuz/doc/archivesubscriber.txt'
84--- lib/lp/soyuz/doc/archivesubscriber.txt 2018-05-27 18:32:33 +0000
85+++ lib/lp/soyuz/doc/archivesubscriber.txt 2019-03-12 10:25:44 +0000
86@@ -446,6 +446,26 @@
87 >>> login("admin@canonical.com")
88 >>> new_sub.cancel(cprov)
89
90+We can cancel subscriptions in bulk:
91+
92+ >>> login("celso.providelo@canonical.com")
93+ >>> subs = [
94+ ... cprov_private_ppa.newSubscription(factory.makePerson(), cprov)
95+ ... for _ in range(3)]
96+ >>> sub_set.cancel([subs[0].id, subs[1].id], cprov)
97+ Traceback (most recent call last):
98+ ...
99+ Unauthorized:...
100+
101+ >>> login("admin@canonical.com")
102+ >>> sub_set.cancel([subs[0].id, subs[1].id], cprov)
103+ >>> print(subs[0].status.name)
104+ CANCELLED
105+ >>> print(subs[1].status.name)
106+ CANCELLED
107+ >>> print(subs[2].status.name)
108+ CURRENT
109+
110
111 Finding all non-active subscribers
112 ----------------------------------
113
114=== modified file 'lib/lp/soyuz/interfaces/archivesubscriber.py'
115--- lib/lp/soyuz/interfaces/archivesubscriber.py 2015-03-06 10:22:08 +0000
116+++ lib/lp/soyuz/interfaces/archivesubscriber.py 2019-03-12 10:25:44 +0000
117@@ -1,4 +1,4 @@
118-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
119+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
120 # GNU Affero General Public License version 3 (see the file LICENSE).
121
122 """ArchiveSubscriber interface."""
123@@ -127,8 +127,8 @@
124 export_as_webservice_entry()
125
126
127-class IArchiveSubscriberSet(Interface):
128- """An interface for the set of all archive subscribers."""
129+class IArchiveSubscriberSetView(Interface):
130+ """An interface for launchpad.View ops on all archive subscribers."""
131
132 def getBySubscriber(subscriber, archive=None, current_only=True):
133 """Return all the subscriptions for a person.
134@@ -154,7 +154,7 @@
135 """
136
137 def getByArchive(archive, current_only=True):
138- """Return all the subscripions for an archive.
139+ """Return all the subscriptions for an archive.
140
141 :param archive: An `IArchive` for which to return all
142 `ArchiveSubscriber` records.
143@@ -163,6 +163,26 @@
144 """
145
146
147+class IArchiveSubscriberSetAdmin(Interface):
148+ """An interface for launchpad.Admin ops on all archive subscribers."""
149+
150+ def cancel(archive_subscriber_ids, cancelled_by):
151+ """Cancel a set of subscriptions.
152+
153+ :param archive_subscriber_ids: A sequence of `ArchiveSubscriber.id`
154+ values.
155+ :param cancelled_by: An `IPerson` who is cancelling the subscription.
156+
157+ Sets cancelled_by to the supplied person and date_cancelled to
158+ the current date/time.
159+ """
160+
161+
162+class IArchiveSubscriberSet(
163+ IArchiveSubscriberSetView, IArchiveSubscriberSetAdmin):
164+ """An interface for the set of all archive subscribers."""
165+
166+
167 class IPersonalArchiveSubscription(Interface):
168 """An abstract interface representing a subscription for an individual.
169
170
171=== modified file 'lib/lp/soyuz/model/archivesubscriber.py'
172--- lib/lp/soyuz/model/archivesubscriber.py 2015-09-23 15:38:13 +0000
173+++ lib/lp/soyuz/model/archivesubscriber.py 2019-03-12 10:25:44 +0000
174@@ -1,4 +1,4 @@
175-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
176+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
177 # GNU Affero General Public License version 3 (see the file LICENSE).
178
179 """Database class for table ArchiveSubscriber."""
180@@ -29,6 +29,7 @@
181 from storm.store import EmptyResultSet
182 from zope.component import getUtility
183 from zope.interface import implementer
184+from zope.security.proxy import removeSecurityProxy
185
186 from lp.registry.interfaces.person import (
187 IPersonSet,
188@@ -45,7 +46,10 @@
189 from lp.services.webapp.authorization import precache_permission_for_objects
190 from lp.soyuz.enums import ArchiveSubscriberStatus
191 from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet
192-from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriber
193+from lp.soyuz.interfaces.archivesubscriber import (
194+ IArchiveSubscriber,
195+ IArchiveSubscriberSet,
196+ )
197 from lp.soyuz.model.archive import Archive
198 from lp.soyuz.model.archiveauthtoken import ArchiveAuthToken
199
200@@ -94,9 +98,11 @@
201
202 def cancel(self, cancelled_by):
203 """See `IArchiveSubscriber`."""
204- self.date_cancelled = UTC_NOW
205- self.cancelled_by = cancelled_by
206- self.status = ArchiveSubscriberStatus.CANCELLED
207+ # The bulk cancel normally has stricter permissions, but if we've
208+ # got this far then we know the caller has enough permissions to
209+ # cancel just this subscription.
210+ removeSecurityProxy(getUtility(IArchiveSubscriberSet)).cancel(
211+ [self.id], cancelled_by)
212
213 def getNonActiveSubscribers(self):
214 """See `IArchiveSubscriber`."""
215@@ -149,6 +155,7 @@
216 EmailAddress.status == EmailAddressStatus.PREFERRED)
217
218
219+@implementer(IArchiveSubscriberSet)
220 class ArchiveSubscriberSet:
221 """See `IArchiveSubscriberSet`."""
222
223@@ -253,3 +260,12 @@
224 ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT)
225
226 return extra_exprs
227+
228+ def cancel(self, archive_subscriber_ids, cancelled_by):
229+ """See `IArchiveSubscriberSet`."""
230+ Store.of(cancelled_by).find(
231+ ArchiveSubscriber,
232+ ArchiveSubscriber.id.is_in(archive_subscriber_ids)).set(
233+ date_cancelled=UTC_NOW,
234+ cancelled_by_id=cancelled_by.id,
235+ status=ArchiveSubscriberStatus.CANCELLED)