Merge ~pappacena/launchpad:ui-create-ociproject-for-projects into launchpad:master
- Git
- lp:~pappacena/launchpad
- ui-create-ociproject-for-projects
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Thiago F. Pappacena |
Approved revision: | c78a3c2b0236d70d760598effc20aa254e10b14b |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:ui-create-ociproject-for-projects |
Merge into: | launchpad:master |
Prerequisite: | ~pappacena/launchpad:gitrepo-ociproject-refactoring |
Diff against target: |
426 lines (+172/-26) 9 files modified
lib/lp/code/browser/tests/test_product.py (+14/-0) lib/lp/registry/browser/configure.zcml (+7/-0) lib/lp/registry/browser/ociproject.py (+31/-10) lib/lp/registry/browser/product.py (+12/-1) lib/lp/registry/browser/tests/test_ociproject.py (+72/-8) lib/lp/registry/interfaces/product.py (+9/-1) lib/lp/registry/model/product.py (+19/-1) lib/lp/registry/templates/ociproject-index.pt (+7/-4) lib/lp/security.py (+1/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Tom Wardill (community) | Approve | ||
Review via email: mp+384506@code.launchpad.net |
Commit message
UI to allow creating and editing OCI projects based on projects.
Description of the change
To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) wrote : | # |
In Unit tests I'd prob go for soupmatchers , but other than that LGTM!
review:
Approve
- 8290700... by Thiago F. Pappacena
-
Displaying pillar type on error message
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
Pushed the requested change. Merging this MP now.
- c78a3c2... by Thiago F. Pappacena
-
Merge branch 'master' into ui-create-
ociproject- for-projects
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/code/browser/tests/test_product.py b/lib/lp/code/browser/tests/test_product.py |
2 | index 360745e..d33a85e 100644 |
3 | --- a/lib/lp/code/browser/tests/test_product.py |
4 | +++ b/lib/lp/code/browser/tests/test_product.py |
5 | @@ -426,3 +426,17 @@ class TestCanConfigureBranches(TestCaseWithFactory): |
6 | login_person(product.owner) |
7 | view = create_view(product, '+branches') |
8 | self.assertTrue(view.can_configure_branches()) |
9 | + |
10 | + |
11 | +class TestProductOverviewOCIProject(TestCaseWithFactory): |
12 | + |
13 | + layer = DatabaseFunctionalLayer |
14 | + |
15 | + def test_displays_create_oci_project_link(self): |
16 | + product = self.factory.makeProduct() |
17 | + |
18 | + browser = self.getUserBrowser( |
19 | + canonical_url(product), user=product.owner) |
20 | + text = extract_text(find_tag_by_id(browser.contents, 'global-actions')) |
21 | + |
22 | + self.assertIn("Create an OCI Project", text) |
23 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
24 | index 5e1489b..7e97bf9 100644 |
25 | --- a/lib/lp/registry/browser/configure.zcml |
26 | +++ b/lib/lp/registry/browser/configure.zcml |
27 | @@ -2170,6 +2170,13 @@ |
28 | template="../templates/ociproject-new.pt" |
29 | /> |
30 | <browser:page |
31 | + name="+new-oci-project" |
32 | + for="lp.registry.interfaces.product.IProduct" |
33 | + class="lp.registry.browser.ociproject.OCIProjectAddView" |
34 | + permission="launchpad.AnyPerson" |
35 | + template="../templates/ociproject-new.pt" |
36 | + /> |
37 | + <browser:page |
38 | name="+search-oci-project" |
39 | for="lp.registry.interfaces.distribution.IDistribution" |
40 | class="lp.registry.browser.distribution.DistributionOCIProjectSearchView" |
41 | diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py |
42 | index df7ac85..99c5b43 100644 |
43 | --- a/lib/lp/registry/browser/ociproject.py |
44 | +++ b/lib/lp/registry/browser/ociproject.py |
45 | @@ -28,6 +28,7 @@ from lp.app.browser.tales import CustomizableFormatter |
46 | from lp.app.errors import NotFoundError |
47 | from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin |
48 | from lp.oci.interfaces.ocirecipe import IOCIRecipeSet |
49 | +from lp.registry.interfaces.distribution import IDistribution |
50 | from lp.registry.interfaces.ociproject import ( |
51 | IOCIProject, |
52 | IOCIProjectSet, |
53 | @@ -38,6 +39,7 @@ from lp.registry.interfaces.ociprojectname import ( |
54 | IOCIProjectName, |
55 | IOCIProjectNameSet, |
56 | ) |
57 | +from lp.registry.interfaces.product import IProduct |
58 | from lp.services.features import getFeatureFlag |
59 | from lp.services.webapp import ( |
60 | canonical_url, |
61 | @@ -53,6 +55,15 @@ from lp.services.webapp.breadcrumb import Breadcrumb |
62 | from lp.services.webapp.interfaces import IMultiFacetedBreadcrumb |
63 | |
64 | |
65 | +def getPillarFieldName(pillar): |
66 | + if IDistribution.providedBy(pillar): |
67 | + return 'distribution' |
68 | + elif IProduct.providedBy(pillar): |
69 | + return 'project' |
70 | + raise NotImplementedError("This view only supports distribution or " |
71 | + "project as pillars for OCIProject.") |
72 | + |
73 | + |
74 | class OCIProjectAddView(LaunchpadFormView): |
75 | |
76 | schema = IOCIProjectName |
77 | @@ -85,10 +96,10 @@ class OCIProjectAddView(LaunchpadFormView): |
78 | oci_project = getUtility(IOCIProjectSet).getByPillarAndName( |
79 | self.context, oci_project_name.name) |
80 | if oci_project: |
81 | - self.setFieldError( |
82 | - 'name', |
83 | - 'There is already an OCI project in %s with this name.' % ( |
84 | - self.context.display_name)) |
85 | + pillar_type = getPillarFieldName(self.context) |
86 | + msg = ('There is already an OCI project in %s %s with this name.' |
87 | + % (pillar_type, self.context.display_name)) |
88 | + self.setFieldError('name', msg) |
89 | |
90 | |
91 | class OCIProjectFormatterAPI(CustomizableFormatter): |
92 | @@ -170,11 +181,20 @@ class OCIProjectEditView(LaunchpadEditFormView): |
93 | |
94 | schema = IOCIProject |
95 | field_names = [ |
96 | - 'distribution', |
97 | 'name', |
98 | 'official_recipe', |
99 | ] |
100 | |
101 | + def setUpFields(self): |
102 | + pillar_key = getPillarFieldName(self.context.pillar) |
103 | + self.field_names = [pillar_key] + self.field_names |
104 | + |
105 | + super(OCIProjectEditView, self).setUpFields() |
106 | + |
107 | + # Set the correct pillar field as mandatory |
108 | + pillar_field = self.form_fields.get(pillar_key).field |
109 | + pillar_field.required = True |
110 | + |
111 | def extendFields(self): |
112 | official_recipe = self.context.getOfficialRecipe() |
113 | self.form_fields += form.Fields( |
114 | @@ -191,16 +211,17 @@ class OCIProjectEditView(LaunchpadEditFormView): |
115 | |
116 | def validate(self, data): |
117 | super(OCIProjectEditView, self).validate(data) |
118 | - distribution = data.get('distribution') |
119 | + pillar_type_field = getPillarFieldName(self.context.pillar) |
120 | + pillar = data.get(pillar_type_field) |
121 | name = data.get('name') |
122 | - if distribution and name: |
123 | + if pillar and name: |
124 | oci_project = getUtility(IOCIProjectSet).getByPillarAndName( |
125 | - distribution, name) |
126 | + pillar, name) |
127 | if oci_project is not None and oci_project != self.context: |
128 | self.setFieldError( |
129 | 'name', |
130 | - 'There is already an OCI project in %s with this name.' % ( |
131 | - distribution.display_name)) |
132 | + 'There is already an OCI project in %s %s with this name.' |
133 | + % (pillar_type_field, pillar.display_name)) |
134 | |
135 | @action('Update OCI project', name='update') |
136 | def update_action(self, action, data): |
137 | diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py |
138 | index 5c3ebce..f974d39 100644 |
139 | --- a/lib/lp/registry/browser/product.py |
140 | +++ b/lib/lp/registry/browser/product.py |
141 | @@ -275,6 +275,10 @@ class ProductNavigation( |
142 | def traverse_commercialsubscription(self, name): |
143 | return self.context.commercial_subscription |
144 | |
145 | + @stepthrough('+oci') |
146 | + def traverse_oci(self, name): |
147 | + return self.context.getOCIProject(name) |
148 | + |
149 | @stepthrough('+series') |
150 | def traverse_series(self, name): |
151 | series = self.context.getSeries(name) |
152 | @@ -431,6 +435,7 @@ class ProductNavigationMenu(NavigationMenu): |
153 | facet = 'overview' |
154 | links = [ |
155 | 'details', |
156 | + 'new_oci_project', |
157 | 'announcements', |
158 | 'downloads', |
159 | ] |
160 | @@ -504,6 +509,11 @@ class ProductEditLinksMixin(StructuralSubscriptionMenuMixin): |
161 | def sharing(self): |
162 | return Link('+sharing', 'Sharing', icon='edit') |
163 | |
164 | + @enabled_with_permission('launchpad.Driver') |
165 | + def new_oci_project(self): |
166 | + text = 'Create an OCI Project' |
167 | + return Link('+new-oci-project', text, icon='add') |
168 | + |
169 | |
170 | class IProductEditMenu(Interface): |
171 | """A marker interface for the 'Change details' navigation menu.""" |
172 | @@ -522,7 +532,8 @@ class ProductActionNavigationMenu(NavigationMenu, ProductEditLinksMixin): |
173 | |
174 | @cachedproperty |
175 | def links(self): |
176 | - links = ['edit', 'review_license', 'administer', 'sharing'] |
177 | + links = ['edit', 'review_license', 'administer', 'sharing', |
178 | + 'new_oci_project'] |
179 | add_subscribe_link(links) |
180 | return links |
181 | |
182 | diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py |
183 | index 5187cd6..648f4d4 100644 |
184 | --- a/lib/lp/registry/browser/tests/test_ociproject.py |
185 | +++ b/lib/lp/registry/browser/tests/test_ociproject.py |
186 | @@ -79,7 +79,7 @@ class TestOCIProjectView(BrowserTestCase): |
187 | |
188 | layer = DatabaseFunctionalLayer |
189 | |
190 | - def test_index(self): |
191 | + def test_index_distribution_pillar(self): |
192 | distribution = self.factory.makeDistribution(displayname="My Distro") |
193 | oci_project = self.factory.makeOCIProject( |
194 | pillar=distribution, ociprojectname="oci-name") |
195 | @@ -91,6 +91,18 @@ class TestOCIProjectView(BrowserTestCase): |
196 | Name: oci-name |
197 | """, self.getMainText(oci_project)) |
198 | |
199 | + def test_index_project_pillar(self): |
200 | + product = self.factory.makeProduct(displayname="My Project") |
201 | + oci_project = self.factory.makeOCIProject( |
202 | + pillar=product, ociprojectname="oci-name") |
203 | + self.assertTextMatchesExpressionIgnoreWhitespace("""\ |
204 | + OCI project oci-name for My Project |
205 | + .* |
206 | + OCI project information |
207 | + Project: My Project |
208 | + Name: oci-name |
209 | + """, self.getMainText(oci_project)) |
210 | + |
211 | |
212 | class TestOCIProjectEditView(BrowserTestCase): |
213 | |
214 | @@ -123,11 +135,39 @@ class TestOCIProjectEditView(BrowserTestCase): |
215 | self.assertThat( |
216 | "Distribution:\n%s\nEdit OCI project" % ( |
217 | new_distribution.display_name), |
218 | - MatchesTagText(content, "distribution")) |
219 | + MatchesTagText(content, "pillar")) |
220 | self.assertThat( |
221 | "Name:\nnew-name\nEdit OCI project", |
222 | MatchesTagText(content, "name")) |
223 | |
224 | + def test_edit_oci_project_change_project_pillar(self): |
225 | + with admin_logged_in(): |
226 | + owner = self.factory.makePerson() |
227 | + project = self.factory.makeProduct(owner=owner) |
228 | + new_project = self.factory.makeProduct(owner=owner) |
229 | + oci_project = self.factory.makeOCIProject(pillar=project) |
230 | + new_project_name = new_project.name |
231 | + |
232 | + browser = self.getViewBrowser( |
233 | + oci_project, user=oci_project.pillar.owner) |
234 | + browser.getLink("Edit OCI project").click() |
235 | + browser.getControl(name="field.project").value = [new_project_name] |
236 | + browser.getControl(name="field.name").value = "new-name" |
237 | + browser.getControl("Update OCI project").click() |
238 | + |
239 | + content = find_main_content(browser.contents) |
240 | + with person_logged_in(owner): |
241 | + self.assertEqual( |
242 | + "OCI project new-name for %s" % new_project.display_name, |
243 | + extract_text(content.h1)) |
244 | + self.assertThat( |
245 | + "Project:\n%s\nEdit OCI project" % ( |
246 | + new_project.display_name), |
247 | + MatchesTagText(content, "pillar")) |
248 | + self.assertThat( |
249 | + "Name:\nnew-name\nEdit OCI project", |
250 | + MatchesTagText(content, "name")) |
251 | + |
252 | def test_edit_oci_project_ad_oci_project_admin(self): |
253 | admin_person = self.factory.makePerson() |
254 | admin_team = self.factory.makeTeam(members=[admin_person]) |
255 | @@ -153,7 +193,7 @@ class TestOCIProjectEditView(BrowserTestCase): |
256 | self.assertThat( |
257 | "Distribution:\n%s\nEdit OCI project" % ( |
258 | new_distribution.display_name), |
259 | - MatchesTagText(content, "distribution")) |
260 | + MatchesTagText(content, "pillar")) |
261 | self.assertThat( |
262 | "Name:\nnew-name\nEdit OCI project", |
263 | MatchesTagText(content, "name")) |
264 | @@ -180,8 +220,8 @@ class TestOCIProjectEditView(BrowserTestCase): |
265 | oci_project, user=oci_project.pillar.owner) |
266 | self.submitEditForm(browser, "two") |
267 | self.assertEqual( |
268 | - "There is already an OCI project in %s with this name." % ( |
269 | - pillar_display_name), |
270 | + "There is already an OCI project in distribution %s with this " |
271 | + "name." % (pillar_display_name), |
272 | extract_text(find_tags_by_class(browser.contents, "message")[1])) |
273 | |
274 | def test_edit_oci_project_invalid_name(self): |
275 | @@ -280,11 +320,33 @@ class TestOCIProjectAddView(BrowserTestCase): |
276 | self.assertThat( |
277 | "Distribution:\n%s\nEdit OCI project" % ( |
278 | new_distribution.display_name), |
279 | - MatchesTagText(content, "distribution")) |
280 | + MatchesTagText(content, "pillar")) |
281 | self.assertThat( |
282 | "Name:\nnew-name\nEdit OCI project", |
283 | MatchesTagText(content, "name")) |
284 | |
285 | + def test_create_oci_project_for_project(self): |
286 | + oci_project = self.factory.makeOCIProject() |
287 | + user = oci_project.pillar.owner |
288 | + project = self.factory.makeProduct(owner=user) |
289 | + browser = self.getViewBrowser( |
290 | + project, user=user, view_name='+new-oci-project') |
291 | + browser.getControl(name="field.name").value = "new-name" |
292 | + browser.getControl("Create OCI Project").click() |
293 | + |
294 | + content = find_main_content(browser.contents) |
295 | + with person_logged_in(user): |
296 | + self.assertEqual( |
297 | + "OCI project new-name for %s" % project.display_name, |
298 | + extract_text(content.h1)) |
299 | + self.assertThat( |
300 | + "Project:\n%s\nEdit OCI project" % ( |
301 | + project.display_name), |
302 | + MatchesTagText(content, "pillar")) |
303 | + self.assertThat( |
304 | + "Name:\nnew-name\nEdit OCI project", |
305 | + MatchesTagText(content, "name")) |
306 | + |
307 | def test_create_oci_project_already_exists(self): |
308 | person = self.factory.makePerson() |
309 | distribution = self.factory.makeDistribution(oci_project_admin=person) |
310 | @@ -297,9 +359,11 @@ class TestOCIProjectAddView(BrowserTestCase): |
311 | browser.getControl(name="field.name").value = "new-name" |
312 | browser.getControl("Create OCI Project").click() |
313 | |
314 | + expected_msg = ( |
315 | + "There is already an OCI project in distribution %s with this " |
316 | + "name." % distribution.display_name) |
317 | self.assertEqual( |
318 | - "There is already an OCI project in %s with this name." % ( |
319 | - distribution.display_name), |
320 | + expected_msg, |
321 | extract_text(find_tags_by_class(browser.contents, "message")[1])) |
322 | |
323 | def test_create_oci_project_no_permission(self): |
324 | diff --git a/lib/lp/registry/interfaces/product.py b/lib/lp/registry/interfaces/product.py |
325 | index c15a8b3..b386fca 100644 |
326 | --- a/lib/lp/registry/interfaces/product.py |
327 | +++ b/lib/lp/registry/interfaces/product.py |
328 | @@ -1,4 +1,4 @@ |
329 | -# Copyright 2009-2016 Canonical Ltd. This software is licensed under the |
330 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
331 | # GNU Affero General Public License version 3 (see the file LICENSE). |
332 | |
333 | """Interfaces including and related to IProduct.""" |
334 | @@ -798,6 +798,14 @@ class IProductView( |
335 | filter_statuses. |
336 | """ |
337 | |
338 | + def canAdministerOCIProjects(person): |
339 | + """Checks if the given person can manage OCI projects for this |
340 | + Product.""" |
341 | + |
342 | + def getOCIProject(name): |
343 | + """Return a `OCIProject` with the given name for this product, or None. |
344 | + """ |
345 | + |
346 | def getPackage(distroseries): |
347 | """Return a package in that distroseries for this product.""" |
348 | |
349 | diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py |
350 | index a99d846..8cdd446 100644 |
351 | --- a/lib/lp/registry/model/product.py |
352 | +++ b/lib/lp/registry/model/product.py |
353 | @@ -1,4 +1,4 @@ |
354 | -# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
355 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
356 | # GNU Affero General Public License version 3 (see the file LICENSE). |
357 | |
358 | """Database classes including and related to Product.""" |
359 | @@ -142,6 +142,7 @@ from lp.registry.interfaces.accesspolicy import ( |
360 | IAccessPolicyGrantSource, |
361 | IAccessPolicySource, |
362 | ) |
363 | +from lp.registry.interfaces.ociproject import IOCIProjectSet |
364 | from lp.registry.interfaces.oopsreferences import IHasOOPSReferences |
365 | from lp.registry.interfaces.person import ( |
366 | IPersonSet, |
367 | @@ -1140,6 +1141,23 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements, |
368 | """See `IBugTarget`.""" |
369 | return self.name |
370 | |
371 | + def getOCIProject(self, name): |
372 | + return getUtility(IOCIProjectSet).getByPillarAndName( |
373 | + self, name) |
374 | + |
375 | + def canAdministerOCIProjects(self, person): |
376 | + if person is None: |
377 | + return False |
378 | + # XXX: pappacena 2020-05-25: Maybe we should have an attribute named |
379 | + # oci_project_admin on Product too, the same way we have on |
380 | + # Distribution. |
381 | + if person.inTeam(self.driver): |
382 | + return True |
383 | + person_roles = IPersonRoles(person) |
384 | + if person_roles.in_admin or person_roles.isOwner(self): |
385 | + return True |
386 | + return False |
387 | + |
388 | def getPackage(self, distroseries): |
389 | """See `IProduct`.""" |
390 | if isinstance(distroseries, Distribution): |
391 | diff --git a/lib/lp/registry/templates/ociproject-index.pt b/lib/lp/registry/templates/ociproject-index.pt |
392 | index 427b383..9a14cb7 100644 |
393 | --- a/lib/lp/registry/templates/ociproject-index.pt |
394 | +++ b/lib/lp/registry/templates/ociproject-index.pt |
395 | @@ -28,11 +28,14 @@ |
396 | <div metal:fill-slot="main"> |
397 | <h2>OCI project information</h2> |
398 | <div class="two-column-list"> |
399 | - <dl id="distribution" tal:define="distribution context/distribution"> |
400 | - <dt>Distribution:</dt> |
401 | + <dl id="pillar" tal:define="pillar context/pillar"> |
402 | + <dt> |
403 | + <span tal:condition="context/distribution">Distribution:</span> |
404 | + <span tal:condition="context/project">Project:</span> |
405 | + </dt> |
406 | <dd> |
407 | - <a tal:attributes="href distribution/fmt:url" |
408 | - tal:content="distribution/display_name"/> |
409 | + <a tal:attributes="href pillar/fmt:url" |
410 | + tal:content="pillar/display_name"/> |
411 | <a tal:replace="structure context/menu:overview/edit/fmt:icon"/> |
412 | </dd> |
413 | </dl> |
414 | diff --git a/lib/lp/security.py b/lib/lp/security.py |
415 | index f7d7bf8..dfc06c2 100644 |
416 | --- a/lib/lp/security.py |
417 | +++ b/lib/lp/security.py |
418 | @@ -3464,7 +3464,7 @@ class EditOCIProject(AuthorizationBase): |
419 | """Maintainers, drivers, and admins can drive projects.""" |
420 | return (user.in_admin or |
421 | user.isDriver(self.obj.pillar) or |
422 | - user.inTeam(self.obj.pillar.oci_project_admin)) |
423 | + self.obj.pillar.canAdministerOCIProjects(user)) |
424 | |
425 | |
426 | class EditOCIProjectSeries(DelegatedAuthorization): |
One minor comment, one general comment, but LGTM