Merge ~pappacena/launchpad:official-oci-recipe into launchpad:master
- Git
- lp:~pappacena/launchpad
- official-oci-recipe
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Thiago F. Pappacena |
Approved revision: | 2e682218c21de8331ecfde6b8ffc3805d05eff5d |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:official-oci-recipe |
Merge into: | launchpad:master |
Diff against target: |
603 lines (+368/-18) 9 files modified
lib/lp/oci/interfaces/ocirecipe.py (+7/-7) lib/lp/oci/model/ocirecipe.py (+9/-2) lib/lp/oci/tests/test_ocirecipe.py (+153/-1) lib/lp/oci/vocabularies.py (+33/-1) lib/lp/oci/vocabularies.zcml (+11/-0) lib/lp/registry/browser/ociproject.py (+13/-0) lib/lp/registry/browser/tests/test_ociproject.py (+78/-7) lib/lp/registry/interfaces/ociproject.py (+16/-0) lib/lp/registry/model/ociproject.py (+48/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+383542@code.launchpad.net |
Commit message
Adding picker to select, on OCI project edit page, which recipe is the official one.
Description of the change
To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) wrote : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Needs Fixing
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
It should be good now for another round of review, cjwatson.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
I've made the requested changes, but we cannot remove IOCIRecipe.
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Thiago F. Pappacena (pappacena) : | # |
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote : | # |
- 2e68221... by Thiago F. Pappacena
-
Merge branch 'master' into official-oci-recipe
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py |
2 | index 3163f0f..4b2b06d 100644 |
3 | --- a/lib/lp/oci/interfaces/ocirecipe.py |
4 | +++ b/lib/lp/oci/interfaces/ocirecipe.py |
5 | @@ -200,6 +200,13 @@ class IOCIRecipeView(Interface): |
6 | "The architectures that are available to be enabled or disabled for " |
7 | "this OCI recipe.") |
8 | |
9 | + # This should only be set by using IOCIProject.setOfficialRecipe |
10 | + official = Bool( |
11 | + title=_("OCI project official"), |
12 | + required=False, |
13 | + description=_("True if this recipe is official for its OCI project."), |
14 | + readonly=True) |
15 | + |
16 | @call_with(check_permissions=True, user=REQUEST_USER) |
17 | @operation_parameters( |
18 | processors=List( |
19 | @@ -340,13 +347,6 @@ class IOCIRecipeEditableAttributes(IHasOwner): |
20 | required=True, |
21 | readonly=True)) |
22 | |
23 | - official = Bool( |
24 | - title=_("OCI project official"), |
25 | - required=True, |
26 | - default=False, |
27 | - description=_("True if this recipe is official for its OCI project."), |
28 | - readonly=False) |
29 | - |
30 | git_ref = exported(Reference( |
31 | IGitRef, title=_("Git branch"), required=True, readonly=False, |
32 | description=_( |
33 | diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py |
34 | index f894e7a..8562778 100644 |
35 | --- a/lib/lp/oci/model/ocirecipe.py |
36 | +++ b/lib/lp/oci/model/ocirecipe.py |
37 | @@ -130,7 +130,9 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
38 | name = Unicode(name="name", allow_none=False) |
39 | description = Unicode(name="description", allow_none=True) |
40 | |
41 | - official = Bool(name="official", default=False) |
42 | + # OCIRecipe.official shouldn't be set directly. Instead, call |
43 | + # oci_project.setOfficialRecipe method. |
44 | + _official = Bool(name="official", default=False) |
45 | |
46 | git_repository_id = Int(name="git_repository", allow_none=True) |
47 | git_repository = Reference(git_repository_id, "GitRepository.id") |
48 | @@ -154,7 +156,7 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
49 | self.oci_project = oci_project |
50 | self.description = description |
51 | self.build_file = build_file |
52 | - self.official = official |
53 | + self._official = official |
54 | self.require_virtualized = require_virtualized |
55 | self.build_daily = build_daily |
56 | self.date_created = date_created |
57 | @@ -165,6 +167,11 @@ class OCIRecipe(Storm, WebhookTargetMixin): |
58 | def valid_webhook_event_types(self): |
59 | return ["oci-recipe:build:0.1"] |
60 | |
61 | + @property |
62 | + def official(self): |
63 | + """See `IOCIProject.setOfficialRecipe` method.""" |
64 | + return self._official |
65 | + |
66 | def destroySelf(self): |
67 | """See `IOCIRecipe`.""" |
68 | # XXX twom 2019-11-26 This needs to expand as more build artifacts |
69 | diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py |
70 | index fac8638..74d15e0 100644 |
71 | --- a/lib/lp/oci/tests/test_ocirecipe.py |
72 | +++ b/lib/lp/oci/tests/test_ocirecipe.py |
73 | @@ -24,6 +24,10 @@ from testtools.matchers import ( |
74 | ) |
75 | import transaction |
76 | from zope.component import getUtility |
77 | +from zope.security.interfaces import ( |
78 | + ForbiddenAttribute, |
79 | + Unauthorized, |
80 | + ) |
81 | from zope.security.proxy import removeSecurityProxy |
82 | |
83 | from lp.buildmaster.enums import BuildStatus |
84 | @@ -61,7 +65,10 @@ from lp.services.webhooks.testing import LogsScheduledWebhooks |
85 | from lp.testing import ( |
86 | admin_logged_in, |
87 | api_url, |
88 | + login_admin, |
89 | + login_person, |
90 | person_logged_in, |
91 | + StormStatementRecorder, |
92 | TestCaseWithFactory, |
93 | ) |
94 | from lp.testing.dbuser import dbuser |
95 | @@ -441,6 +448,151 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory): |
96 | recipe.newPushRule, |
97 | recipe.owner, url, image_name, credentials) |
98 | |
99 | + def test_set_official_directly_is_forbidden(self): |
100 | + recipe = self.factory.makeOCIRecipe() |
101 | + self.assertRaises( |
102 | + ForbiddenAttribute, setattr, recipe, 'official', True) |
103 | + |
104 | + def test_set_recipe_as_official_for_oci_project(self): |
105 | + distro = self.factory.makeDistribution() |
106 | + owner = distro.owner |
107 | + login_person(owner) |
108 | + oci_project1 = self.factory.makeOCIProject( |
109 | + registrant=owner, pillar=distro) |
110 | + oci_project2 = self.factory.makeOCIProject( |
111 | + registrant=owner, pillar=distro) |
112 | + |
113 | + oci_proj1_recipes = [ |
114 | + self.factory.makeOCIRecipe( |
115 | + oci_project=oci_project1, registrant=owner, owner=owner) |
116 | + for _ in range(3)] |
117 | + |
118 | + # Recipes for project 2 |
119 | + oci_proj2_recipes = [ |
120 | + self.factory.makeOCIRecipe( |
121 | + oci_project=oci_project2, registrant=owner, owner=owner) |
122 | + for _ in range(2)] |
123 | + |
124 | + self.assertIsNone(oci_project1.getOfficialRecipe()) |
125 | + self.assertIsNone(oci_project2.getOfficialRecipe()) |
126 | + for recipe in oci_proj1_recipes + oci_proj2_recipes: |
127 | + self.assertFalse(recipe.official) |
128 | + |
129 | + # Set official for project1 and make sure nothing else got changed. |
130 | + with StormStatementRecorder() as recorder: |
131 | + oci_project1.setOfficialRecipe(oci_proj1_recipes[0]) |
132 | + self.assertEqual(2, recorder.count) |
133 | + |
134 | + self.assertIsNone(oci_project2.getOfficialRecipe()) |
135 | + self.assertEqual( |
136 | + oci_proj1_recipes[0], oci_project1.getOfficialRecipe()) |
137 | + self.assertTrue(oci_proj1_recipes[0].official) |
138 | + for recipe in oci_proj1_recipes[1:] + oci_proj2_recipes: |
139 | + self.assertFalse(recipe.official) |
140 | + |
141 | + # Set back no recipe as official. |
142 | + with StormStatementRecorder() as recorder: |
143 | + oci_project1.setOfficialRecipe(None) |
144 | + self.assertEqual(1, recorder.count) |
145 | + |
146 | + for recipe in oci_proj1_recipes + oci_proj2_recipes: |
147 | + self.assertFalse(recipe.official) |
148 | + |
149 | + def test_set_recipe_as_official_for_wrong_oci_project(self): |
150 | + distro = self.factory.makeDistribution() |
151 | + owner = distro.owner |
152 | + login_person(owner) |
153 | + oci_project = self.factory.makeOCIProject( |
154 | + registrant=owner, pillar=distro) |
155 | + another_oci_project = self.factory.makeOCIProject( |
156 | + registrant=owner, pillar=distro) |
157 | + |
158 | + recipe = self.factory.makeOCIRecipe( |
159 | + oci_project=oci_project, registrant=owner) |
160 | + |
161 | + self.assertRaises( |
162 | + ValueError, another_oci_project.setOfficialRecipe, recipe) |
163 | + |
164 | + def test_permission_check_on_setOfficialRecipe(self): |
165 | + distro = self.factory.makeDistribution() |
166 | + owner = distro.owner |
167 | + login_person(owner) |
168 | + oci_project = self.factory.makeOCIProject( |
169 | + registrant=owner, pillar=distro) |
170 | + |
171 | + another_user = self.factory.makePerson() |
172 | + with person_logged_in(another_user): |
173 | + self.assertRaises( |
174 | + Unauthorized, getattr, oci_project, 'setOfficialRecipe') |
175 | + |
176 | + def test_oci_project_get_recipe_by_name_and_owner(self): |
177 | + owner = self.factory.makePerson() |
178 | + login_person(owner) |
179 | + oci_project = self.factory.makeOCIProject(registrant=owner) |
180 | + |
181 | + recipe = self.factory.makeOCIRecipe( |
182 | + oci_project=oci_project, registrant=owner, owner=owner, |
183 | + name="foo-recipe") |
184 | + |
185 | + self.assertEqual( |
186 | + recipe, |
187 | + oci_project.getRecipeByNameAndOwner(recipe.name, owner.name)) |
188 | + self.assertIsNone( |
189 | + oci_project.getRecipeByNameAndOwner(recipe.name, "someone")) |
190 | + self.assertIsNone( |
191 | + oci_project.getRecipeByNameAndOwner("some-recipe", owner.name)) |
192 | + |
193 | + def test_search_recipe_from_oci_project(self): |
194 | + owner = self.factory.makePerson() |
195 | + login_person(owner) |
196 | + oci_project = self.factory.makeOCIProject(registrant=owner) |
197 | + another_oci_project = self.factory.makeOCIProject(registrant=owner) |
198 | + |
199 | + recipe1 = self.factory.makeOCIRecipe( |
200 | + name="a something", oci_project=oci_project, registrant=owner) |
201 | + recipe2 = self.factory.makeOCIRecipe( |
202 | + name="banana", oci_project=oci_project, registrant=owner) |
203 | + # Recipe from another project. |
204 | + self.factory.makeOCIRecipe( |
205 | + name="something too", oci_project=another_oci_project, |
206 | + registrant=owner) |
207 | + |
208 | + self.assertEqual([recipe1], list(oci_project.searchRecipes("somet"))) |
209 | + self.assertEqual([recipe2], list(oci_project.searchRecipes("bana"))) |
210 | + self.assertEqual([], list(oci_project.searchRecipes("foo"))) |
211 | + |
212 | + def test_search_recipe_from_oci_project_is_ordered(self): |
213 | + login_admin() |
214 | + team = self.factory.makeTeam() |
215 | + owner1 = self.factory.makePerson(name="a-user") |
216 | + owner2 = self.factory.makePerson(name="b-user") |
217 | + owner3 = self.factory.makePerson(name="foo-person") |
218 | + team.addMember(owner1, team) |
219 | + team.addMember(owner2, team) |
220 | + team.addMember(owner3, team) |
221 | + |
222 | + distro = self.factory.makeDistribution(oci_project_admin=team) |
223 | + oci_project = self.factory.makeOCIProject( |
224 | + registrant=team, pillar=distro) |
225 | + recipe1 = self.factory.makeOCIRecipe( |
226 | + name="same-name", oci_project=oci_project, |
227 | + registrant=owner1, owner=owner1) |
228 | + recipe2 = self.factory.makeOCIRecipe( |
229 | + name="same-name", oci_project=oci_project, |
230 | + registrant=owner2, owner=owner2) |
231 | + recipe3 = self.factory.makeOCIRecipe( |
232 | + name="a-first", oci_project=oci_project, |
233 | + registrant=owner1, owner=owner1) |
234 | + # This one should be filtered out. |
235 | + self.factory.makeOCIRecipe( |
236 | + name="xxx", oci_project=oci_project, |
237 | + registrant=owner3, owner=owner3) |
238 | + |
239 | + # It should be sorted by owner's name first, then recipe name. |
240 | + self.assertEqual( |
241 | + [recipe3, recipe1, recipe2], |
242 | + list(oci_project.searchRecipes(u"a"))) |
243 | + |
244 | |
245 | class TestOCIRecipeProcessors(TestCaseWithFactory): |
246 | |
247 | @@ -955,7 +1107,7 @@ class TestOCIRecipeWebservice(OCIConfigHelperMixin, TestCaseWithFactory): |
248 | recipe = self.factory.makeOCIRecipe( |
249 | oci_project=oci_project, owner=self.person, |
250 | registrant=self.person) |
251 | - push_rule = self.factory.makeOCIPushRule( |
252 | + self.factory.makeOCIPushRule( |
253 | recipe=recipe, image_name=image_name) |
254 | url = api_url(recipe) |
255 | |
256 | diff --git a/lib/lp/oci/vocabularies.py b/lib/lp/oci/vocabularies.py |
257 | index 2e44624..aa1fae3 100644 |
258 | --- a/lib/lp/oci/vocabularies.py |
259 | +++ b/lib/lp/oci/vocabularies.py |
260 | @@ -8,9 +8,14 @@ from __future__ import absolute_import, print_function, unicode_literals |
261 | __metaclass__ = type |
262 | __all__ = [] |
263 | |
264 | +from zope.interface import implementer |
265 | from zope.schema.vocabulary import SimpleTerm |
266 | |
267 | -from lp.services.webapp.vocabulary import StormVocabularyBase |
268 | +from lp.oci.model.ocirecipe import OCIRecipe |
269 | +from lp.services.webapp.vocabulary import ( |
270 | + IHugeVocabulary, |
271 | + StormVocabularyBase, |
272 | + ) |
273 | from lp.soyuz.model.distroarchseries import DistroArchSeries |
274 | |
275 | |
276 | @@ -28,3 +33,30 @@ class OCIRecipeDistroArchSeriesVocabulary(StormVocabularyBase): |
277 | |
278 | def __len__(self): |
279 | return len(self.context.getAllowedArchitectures()) |
280 | + |
281 | + |
282 | +@implementer(IHugeVocabulary) |
283 | +class OCIRecipeVocabulary(StormVocabularyBase): |
284 | + """All OCI Recipes of a given OCI project.""" |
285 | + |
286 | + _table = OCIRecipe |
287 | + displayname = 'Select a recipe' |
288 | + step_title = 'Search' |
289 | + |
290 | + def toTerm(self, recipe): |
291 | + token = "%s/%s" % (recipe.owner.name, recipe.name) |
292 | + title = "~%s" % token |
293 | + return SimpleTerm(recipe, token, title) |
294 | + |
295 | + def getTermByToken(self, token): |
296 | + owner_name, recipe_name = token.split('/') |
297 | + recipe = self.context.getRecipeByNameAndOwner(recipe_name, owner_name) |
298 | + if recipe is None: |
299 | + raise LookupError(token) |
300 | + return self.toTerm(recipe) |
301 | + |
302 | + def search(self, query, vocab_filter=None): |
303 | + return self.context.searchRecipes(query) |
304 | + |
305 | + def _entries(self): |
306 | + return self.context.getRecipes() |
307 | diff --git a/lib/lp/oci/vocabularies.zcml b/lib/lp/oci/vocabularies.zcml |
308 | index fae4a6d..1a6b75c 100644 |
309 | --- a/lib/lp/oci/vocabularies.zcml |
310 | +++ b/lib/lp/oci/vocabularies.zcml |
311 | @@ -15,4 +15,15 @@ |
312 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> |
313 | </class> |
314 | |
315 | + <securedutility |
316 | + name="OCIRecipe" |
317 | + component="lp.oci.vocabularies.OCIRecipeVocabulary" |
318 | + provides="zope.schema.interfaces.IVocabularyFactory"> |
319 | + <allow interface="zope.schema.interfaces.IVocabularyFactory" /> |
320 | + </securedutility> |
321 | + |
322 | + <class class="lp.oci.vocabularies.OCIRecipeVocabulary"> |
323 | + <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> |
324 | + </class> |
325 | + |
326 | </configure> |
327 | diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py |
328 | index 700af68..eedec0c 100644 |
329 | --- a/lib/lp/registry/browser/ociproject.py |
330 | +++ b/lib/lp/registry/browser/ociproject.py |
331 | @@ -15,7 +15,9 @@ __all__ = [ |
332 | ] |
333 | |
334 | from zope.component import getUtility |
335 | +from zope.formlib import form |
336 | from zope.interface import implementer |
337 | +from zope.schema import Choice |
338 | |
339 | from lp.app.browser.launchpadform import ( |
340 | action, |
341 | @@ -170,8 +172,17 @@ class OCIProjectEditView(LaunchpadEditFormView): |
342 | field_names = [ |
343 | 'distribution', |
344 | 'name', |
345 | + 'official_recipe', |
346 | ] |
347 | |
348 | + def extendFields(self): |
349 | + official_recipe = self.context.getOfficialRecipe() |
350 | + self.form_fields += form.Fields( |
351 | + Choice( |
352 | + __name__="official_recipe", title=u"Official recipe", |
353 | + required=False, vocabulary="OCIRecipe", |
354 | + default=official_recipe)) |
355 | + |
356 | @property |
357 | def label(self): |
358 | return 'Edit %s OCI project' % self.context.name |
359 | @@ -193,7 +204,9 @@ class OCIProjectEditView(LaunchpadEditFormView): |
360 | |
361 | @action('Update OCI project', name='update') |
362 | def update_action(self, action, data): |
363 | + official_recipe = data.pop("official_recipe") |
364 | self.updateContextFromData(data) |
365 | + self.context.setOfficialRecipe(official_recipe) |
366 | |
367 | @property |
368 | def next_url(self): |
369 | diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py |
370 | index 5d08e1c..5a22137 100644 |
371 | --- a/lib/lp/registry/browser/tests/test_ociproject.py |
372 | +++ b/lib/lp/registry/browser/tests/test_ociproject.py |
373 | @@ -13,6 +13,7 @@ from datetime import datetime |
374 | |
375 | import pytz |
376 | |
377 | +from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE |
378 | from lp.registry.interfaces.ociproject import ( |
379 | OCI_PROJECT_ALLOW_CREATE, |
380 | OCIProjectCreateFeatureDisabled, |
381 | @@ -22,6 +23,7 @@ from lp.services.features.testing import FeatureFixture |
382 | from lp.services.webapp import canonical_url |
383 | from lp.services.webapp.escaping import structured |
384 | from lp.testing import ( |
385 | + admin_logged_in, |
386 | BrowserTestCase, |
387 | person_logged_in, |
388 | test_tales, |
389 | @@ -94,6 +96,13 @@ class TestOCIProjectEditView(BrowserTestCase): |
390 | |
391 | layer = DatabaseFunctionalLayer |
392 | |
393 | + def submitEditForm(self, browser, name, official_recipe=''): |
394 | + browser.getLink("Edit OCI project").click() |
395 | + browser.getControl(name="field.name").value = name |
396 | + browser.getControl(name="field.official_recipe").value = ( |
397 | + official_recipe) |
398 | + browser.getControl("Update OCI project").click() |
399 | + |
400 | def test_edit_oci_project(self): |
401 | oci_project = self.factory.makeOCIProject() |
402 | new_distribution = self.factory.makeDistribution( |
403 | @@ -127,7 +136,8 @@ class TestOCIProjectEditView(BrowserTestCase): |
404 | with person_logged_in(oci_project.pillar.owner): |
405 | view = create_initialized_view( |
406 | oci_project, name="+edit", principal=oci_project.pillar.owner) |
407 | - view.update_action.success({"name": "changed"}) |
408 | + view.update_action.success( |
409 | + {"name": "changed", "official_recipe": None}) |
410 | self.assertSqlAttributeEqualsDate( |
411 | oci_project, "date_last_modified", UTC_NOW) |
412 | |
413 | @@ -138,9 +148,7 @@ class TestOCIProjectEditView(BrowserTestCase): |
414 | pillar_display_name = oci_project.pillar.display_name |
415 | browser = self.getViewBrowser( |
416 | oci_project, user=oci_project.pillar.owner) |
417 | - browser.getLink("Edit OCI project").click() |
418 | - browser.getControl(name="field.name").value = "two" |
419 | - browser.getControl("Update OCI project").click() |
420 | + self.submitEditForm(browser, "two") |
421 | self.assertEqual( |
422 | "There is already an OCI project in %s with this name." % ( |
423 | pillar_display_name), |
424 | @@ -150,13 +158,76 @@ class TestOCIProjectEditView(BrowserTestCase): |
425 | oci_project = self.factory.makeOCIProject() |
426 | browser = self.getViewBrowser( |
427 | oci_project, user=oci_project.pillar.owner) |
428 | - browser.getLink("Edit OCI project").click() |
429 | - browser.getControl(name="field.name").value = "invalid name" |
430 | - browser.getControl("Update OCI project").click() |
431 | + self.submitEditForm(browser, "invalid name") |
432 | + |
433 | self.assertStartsWith( |
434 | extract_text(find_tags_by_class(browser.contents, "message")[1]), |
435 | "Invalid name 'invalid name'.") |
436 | |
437 | + def test_edit_oci_project_setting_official_recipe(self): |
438 | + self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'})) |
439 | + |
440 | + with admin_logged_in(): |
441 | + oci_project = self.factory.makeOCIProject() |
442 | + user = oci_project.pillar.owner |
443 | + recipe1 = self.factory.makeOCIRecipe( |
444 | + registrant=user, owner=user, oci_project=oci_project) |
445 | + recipe2 = self.factory.makeOCIRecipe( |
446 | + registrant=user, owner=user, oci_project=oci_project) |
447 | + |
448 | + name_value = oci_project.name |
449 | + recipe_value = "%s/%s" % (user.name, recipe1.name) |
450 | + |
451 | + browser = self.getViewBrowser(oci_project, user=user) |
452 | + self.submitEditForm(browser, name_value, recipe_value) |
453 | + |
454 | + with admin_logged_in(): |
455 | + self.assertEqual(recipe1, oci_project.getOfficialRecipe()) |
456 | + self.assertTrue(recipe1.official) |
457 | + self.assertFalse(recipe2.official) |
458 | + |
459 | + def test_edit_oci_project_overriding_official_recipe(self): |
460 | + self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'})) |
461 | + with admin_logged_in(): |
462 | + oci_project = self.factory.makeOCIProject() |
463 | + user = oci_project.pillar.owner |
464 | + recipe1 = self.factory.makeOCIRecipe( |
465 | + registrant=user, owner=user, oci_project=oci_project) |
466 | + recipe2 = self.factory.makeOCIRecipe( |
467 | + registrant=user, owner=user, oci_project=oci_project) |
468 | + |
469 | + # Sets recipe1 as the current official one |
470 | + oci_project.setOfficialRecipe(recipe1) |
471 | + |
472 | + # And we will try to set recipe2 as the new official. |
473 | + name_value = oci_project.name |
474 | + recipe_value = "%s/%s" % (user.name, recipe2.name) |
475 | + |
476 | + browser = self.getViewBrowser(oci_project, user=user) |
477 | + self.submitEditForm(browser, name_value, recipe_value) |
478 | + |
479 | + with admin_logged_in(): |
480 | + self.assertEqual(recipe2, oci_project.getOfficialRecipe()) |
481 | + self.assertFalse(recipe1.official) |
482 | + self.assertTrue(recipe2.official) |
483 | + |
484 | + def test_edit_oci_project_unsetting_official_recipe(self): |
485 | + self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'})) |
486 | + with admin_logged_in(): |
487 | + oci_project = self.factory.makeOCIProject() |
488 | + user = oci_project.pillar.owner |
489 | + recipe = self.factory.makeOCIRecipe( |
490 | + registrant=user, owner=user, oci_project=oci_project) |
491 | + oci_project.setOfficialRecipe(recipe) |
492 | + name_value = oci_project.name |
493 | + |
494 | + browser = self.getViewBrowser(oci_project, user=user) |
495 | + self.submitEditForm(browser, name_value, '') |
496 | + |
497 | + with admin_logged_in(): |
498 | + self.assertEqual(None, oci_project.getOfficialRecipe()) |
499 | + self.assertFalse(recipe.official) |
500 | + |
501 | |
502 | class TestOCIProjectAddView(BrowserTestCase): |
503 | |
504 | diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py |
505 | index 6d0b9bc..8e66f5d 100644 |
506 | --- a/lib/lp/registry/interfaces/ociproject.py |
507 | +++ b/lib/lp/registry/interfaces/ociproject.py |
508 | @@ -85,6 +85,18 @@ class IOCIProjectView(IHasGitRepositories, Interface): |
509 | def getSeriesByName(name): |
510 | """Get an OCIProjectSeries for this OCIProject by series' name.""" |
511 | |
512 | + def getRecipeByNameAndOwner(recipe_name, owner_name): |
513 | + """Returns the exact match search for recipe_name AND owner_name.""" |
514 | + |
515 | + def getRecipes(): |
516 | + """Returns the set of OCI recipes for this project.""" |
517 | + |
518 | + def searchRecipes(query): |
519 | + """Searches for recipes in this OCI project.""" |
520 | + |
521 | + def getOfficialRecipe(): |
522 | + """Gets the official recipe for this OCI project.""" |
523 | + |
524 | |
525 | class IOCIProjectEditableAttributes(IBugTarget): |
526 | """IOCIProject attributes that can be edited. |
527 | @@ -120,6 +132,10 @@ class IOCIProjectEdit(Interface): |
528 | status=SeriesStatus.DEVELOPMENT, date_created=DEFAULT): |
529 | """Creates a new `IOCIProjectSeries`.""" |
530 | |
531 | + def setOfficialRecipe(recipe): |
532 | + """Sets the given recipe as the official one. If recipe is None, |
533 | + the current official recipe will be unset.""" |
534 | + |
535 | |
536 | class IOCIProjectLegitimate(Interface): |
537 | """IOCIProject methods that require launchpad.AnyLegitimatePerson |
538 | diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py |
539 | index 230f32a..0d94e38 100644 |
540 | --- a/lib/lp/registry/model/ociproject.py |
541 | +++ b/lib/lp/registry/model/ociproject.py |
542 | @@ -36,6 +36,7 @@ from lp.registry.interfaces.person import IPersonSet |
543 | from lp.registry.interfaces.series import SeriesStatus |
544 | from lp.registry.model.ociprojectname import OCIProjectName |
545 | from lp.registry.model.ociprojectseries import OCIProjectSeries |
546 | +from lp.registry.model.person import Person |
547 | from lp.services.database.bulk import load_related |
548 | from lp.services.database.constants import ( |
549 | DEFAULT, |
550 | @@ -149,6 +150,53 @@ class OCIProject(BugTargetBase, StormBase): |
551 | def getSeriesByName(self, name): |
552 | return self.series.find(OCIProjectSeries.name == name).one() |
553 | |
554 | + def getRecipes(self): |
555 | + """See `IOCIProject`.""" |
556 | + from lp.oci.model.ocirecipe import OCIRecipe |
557 | + rs = IStore(OCIRecipe).find( |
558 | + OCIRecipe, |
559 | + OCIRecipe.owner_id == Person.id, |
560 | + OCIRecipe.oci_project == self) |
561 | + return rs.order_by(Person.name, OCIRecipe.name) |
562 | + |
563 | + def getRecipeByNameAndOwner(self, recipe_name, owner_name): |
564 | + """See `IOCIProject`.""" |
565 | + from lp.oci.model.ocirecipe import OCIRecipe |
566 | + q = self.getRecipes().find( |
567 | + OCIRecipe.name == recipe_name, |
568 | + Person.name == owner_name) |
569 | + return q.one() |
570 | + |
571 | + def searchRecipes(self, query): |
572 | + """See `IOCIProject`.""" |
573 | + from lp.oci.model.ocirecipe import OCIRecipe |
574 | + q = self.getRecipes().find( |
575 | + OCIRecipe.name.contains_string(query) | |
576 | + Person.name.contains_string(query)) |
577 | + return q.order_by(Person.name, OCIRecipe.name) |
578 | + |
579 | + def getOfficialRecipe(self): |
580 | + """See `IOCIProject`.""" |
581 | + from lp.oci.model.ocirecipe import OCIRecipe |
582 | + return self.getRecipes().find(OCIRecipe._official == True).one() |
583 | + |
584 | + def setOfficialRecipe(self, recipe): |
585 | + """See `IOCIProject`.""" |
586 | + if recipe is not None and recipe.oci_project != self: |
587 | + raise ValueError( |
588 | + "An OCI recipe cannot be set as the official recipe of " |
589 | + "another OCI project.") |
590 | + # Removing security proxy here because `_official` is a private |
591 | + # attribute not declared on the Interface, and we need to set it |
592 | + # regardless of security checks on OCIRecipe objects. |
593 | + recipe = removeSecurityProxy(recipe) |
594 | + previous = removeSecurityProxy(self.getOfficialRecipe()) |
595 | + if previous != recipe: |
596 | + if previous is not None: |
597 | + previous._official = False |
598 | + if recipe is not None: |
599 | + recipe._official = True |
600 | + |
601 | |
602 | @implementer(IOCIProjectSet) |
603 | class OCIProjectSet: |
This looks to me like it has all the right elements but I would wait for a second review.