Merge ~pappacena/launchpad:git-repo-async-privacy into launchpad:master
- Git
- lp:~pappacena/launchpad
- git-repo-async-privacy
- Merge into master
Status: | Needs review |
---|---|
Proposed branch: | ~pappacena/launchpad:git-repo-async-privacy |
Merge into: | launchpad:master |
Diff against target: |
734 lines (+312/-31) 17 files modified
database/schema/security.cfg (+18/-0) lib/lp/app/widgets/itemswidgets.py (+6/-2) lib/lp/code/browser/gitrepository.py (+9/-1) lib/lp/code/browser/tests/test_branchmergeproposal.py (+1/-1) lib/lp/code/browser/tests/test_gitrepository.py (+69/-3) lib/lp/code/configure.zcml (+9/-0) lib/lp/code/interfaces/gitjob.py (+20/-1) lib/lp/code/interfaces/gitrepository.py (+4/-0) lib/lp/code/model/gitjob.py (+57/-0) lib/lp/code/model/gitref.py (+1/-1) lib/lp/code/model/gitrepository.py (+42/-9) lib/lp/code/model/tests/test_gitcollection.py (+1/-1) lib/lp/code/model/tests/test_gitjob.py (+59/-0) lib/lp/code/model/tests/test_gitrepository.py (+4/-6) lib/lp/code/xmlrpc/tests/test_git.py (+5/-5) lib/lp/services/config/schema-lazr.conf (+6/-0) lib/lp/testing/factory.py (+1/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Needs Information | ||
Ioana Lasc (community) | Approve | ||
Review via email: mp+392776@code.launchpad.net |
Commit message
Doing repository privacy change in background, and blocking user from changing it while another change is in progress.
Description of the change
- 974b22a... by Thiago F. Pappacena
-
Fixing test
Colin Watson (cjwatson) wrote : | # |
I think this is mostly OK, but I wonder if it could be even better. Instead of adding a new item to GitRepositorySt
- 826cbad... by Thiago F. Pappacena
-
Encapsulating GitRepository.
pending_ change_ information_ type - eeeb231... by Thiago F. Pappacena
-
Renaming GitRepo information type change job
- 2493c1d... by Thiago F. Pappacena
-
Changing exception msg when blocking info type transition for git repo
Thiago F. Pappacena (pappacena) wrote : | # |
Pushed the requested changes.
I thought about just checking on-the-fly for unfinished jobs, but didn't go that way because it could be slightly expensive to run such query. But we do that in so few places that I agree it doesn't worth to concentrate that check in a garbo job. I have changed it too.
Unmerged commits
- 2493c1d... by Thiago F. Pappacena
-
Changing exception msg when blocking info type transition for git repo
- eeeb231... by Thiago F. Pappacena
-
Renaming GitRepo information type change job
- 826cbad... by Thiago F. Pappacena
-
Encapsulating GitRepository.
pending_ change_ information_ type - 974b22a... by Thiago F. Pappacena
-
Fixing test
- 4bd7dcc... by Thiago F. Pappacena
-
Fixing configuration of info type transition job
- 76e0272... by Thiago F. Pappacena
-
Blocking at the UI information type change while it is already changing
- caf2673... by Thiago F. Pappacena
-
Adding tests for async GitRepo.
transitionToInf ormationType - 3a5e053... by Thiago F. Pappacena
-
Changing GitRepo.
transitionInfor mationType method to be async
Preview Diff
1 | diff --git a/database/schema/security.cfg b/database/schema/security.cfg |
2 | index 11aba6f..290e7bc 100644 |
3 | --- a/database/schema/security.cfg |
4 | +++ b/database/schema/security.cfg |
5 | @@ -2082,6 +2082,24 @@ public.webhookjob = SELECT, INSERT |
6 | public.xref = SELECT, INSERT, DELETE |
7 | type=user |
8 | |
9 | +[privacy-change-jobs] |
10 | +groups=script |
11 | +public.accessartifact = SELECT, UPDATE, DELETE, INSERT |
12 | +public.accessartifactgrant = SELECT, UPDATE, DELETE, INSERT |
13 | +public.accesspolicyartifact = SELECT, UPDATE, DELETE, INSERT |
14 | +public.accesspolicygrant = SELECT, UPDATE, DELETE |
15 | +public.account = SELECT |
16 | +public.distribution = SELECT |
17 | +public.gitjob = SELECT, UPDATE |
18 | +public.gitrepository = SELECT, UPDATE |
19 | +public.gitsubscription = SELECT, UPDATE, DELETE |
20 | +public.job = SELECT, INSERT, UPDATE |
21 | +public.person = SELECT |
22 | +public.product = SELECT |
23 | +public.sharingjob = SELECT, INSERT, UPDATE |
24 | +public.teamparticipation = SELECT |
25 | +type=user |
26 | + |
27 | [sharing-jobs] |
28 | groups=script |
29 | public.accessartifactgrant = SELECT, UPDATE, DELETE |
30 | diff --git a/lib/lp/app/widgets/itemswidgets.py b/lib/lp/app/widgets/itemswidgets.py |
31 | index 1dbb59f..a644a96 100644 |
32 | --- a/lib/lp/app/widgets/itemswidgets.py |
33 | +++ b/lib/lp/app/widgets/itemswidgets.py |
34 | @@ -189,25 +189,29 @@ class LaunchpadRadioWidgetWithDescription(LaunchpadRadioWidget): |
35 | """Render an item of the list.""" |
36 | text = html_escape(text) |
37 | id = '%s.%s' % (name, index) |
38 | + extra_attr = {"disabled": "disabled"} if self.context.readonly else {} |
39 | elem = renderElement(u'input', |
40 | value=value, |
41 | name=name, |
42 | id=id, |
43 | cssClass=cssClass, |
44 | - type='radio') |
45 | + type='radio', |
46 | + **extra_attr) |
47 | return self._renderRow(text, value, id, elem) |
48 | |
49 | def renderSelectedItem(self, index, text, value, name, cssClass): |
50 | """Render a selected item of the list.""" |
51 | text = html_escape(text) |
52 | id = '%s.%s' % (name, index) |
53 | + extra_attr = {"disabled": "disabled"} if self.context.readonly else {} |
54 | elem = renderElement(u'input', |
55 | value=value, |
56 | name=name, |
57 | id=id, |
58 | cssClass=cssClass, |
59 | checked="checked", |
60 | - type='radio') |
61 | + type='radio', |
62 | + **extra_attr) |
63 | return self._renderRow(text, value, id, elem) |
64 | |
65 | def renderExtraHint(self): |
66 | diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py |
67 | index 2552ffb..1ec1b16 100644 |
68 | --- a/lib/lp/code/browser/gitrepository.py |
69 | +++ b/lib/lp/code/browser/gitrepository.py |
70 | @@ -483,6 +483,8 @@ class GitRepositoryView(InformationTypePortletMixin, LaunchpadView, |
71 | def warning_message(self): |
72 | if self.context.status == GitRepositoryStatus.CREATING: |
73 | return "This repository is being created." |
74 | + if self.context.pending_change_information_type: |
75 | + return "This repository's information type is being changed." |
76 | return None |
77 | |
78 | @property |
79 | @@ -573,6 +575,7 @@ class GitRepositoryEditFormView(LaunchpadEditFormView): |
80 | @cachedproperty |
81 | def schema(self): |
82 | info_types = self.getInformationTypesToShow() |
83 | + read_only_info_type = self.context.pending_change_information_type |
84 | |
85 | class GitRepositoryEditSchema(Interface): |
86 | """Defines the fields for the edit form. |
87 | @@ -582,7 +585,8 @@ class GitRepositoryEditFormView(LaunchpadEditFormView): |
88 | """ |
89 | use_template(IGitRepository, include=["default_branch"]) |
90 | information_type = copy_field( |
91 | - IGitRepository["information_type"], readonly=False, |
92 | + IGitRepository["information_type"], |
93 | + readonly=read_only_info_type, |
94 | vocabulary=InformationTypeVocabulary(types=info_types)) |
95 | name = copy_field(IGitRepository["name"], readonly=False) |
96 | owner = copy_field(IGitRepository["owner"], readonly=False) |
97 | @@ -785,6 +789,10 @@ class GitRepositoryEditView(CodeEditOwnerMixin, GitRepositoryEditFormView): |
98 | self.widgets["target"].hint = ( |
99 | "This is the default repository for this target, so it " |
100 | "cannot be moved to another target.") |
101 | + if self.context.pending_change_information_type: |
102 | + self.widgets["information_type"].hint = ( |
103 | + "The information type is being changed. The operation needs " |
104 | + "to finish before you can change it again.") |
105 | if self.context.default_branch: |
106 | self.widgets['default_branch'].context.required = True |
107 | |
108 | diff --git a/lib/lp/code/browser/tests/test_branchmergeproposal.py b/lib/lp/code/browser/tests/test_branchmergeproposal.py |
109 | index f2ec4ec..83c51dc 100644 |
110 | --- a/lib/lp/code/browser/tests/test_branchmergeproposal.py |
111 | +++ b/lib/lp/code/browser/tests/test_branchmergeproposal.py |
112 | @@ -2336,7 +2336,7 @@ class TestLatestProposalsForEachBranchGit( |
113 | |
114 | @staticmethod |
115 | def _setBranchInvisible(branch): |
116 | - removeSecurityProxy(branch.repository).transitionToInformationType( |
117 | + removeSecurityProxy(branch.repository)._transitionToInformationType( |
118 | InformationType.USERDATA, branch.owner, verify_policy=False) |
119 | |
120 | |
121 | diff --git a/lib/lp/code/browser/tests/test_gitrepository.py b/lib/lp/code/browser/tests/test_gitrepository.py |
122 | index 262d038..13a3a16 100644 |
123 | --- a/lib/lp/code/browser/tests/test_gitrepository.py |
124 | +++ b/lib/lp/code/browser/tests/test_gitrepository.py |
125 | @@ -60,6 +60,9 @@ from lp.code.enums import ( |
126 | GitRepositoryType, |
127 | ) |
128 | from lp.code.interfaces.gitcollection import IGitCollection |
129 | +from lp.code.interfaces.gitjob import ( |
130 | + IGitRepositoryChangeInformationTypeJobSource, |
131 | + ) |
132 | from lp.code.interfaces.gitrepository import IGitRepositorySet |
133 | from lp.code.interfaces.revision import IRevisionSet |
134 | from lp.code.model.gitjob import GitRefScanJob |
135 | @@ -161,6 +164,15 @@ class TestGitRepositoryView(BrowserTestCase): |
136 | self.assertTextMatchesExpressionIgnoreWhitespace( |
137 | r"""This repository is being created\..*""", text) |
138 | |
139 | + def test_changing_info_type_warning_message_is_present(self): |
140 | + repository = removeSecurityProxy(self.factory.makeGitRepository()) |
141 | + repository.transitionToInformationType( |
142 | + InformationType.PRIVATESECURITY, repository.owner) |
143 | + text = self.getMainText(repository, "+index", user=repository.owner) |
144 | + self.assertTextMatchesExpressionIgnoreWhitespace( |
145 | + r"""This repository's information type is being changed\..*""", |
146 | + text) |
147 | + |
148 | def test_creating_warning_message_is_not_shown(self): |
149 | repository = removeSecurityProxy(self.factory.makeGitRepository()) |
150 | repository.status = GitRepositoryStatus.AVAILABLE |
151 | @@ -1185,8 +1197,54 @@ class TestGitRepositoryEditView(TestCaseWithFactory): |
152 | browser.getControl("Private", index=1).click() |
153 | browser.getControl("Change Git Repository").click() |
154 | with person_logged_in(person): |
155 | - self.assertEqual( |
156 | - InformationType.USERDATA, repository.information_type) |
157 | + self.assertTrue(repository.pending_change_information_type) |
158 | + job_util = getUtility( |
159 | + IGitRepositoryChangeInformationTypeJobSource) |
160 | + jobs = list(job_util.iterReady()) |
161 | + self.assertEqual(1, len(jobs)) |
162 | + job = removeSecurityProxy(jobs[0]) |
163 | + self.assertEqual(repository, job.repository) |
164 | + self.assertEqual(InformationType.USERDATA, job.information_type) |
165 | + self.assertEqual(admin, job.user) |
166 | + |
167 | + def test_information_type_in_ui_blocked_if_already_changing(self): |
168 | + # The information_type of a repository can't be changed via the UI |
169 | + # if the repository is already pending a info type change. |
170 | + person = self.factory.makePerson() |
171 | + repository = self.factory.makeGitRepository(owner=person) |
172 | + with admin_logged_in(): |
173 | + repository.transitionToInformationType( |
174 | + InformationType.PRIVATESECURITY, repository.owner) |
175 | + # There should be 1 job pending to be executed. |
176 | + job_util = getUtility( |
177 | + IGitRepositoryChangeInformationTypeJobSource) |
178 | + jobs = list(job_util.iterReady()) |
179 | + self.assertEqual(1, len(jobs)) |
180 | + admin = getUtility(ILaunchpadCelebrities).admin.teamowner |
181 | + browser = self.getUserBrowser( |
182 | + canonical_url(repository) + "/+edit", user=admin) |
183 | + # Make sure the privacy controls are all disabled in the UI. |
184 | + controls = [ |
185 | + "Public", "Public Security", "Private Security", "Private", |
186 | + "Proprietary", "Embargoed"] |
187 | + self.assertTrue( |
188 | + all(browser.getControl(i, index=0).disabled for i in controls)) |
189 | + expected_msg = ( |
190 | + "The information type is being changed. The operation needs to " |
191 | + "finish before you can change it again.") |
192 | + self.assertIn(expected_msg, extract_text(browser.contents)) |
193 | + |
194 | + # Trying to change should have no effect in the backend, since the |
195 | + # repository is already changing info type and this field is read-only. |
196 | + browser.getControl("Private", index=1).click() |
197 | + browser.getControl("Change Git Repository").click() |
198 | + with person_logged_in(person): |
199 | + self.assertTrue(repository.pending_change_information_type) |
200 | + # It should have the same job it already had. |
201 | + job_util = getUtility( |
202 | + IGitRepositoryChangeInformationTypeJobSource) |
203 | + new_jobs = list(job_util.iterReady()) |
204 | + self.assertEqual(jobs, new_jobs) |
205 | |
206 | def test_edit_view_ajax_render(self): |
207 | # An information type change request is processed as expected when |
208 | @@ -1208,8 +1266,16 @@ class TestGitRepositoryEditView(TestCaseWithFactory): |
209 | person, repository.target, repository, view] |
210 | result = view.render() |
211 | self.assertEqual("", result) |
212 | + self.assertTrue(repository.pending_change_information_type) |
213 | + job_util = getUtility( |
214 | + IGitRepositoryChangeInformationTypeJobSource) |
215 | + jobs = list(job_util.iterReady()) |
216 | + self.assertEqual(1, len(jobs)) |
217 | + job = removeSecurityProxy(jobs[0]) |
218 | + self.assertEqual(repository, job.repository) |
219 | self.assertEqual( |
220 | - repository.information_type, InformationType.PUBLICSECURITY) |
221 | + InformationType.PUBLICSECURITY, job.information_type) |
222 | + self.assertEqual(person, job.user) |
223 | |
224 | def test_change_default_branch(self): |
225 | # An authorised user can change the default branch to one that |
226 | diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml |
227 | index 898e645..3b03c7d 100644 |
228 | --- a/lib/lp/code/configure.zcml |
229 | +++ b/lib/lp/code/configure.zcml |
230 | @@ -1111,6 +1111,11 @@ |
231 | provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource"> |
232 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" /> |
233 | </securedutility> |
234 | + <securedutility |
235 | + component="lp.code.model.gitjob.GitRepositoryChangeInformationTypeJob" |
236 | + provides="lp.code.interfaces.gitjob.IGitRepositoryChangeInformationTypeJobSource"> |
237 | + <allow interface="lp.code.interfaces.gitjob.IGitRepositoryChangeInformationTypeJobSource" /> |
238 | + </securedutility> |
239 | <class class="lp.code.model.gitjob.GitRefScanJob"> |
240 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
241 | <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" /> |
242 | @@ -1123,6 +1128,10 @@ |
243 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
244 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" /> |
245 | </class> |
246 | + <class class="lp.code.model.gitjob.GitRepositoryChangeInformationTypeJob"> |
247 | + <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
248 | + <allow interface="lp.code.interfaces.gitjob.IGitRepositoryChangeInformationTypeJob" /> |
249 | + </class> |
250 | |
251 | <lp:help-folder folder="help" name="+help-code" /> |
252 | |
253 | diff --git a/lib/lp/code/interfaces/gitjob.py b/lib/lp/code/interfaces/gitjob.py |
254 | index 4f31b19..4bbca04 100644 |
255 | --- a/lib/lp/code/interfaces/gitjob.py |
256 | +++ b/lib/lp/code/interfaces/gitjob.py |
257 | @@ -1,4 +1,4 @@ |
258 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
259 | +# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
260 | # GNU Affero General Public License version 3 (see the file LICENSE). |
261 | |
262 | """GitJob interfaces.""" |
263 | @@ -11,6 +11,8 @@ __all__ = [ |
264 | 'IGitRefScanJobSource', |
265 | 'IGitRepositoryModifiedMailJob', |
266 | 'IGitRepositoryModifiedMailJobSource', |
267 | + 'IGitRepositoryChangeInformationTypeJob', |
268 | + 'IGitRepositoryChangeInformationTypeJobSource', |
269 | 'IReclaimGitRepositorySpaceJob', |
270 | 'IReclaimGitRepositorySpaceJobSource', |
271 | ] |
272 | @@ -93,3 +95,20 @@ class IGitRepositoryModifiedMailJobSource(IJobSource): |
273 | :param repository_delta: An `IGitRepositoryDelta` describing the |
274 | changes. |
275 | """ |
276 | + |
277 | + |
278 | +class IGitRepositoryChangeInformationTypeJob(IRunnableJob): |
279 | + """A Job to change repository's information type.""" |
280 | + |
281 | + |
282 | +class IGitRepositoryChangeInformationTypeJobSource(IJobSource): |
283 | + |
284 | + def create(repository, user, information_type, verify_policy=True): |
285 | + """Create a job to change git repository's information type. |
286 | + |
287 | + :param repository: The `IGitRepository` that was modified. |
288 | + :param information_type: The `InformationType` to transition to. |
289 | + :param user: The `IPerson` who is making the change. |
290 | + :param verify_policy: Check if the new information type complies |
291 | + with the `IGitNamespacePolicy`. |
292 | + """ |
293 | diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py |
294 | index 192133a..dac0e8f 100644 |
295 | --- a/lib/lp/code/interfaces/gitrepository.py |
296 | +++ b/lib/lp/code/interfaces/gitrepository.py |
297 | @@ -582,6 +582,10 @@ class IGitRepositoryView(IHasRecipes): |
298 | "Whether there are recent changes in this repository that have not " |
299 | "yet been scanned.") |
300 | |
301 | + pending_change_information_type = Attribute( |
302 | + "Whether there is a pending request to change the information type " |
303 | + "of this repository that have not been processed yet.") |
304 | + |
305 | def updateMergeCommitIDs(paths): |
306 | """Update commit SHA1s of merge proposals for this repository. |
307 | |
308 | diff --git a/lib/lp/code/model/gitjob.py b/lib/lp/code/model/gitjob.py |
309 | index 1eb87da..f1e3d2a 100644 |
310 | --- a/lib/lp/code/model/gitjob.py |
311 | +++ b/lib/lp/code/model/gitjob.py |
312 | @@ -33,10 +33,12 @@ from zope.interface import ( |
313 | provider, |
314 | ) |
315 | |
316 | +from lp.app.enums import InformationType |
317 | from lp.app.errors import NotFoundError |
318 | from lp.code.enums import ( |
319 | GitActivityType, |
320 | GitPermissionType, |
321 | + GitRepositoryStatus, |
322 | ) |
323 | from lp.code.interfaces.githosting import IGitHostingClient |
324 | from lp.code.interfaces.gitjob import ( |
325 | @@ -45,6 +47,8 @@ from lp.code.interfaces.gitjob import ( |
326 | IGitRefScanJobSource, |
327 | IGitRepositoryModifiedMailJob, |
328 | IGitRepositoryModifiedMailJobSource, |
329 | + IGitRepositoryChangeInformationTypeJob, |
330 | + IGitRepositoryChangeInformationTypeJobSource, |
331 | IReclaimGitRepositorySpaceJob, |
332 | IReclaimGitRepositorySpaceJobSource, |
333 | ) |
334 | @@ -100,6 +104,13 @@ class GitJobType(DBEnumeratedType): |
335 | modifications. |
336 | """) |
337 | |
338 | + REPOSITORY_TRANSITION_TO_INFO_TYPE = DBItem(3, """ |
339 | + Change repository's information type |
340 | + |
341 | + This job runs when a user requests to change privacy settings of a |
342 | + repository. |
343 | + """) |
344 | + |
345 | |
346 | @implementer(IGitJob) |
347 | class GitJob(StormBase): |
348 | @@ -393,3 +404,49 @@ class GitRepositoryModifiedMailJob(GitJobDerived): |
349 | def run(self): |
350 | """See `IGitRepositoryModifiedMailJob`.""" |
351 | self.getMailer().sendAll() |
352 | + |
353 | + |
354 | +@implementer(IGitRepositoryChangeInformationTypeJob) |
355 | +@provider(IGitRepositoryChangeInformationTypeJobSource) |
356 | +class GitRepositoryChangeInformationTypeJob(GitJobDerived): |
357 | + """A Job to change git repository's information type.""" |
358 | + |
359 | + class_job_type = GitJobType.REPOSITORY_TRANSITION_TO_INFO_TYPE |
360 | + |
361 | + config = config.IGitRepositoryChangeInformationTypeJobSource |
362 | + |
363 | + @classmethod |
364 | + def create(cls, repository, information_type, user, verify_policy=True): |
365 | + """See `IGitRepositoryChangeInformationTypeJobSource`.""" |
366 | + metadata = { |
367 | + "user": user.id, |
368 | + "information_type": information_type.value, |
369 | + "verify_policy": verify_policy, |
370 | + } |
371 | + git_job = GitJob(repository, cls.class_job_type, metadata) |
372 | + job = cls(git_job) |
373 | + job.celeryRunOnCommit() |
374 | + return job |
375 | + |
376 | + @property |
377 | + def user(self): |
378 | + return getUtility(IPersonSet).get(self.metadata["user"]) |
379 | + |
380 | + @property |
381 | + def verify_policy(self): |
382 | + return self.metadata["verify_policy"] |
383 | + |
384 | + @property |
385 | + def information_type(self): |
386 | + return InformationType.items[self.metadata["information_type"]] |
387 | + |
388 | + def run(self): |
389 | + """See `IGitRepositoryChangeInformationTypeJob`.""" |
390 | + if not self.repository.pending_change_information_type: |
391 | + raise AttributeError( |
392 | + "The repository %s is not pending information type change." % |
393 | + self.repository) |
394 | + |
395 | + self.repository._transitionToInformationType( |
396 | + self.information_type, self.user, self.verify_policy) |
397 | + self.repository.status = GitRepositoryStatus.AVAILABLE |
398 | diff --git a/lib/lp/code/model/gitref.py b/lib/lp/code/model/gitref.py |
399 | index f7dc142..e2f788d 100644 |
400 | --- a/lib/lp/code/model/gitref.py |
401 | +++ b/lib/lp/code/model/gitref.py |
402 | @@ -180,7 +180,7 @@ class GitRefMixin: |
403 | |
404 | def transitionToInformationType(self, information_type, user, |
405 | verify_policy=True): |
406 | - return self.repository.transitionToInformationType( |
407 | + return self.repository._transitionToInformationType( |
408 | information_type, user, verify_policy=verify_policy) |
409 | |
410 | @property |
411 | diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py |
412 | index fb5e703..d5bd382 100644 |
413 | --- a/lib/lp/code/model/gitrepository.py |
414 | +++ b/lib/lp/code/model/gitrepository.py |
415 | @@ -117,7 +117,10 @@ from lp.code.interfaces.gitcollection import ( |
416 | IGitCollection, |
417 | ) |
418 | from lp.code.interfaces.githosting import IGitHostingClient |
419 | -from lp.code.interfaces.gitjob import IGitRefScanJobSource |
420 | +from lp.code.interfaces.gitjob import ( |
421 | + IGitRefScanJobSource, |
422 | + IGitRepositoryChangeInformationTypeJobSource, |
423 | + ) |
424 | from lp.code.interfaces.gitlookup import IGitLookup |
425 | from lp.code.interfaces.gitnamespace import ( |
426 | get_git_namespace, |
427 | @@ -882,6 +885,28 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin): |
428 | def transitionToInformationType(self, information_type, user, |
429 | verify_policy=True): |
430 | """See `IGitRepository`.""" |
431 | + error_msg = None |
432 | + if self.status != GitRepositoryStatus.AVAILABLE: |
433 | + error_msg = ("Cannot change privacy settings while git " |
434 | + "repository is being created.") |
435 | + elif self.pending_change_information_type: |
436 | + error_msg = ("Cannot change privacy settings while git repository " |
437 | + "is pending another information type change.") |
438 | + if error_msg is not None: |
439 | + raise CannotChangeInformationType(error_msg) |
440 | + util = getUtility( |
441 | + IGitRepositoryChangeInformationTypeJobSource) |
442 | + return util.create(self, information_type, user, verify_policy) |
443 | + |
444 | + def _transitionToInformationType(self, information_type, user, |
445 | + verify_policy=True): |
446 | + """Synchronously make the change in this repository's information |
447 | + type. |
448 | + |
449 | + External callers should use the async, public version of this |
450 | + method, since it deals with the side effects of changing |
451 | + repository's privacy changes. |
452 | + """ |
453 | if self.information_type == information_type: |
454 | return |
455 | if (verify_policy and |
456 | @@ -1120,21 +1145,29 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin): |
457 | """See `IGitRepository`.""" |
458 | return self.namespace.areRepositoriesMergeable(self, other) |
459 | |
460 | - @property |
461 | - def pending_updates(self): |
462 | - """See `IGitRepository`.""" |
463 | - from lp.code.model.gitjob import ( |
464 | - GitJob, |
465 | - GitJobType, |
466 | - ) |
467 | + def hasPendingJob(self, job_type): |
468 | + from lp.code.model.gitjob import GitJob |
469 | jobs = Store.of(self).find( |
470 | GitJob, |
471 | GitJob.repository == self, |
472 | - GitJob.job_type == GitJobType.REF_SCAN, |
473 | + GitJob.job_type == job_type, |
474 | GitJob.job == Job.id, |
475 | Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING])) |
476 | return not jobs.is_empty() |
477 | |
478 | + @property |
479 | + def pending_updates(self): |
480 | + """See `IGitRepository`.""" |
481 | + from lp.code.model.gitjob import GitJobType |
482 | + return self.hasPendingJob(GitJobType.REF_SCAN) |
483 | + |
484 | + @property |
485 | + def pending_change_information_type(self): |
486 | + """See `IGitRepository`.""" |
487 | + from lp.code.model.gitjob import GitJobType |
488 | + return self.hasPendingJob( |
489 | + GitJobType.REPOSITORY_TRANSITION_TO_INFO_TYPE) |
490 | + |
491 | def updateMergeCommitIDs(self, paths): |
492 | """See `IGitRepository`.""" |
493 | store = Store.of(self) |
494 | diff --git a/lib/lp/code/model/tests/test_gitcollection.py b/lib/lp/code/model/tests/test_gitcollection.py |
495 | index 447fcb4..372a633 100644 |
496 | --- a/lib/lp/code/model/tests/test_gitcollection.py |
497 | +++ b/lib/lp/code/model/tests/test_gitcollection.py |
498 | @@ -599,7 +599,7 @@ class TestBranchMergeProposals(TestCaseWithFactory): |
499 | registrant = self.factory.makePerson() |
500 | mp1 = self.factory.makeBranchMergeProposalForGit(registrant=registrant) |
501 | naked_repository = removeSecurityProxy(mp1.target_git_repository) |
502 | - naked_repository.transitionToInformationType( |
503 | + naked_repository._transitionToInformationType( |
504 | InformationType.USERDATA, registrant, verify_policy=False) |
505 | collection = self.all_repositories.visibleByUser(None) |
506 | proposals = collection.getMergeProposals() |
507 | diff --git a/lib/lp/code/model/tests/test_gitjob.py b/lib/lp/code/model/tests/test_gitjob.py |
508 | index 3f606c0..b4487b9 100644 |
509 | --- a/lib/lp/code/model/tests/test_gitjob.py |
510 | +++ b/lib/lp/code/model/tests/test_gitjob.py |
511 | @@ -25,13 +25,16 @@ from testtools.matchers import ( |
512 | MatchesStructure, |
513 | ) |
514 | import transaction |
515 | +from zope.component import getUtility |
516 | from zope.interface import providedBy |
517 | from zope.security.proxy import removeSecurityProxy |
518 | |
519 | +from lp.app.enums import InformationType |
520 | from lp.code.adapters.gitrepository import GitRepositoryDelta |
521 | from lp.code.enums import ( |
522 | GitGranteeType, |
523 | GitObjectType, |
524 | + GitRepositoryStatus, |
525 | ) |
526 | from lp.code.interfaces.branchmergeproposal import ( |
527 | BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG, |
528 | @@ -39,6 +42,7 @@ from lp.code.interfaces.branchmergeproposal import ( |
529 | from lp.code.interfaces.gitjob import ( |
530 | IGitJob, |
531 | IGitRefScanJob, |
532 | + IGitRepositoryChangeInformationTypeJobSource, |
533 | IReclaimGitRepositorySpaceJob, |
534 | ) |
535 | from lp.code.model.gitjob import ( |
536 | @@ -47,9 +51,11 @@ from lp.code.model.gitjob import ( |
537 | GitJobDerived, |
538 | GitJobType, |
539 | GitRefScanJob, |
540 | + GitRepositoryChangeInformationTypeJob, |
541 | ReclaimGitRepositorySpaceJob, |
542 | ) |
543 | from lp.code.tests.helpers import GitHostingFixture |
544 | +from lp.registry.errors import CannotChangeInformationType |
545 | from lp.services.config import config |
546 | from lp.services.database.constants import UTC_NOW |
547 | from lp.services.features.testing import FeatureFixture |
548 | @@ -362,6 +368,59 @@ class TestReclaimGitRepositorySpaceJob(TestCaseWithFactory): |
549 | self.assertEqual([(path,)], hosting_fixture.delete.extract_args()) |
550 | |
551 | |
552 | +class TestGitRepositoryTransitionInformationType(TestCaseWithFactory): |
553 | + |
554 | + layer = ZopelessDatabaseLayer |
555 | + |
556 | + def test_block_multiple_requests_to_change_info_type(self): |
557 | + repo = self.factory.makeGitRepository() |
558 | + repo.transitionToInformationType( |
559 | + InformationType.PRIVATESECURITY, repo.owner) |
560 | + self.assertTrue(repo.pending_change_information_type) |
561 | + expected_msg = ( |
562 | + "Cannot change privacy settings while git repository is " |
563 | + "pending another information type change.") |
564 | + self.assertRaisesRegex( |
565 | + CannotChangeInformationType, expected_msg, |
566 | + repo.transitionToInformationType, |
567 | + InformationType.PROPRIETARY, repo.owner) |
568 | + |
569 | + def test_avoid_transitioning_while_creating(self): |
570 | + repo = self.factory.makeGitRepository() |
571 | + removeSecurityProxy(repo).status = GitRepositoryStatus.CREATING |
572 | + expected_msg = ( |
573 | + "Cannot change privacy settings while git repository is " |
574 | + "being created.") |
575 | + self.assertRaisesRegex( |
576 | + CannotChangeInformationType, expected_msg, |
577 | + repo.transitionToInformationType, |
578 | + InformationType.PROPRIETARY, repo.owner) |
579 | + |
580 | + def test_run_changes_info_type(self): |
581 | + repo = self.factory.makeGitRepository( |
582 | + information_type=InformationType.PUBLIC) |
583 | + # Change to a private info type and with verify_policy, so we hit as |
584 | + # many database tables as possible. |
585 | + repo.transitionToInformationType( |
586 | + InformationType.PRIVATESECURITY, repo.owner, verify_policy=True) |
587 | + self.assertTrue(repo.pending_change_information_type) |
588 | + |
589 | + job_util = getUtility( |
590 | + IGitRepositoryChangeInformationTypeJobSource) |
591 | + jobs = list(job_util.iterReady()) |
592 | + self.assertEqual(1, len(jobs)) |
593 | + with dbuser(GitRepositoryChangeInformationTypeJob.config.dbuser): |
594 | + JobRunner(jobs).runAll() |
595 | + |
596 | + self.assertEqual(GitRepositoryStatus.AVAILABLE, repo.status) |
597 | + self.assertEqual( |
598 | + InformationType.PRIVATESECURITY, repo.information_type) |
599 | + |
600 | + # After the job finished, another change is possible. |
601 | + repo.transitionToInformationType(InformationType.PUBLIC, repo.owner) |
602 | + self.assertTrue(repo.pending_change_information_type) |
603 | + |
604 | + |
605 | class TestDescribeRepositoryDelta(TestCaseWithFactory): |
606 | """Tests for `describe_repository_delta`.""" |
607 | |
608 | diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py |
609 | index b83f9da..f0ba2b4 100644 |
610 | --- a/lib/lp/code/model/tests/test_gitrepository.py |
611 | +++ b/lib/lp/code/model/tests/test_gitrepository.py |
612 | @@ -809,7 +809,7 @@ class TestGitRepositoryDeletion(TestCaseWithFactory): |
613 | |
614 | def test_private_subscription_does_not_disable_deletion(self): |
615 | # A private repository that has a subscription can be deleted. |
616 | - self.repository.transitionToInformationType( |
617 | + removeSecurityProxy(self.repository)._transitionToInformationType( |
618 | InformationType.USERDATA, self.repository.owner, |
619 | verify_policy=False) |
620 | self.repository.subscribe( |
621 | @@ -2054,8 +2054,7 @@ class TestGitRepositoryModerate(TestCaseWithFactory): |
622 | project.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC) |
623 | repository.transitionToInformationType( |
624 | InformationType.PRIVATESECURITY, project.owner) |
625 | - self.assertEqual( |
626 | - InformationType.PRIVATESECURITY, repository.information_type) |
627 | + self.assertTrue(repository.pending_change_information_type) |
628 | |
629 | def test_attribute_smoketest(self): |
630 | # Users with launchpad.Moderate can set attributes. |
631 | @@ -3454,7 +3453,7 @@ class TestGitRepositorySet(TestCaseWithFactory): |
632 | ] |
633 | for repository, modified_date in zip(repositories, modified_dates): |
634 | removeSecurityProxy(repository).date_last_modified = modified_date |
635 | - removeSecurityProxy(repositories[0]).transitionToInformationType( |
636 | + removeSecurityProxy(repositories[0])._transitionToInformationType( |
637 | InformationType.PRIVATESECURITY, repositories[0].registrant) |
638 | self.assertEqual( |
639 | [repositories[3], repositories[4], repositories[1], |
640 | @@ -3966,8 +3965,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory): |
641 | json.dumps({"information_type": "Public Security"})) |
642 | self.assertEqual(209, response.status) |
643 | with person_logged_in(ANONYMOUS): |
644 | - self.assertEqual( |
645 | - InformationType.PUBLICSECURITY, repository_db.information_type) |
646 | + self.assertTrue(repository_db.pending_change_information_type) |
647 | |
648 | def test_set_information_type_other_person(self): |
649 | # An unrelated user cannot change the information type. |
650 | diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py |
651 | index dcbcee6..e884cd2 100644 |
652 | --- a/lib/lp/code/xmlrpc/tests/test_git.py |
653 | +++ b/lib/lp/code/xmlrpc/tests/test_git.py |
654 | @@ -899,7 +899,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): |
655 | for _ in range(2)] |
656 | private_repository = code_imports[0].git_repository |
657 | removeSecurityProxy( |
658 | - private_repository).transitionToInformationType( |
659 | + private_repository)._transitionToInformationType( |
660 | InformationType.PRIVATESECURITY, private_repository.owner) |
661 | with celebrity_logged_in("vcs_imports"): |
662 | jobs = [ |
663 | @@ -1077,7 +1077,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): |
664 | for _ in range(2)] |
665 | private_repository = code_imports[0].git_repository |
666 | removeSecurityProxy( |
667 | - private_repository).transitionToInformationType( |
668 | + private_repository)._transitionToInformationType( |
669 | InformationType.PRIVATESECURITY, private_repository.owner) |
670 | with celebrity_logged_in("vcs_imports"): |
671 | jobs = [ |
672 | @@ -1687,7 +1687,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): |
673 | target_rcs_type=TargetRevisionControlSystems.GIT) |
674 | for _ in range(2)] |
675 | private_repository = code_imports[0].git_repository |
676 | - removeSecurityProxy(private_repository).transitionToInformationType( |
677 | + removeSecurityProxy(private_repository)._transitionToInformationType( |
678 | InformationType.PRIVATESECURITY, private_repository.owner) |
679 | with celebrity_logged_in("vcs_imports"): |
680 | jobs = [ |
681 | @@ -2012,7 +2012,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): |
682 | target_rcs_type=TargetRevisionControlSystems.GIT) |
683 | for _ in range(2)] |
684 | private_repository = code_imports[0].git_repository |
685 | - removeSecurityProxy(private_repository).transitionToInformationType( |
686 | + removeSecurityProxy(private_repository)._transitionToInformationType( |
687 | InformationType.PRIVATESECURITY, private_repository.owner) |
688 | with celebrity_logged_in("vcs_imports"): |
689 | jobs = [ |
690 | @@ -2268,7 +2268,7 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory): |
691 | target_rcs_type=TargetRevisionControlSystems.GIT) |
692 | for _ in range(2)] |
693 | private_repository = code_imports[0].git_repository |
694 | - removeSecurityProxy(private_repository).transitionToInformationType( |
695 | + removeSecurityProxy(private_repository)._transitionToInformationType( |
696 | InformationType.PRIVATESECURITY, private_repository.owner) |
697 | with celebrity_logged_in("vcs_imports"): |
698 | jobs = [ |
699 | diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf |
700 | index aaa9d30..1157cb5 100644 |
701 | --- a/lib/lp/services/config/schema-lazr.conf |
702 | +++ b/lib/lp/services/config/schema-lazr.conf |
703 | @@ -1768,6 +1768,7 @@ job_sources: |
704 | ICommercialExpiredJobSource, |
705 | IExpiringMembershipNotificationJobSource, |
706 | IGitRepositoryModifiedMailJobSource, |
707 | + IGitRepositoryChangeInformationTypeJobSource, |
708 | IMembershipNotificationJobSource, |
709 | IOCIRecipeRequestBuildsJobSource, |
710 | IOCIRegistryUploadJobSource, |
711 | @@ -1829,6 +1830,11 @@ module: lp.code.interfaces.gitjob |
712 | dbuser: send-branch-mail |
713 | crontab_group: MAIN |
714 | |
715 | +[IGitRepositoryChangeInformationTypeJobSource] |
716 | +module: lp.code.interfaces.gitjob |
717 | +dbuser: privacy-change-jobs |
718 | +crontab_group: MAIN |
719 | + |
720 | [IInitializeDistroSeriesJobSource] |
721 | module: lp.soyuz.interfaces.distributionjob |
722 | dbuser: initializedistroseries |
723 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
724 | index 57babc5..9d1f53b 100644 |
725 | --- a/lib/lp/testing/factory.py |
726 | +++ b/lib/lp/testing/factory.py |
727 | @@ -1815,7 +1815,7 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
728 | reviewer=reviewer, **optional_repository_args) |
729 | naked_repository = removeSecurityProxy(repository) |
730 | if information_type is not None: |
731 | - naked_repository.transitionToInformationType( |
732 | + naked_repository._transitionToInformationType( |
733 | information_type, registrant, verify_policy=False) |
734 | return repository |
735 |
This MP is already quite big, so I'll create in another MP the garbo job that will deal with GitRepositories that are PENDING_ INFORMATION_ TYPE_TRANSITION for too long (to avoid blocking them forever).