Merge ~ilasc/launchpad:oci-recipe-edit-existing-rules into launchpad:master
- Git
- lp:~ilasc/launchpad
- oci-recipe-edit-existing-rules
- Merge into master
Status: | Merged |
---|---|
Approved by: | Ioana Lasc |
Approved revision: | bcc0ec04b9f36615961761520bf344a1c57a42d8 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ilasc/launchpad:oci-recipe-edit-existing-rules |
Merge into: | launchpad:master |
Diff against target: |
614 lines (+422/-7) 7 files modified
lib/lp/oci/browser/configure.zcml (+7/-0) lib/lp/oci/browser/ocirecipe.py (+172/-1) lib/lp/oci/browser/tests/test_ocirecipe.py (+161/-0) lib/lp/oci/templates/ocirecipe-edit-push-rules.pt (+41/-0) lib/lp/oci/templates/ocirecipe-index.pt (+5/-0) lib/lp/registry/browser/person.py (+26/-6) lib/lp/security.py (+10/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Thiago F. Pappacena (community) | Approve | ||
Review via email: mp+387293@code.launchpad.net |
Commit message
Edit push rules and credentials on Recipe
Description of the change
This branch adds the capability to edit and delete existing push rules on the Recipe page and from there also navigate to a screen where the user can edit & delete existing OCI registry credentials and add new ones.
The capability to add new push rules will follow in a separate MP.
Thiago F. Pappacena (pappacena) wrote : | # |
- df8fb5e... by Ioana Lasc
-
Redirect to Person page for registry credential CRUD
- 5bf7dbe... by Ioana Lasc
-
Add edit permissions to OCIPushRule
Ioana Lasc (ilasc) wrote : | # |
Thiago thank you for the helpful comments!
Fully agreed on all: edit credentials here was a duplication of the Person page capabilities and we didn't really need to remove the security proxy to edit / destroy push rules in code.
MP is now ready for another look and much shorter!
Tom Wardill (twom) : | # |
Thiago F. Pappacena (pappacena) wrote : | # |
Couple of small details about code style (see also twom comments) and avoiding duplicated URLs for the same set of objects. But otherwise, LGTM.
- 8d719cd... by Ioana Lasc
-
Address code review comments
- 49f4c67... by Ioana Lasc
-
Remove accidentally checked in banner.css
- bcc0ec0... by Ioana Lasc
-
Fix issue with image name
Ioana Lasc (ilasc) wrote : | # |
Thiago this is the branch I mentioned in Standup, addressed all comments and replied to Tom's.
The change I mentioned is in the last commit that adds the missing Unit Tests for "multiple push rules already in the DB when editing the image name on 1 rule".
It might be worth having another look at the MP to see if you spot anything else as there were code updates.
Thanks Thiago
Thiago F. Pappacena (pappacena) wrote : | # |
The new test look really good. Good catch on spotting the bug!
I've added a minor comment, but the MP seems to be good to go.
Preview Diff
1 | diff --git a/lib/lp/oci/browser/configure.zcml b/lib/lp/oci/browser/configure.zcml |
2 | index b66e509..0aa6da8 100644 |
3 | --- a/lib/lp/oci/browser/configure.zcml |
4 | +++ b/lib/lp/oci/browser/configure.zcml |
5 | @@ -61,6 +61,13 @@ |
6 | template="../../app/templates/generic-edit.pt" /> |
7 | <browser:page |
8 | for="lp.oci.interfaces.ocirecipe.IOCIRecipe" |
9 | + class="lp.oci.browser.ocirecipe.OCIRecipeEditPushRulesView" |
10 | + permission="launchpad.Edit" |
11 | + name="+edit-push-rules" |
12 | + template="../templates/ocirecipe-edit-push-rules.pt" /> |
13 | + |
14 | + <browser:page |
15 | + for="lp.oci.interfaces.ocirecipe.IOCIRecipe" |
16 | class="lp.oci.browser.ocirecipe.OCIRecipeDeleteView" |
17 | permission="launchpad.Edit" |
18 | name="+delete" |
19 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py |
20 | index 356ac1c..cac47ac 100644 |
21 | --- a/lib/lp/oci/browser/ocirecipe.py |
22 | +++ b/lib/lp/oci/browser/ocirecipe.py |
23 | @@ -11,6 +11,7 @@ __all__ = [ |
24 | 'OCIRecipeAdminView', |
25 | 'OCIRecipeContextMenu', |
26 | 'OCIRecipeDeleteView', |
27 | + 'OCIRecipeEditPushRulesView', |
28 | 'OCIRecipeEditView', |
29 | 'OCIRecipeNavigation', |
30 | 'OCIRecipeNavigationMenu', |
31 | @@ -23,10 +24,13 @@ from lazr.restful.interface import ( |
32 | use_template, |
33 | ) |
34 | from zope.component import getUtility |
35 | +from zope.formlib.form import FormFields |
36 | from zope.interface import Interface |
37 | from zope.schema import ( |
38 | + Bool, |
39 | Choice, |
40 | List, |
41 | + TextLine, |
42 | ) |
43 | |
44 | from lp.app.browser.launchpadform import ( |
45 | @@ -36,6 +40,7 @@ from lp.app.browser.launchpadform import ( |
46 | ) |
47 | from lp.app.browser.lazrjs import InlinePersonEditPickerWidget |
48 | from lp.app.browser.tales import format_link |
49 | +from lp.app.errors import UnexpectedFormData |
50 | from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget |
51 | from lp.buildmaster.interfaces.processor import IProcessorSet |
52 | from lp.code.browser.widgets.gitref import GitRefWidget |
53 | @@ -138,12 +143,17 @@ class OCIRecipeContextMenu(ContextMenu): |
54 | |
55 | facet = 'overview' |
56 | |
57 | - links = ('request_builds',) |
58 | + links = ('request_builds', 'edit_push_rules') |
59 | |
60 | @enabled_with_permission('launchpad.Edit') |
61 | def request_builds(self): |
62 | return Link('+request-builds', 'Request builds', icon='add') |
63 | |
64 | + @enabled_with_permission('launchpad.Edit') |
65 | + def edit_push_rules(self): |
66 | + return Link( |
67 | + '+edit-push-rules', 'Edit push rules', icon='edit') |
68 | + |
69 | |
70 | class OCIProjectRecipesView(LaunchpadView): |
71 | """Default view for the list of OCI recipes of an OCI project.""" |
72 | @@ -243,6 +253,165 @@ def new_builds_notification_text(builds, already_pending=None): |
73 | return builds_text |
74 | |
75 | |
76 | +class OCIRecipeEditPushRulesView(LaunchpadEditFormView): |
77 | + """View for +ocirecipe-edit-push-rules.pt.""" |
78 | + |
79 | + class schema(Interface): |
80 | + """Schema for editing push rules.""" |
81 | + |
82 | + @cachedproperty |
83 | + def push_rules(self): |
84 | + return list( |
85 | + getUtility(IOCIPushRuleSet).findByRecipe(self.context)) |
86 | + |
87 | + @property |
88 | + def has_push_rules(self): |
89 | + return len(self.push_rules) > 0 |
90 | + |
91 | + def _getFieldName(self, name, rule_id): |
92 | + """Get the combined field name for an `OCIPushRule` ID. |
93 | + |
94 | + In order to be able to render a table, we encode the rule ID |
95 | + in the form field name. |
96 | + """ |
97 | + return "%s.%d" % (name, rule_id) |
98 | + |
99 | + def _parseFieldName(self, field_name): |
100 | + """Parse a combined field name as described in `_getFieldName`. |
101 | + |
102 | + :raises UnexpectedFormData: if the field name cannot be parsed or |
103 | + the `OCIPushRule` cannot be found. |
104 | + """ |
105 | + field_bits = field_name.split(".") |
106 | + if len(field_bits) != 2: |
107 | + raise UnexpectedFormData( |
108 | + "Cannot parse field name: %s" % field_name) |
109 | + field_type = field_bits[0] |
110 | + try: |
111 | + rule_id = int(field_bits[1]) |
112 | + except ValueError: |
113 | + raise UnexpectedFormData( |
114 | + "Cannot parse field name: %s" % field_name) |
115 | + return field_type, rule_id |
116 | + |
117 | + def setUpFields(self): |
118 | + """See `LaunchpadEditFormView`.""" |
119 | + LaunchpadEditFormView.setUpFields(self) |
120 | + |
121 | + image_fields = [] |
122 | + delete_fields = [] |
123 | + creds = [] |
124 | + for elem in list(self.context.push_rules): |
125 | + image_fields.append( |
126 | + TextLine( |
127 | + title=u'Image name', |
128 | + __name__=self._getFieldName('image_name', elem.id), |
129 | + default=elem.image_name, |
130 | + required=True, readonly=False)) |
131 | + delete_fields.append( |
132 | + Bool( |
133 | + title=u'Delete', |
134 | + __name__=self._getFieldName('delete', elem.id), |
135 | + default=False, |
136 | + required=True, readonly=False)) |
137 | + creds.append( |
138 | + TextLine( |
139 | + title=u'Username', |
140 | + __name__=self._getFieldName('username', elem.id), |
141 | + default=elem.registry_credentials.username, |
142 | + required=True, readonly=True)) |
143 | + creds.append( |
144 | + TextLine( |
145 | + title=u'Registry URL', |
146 | + __name__=self._getFieldName('url', elem.id), |
147 | + default=elem.registry_credentials.url, |
148 | + required=True, readonly=True)) |
149 | + |
150 | + self.form_fields += FormFields(*image_fields) |
151 | + self.form_fields += FormFields(*creds) |
152 | + self.form_fields += FormFields(*delete_fields) |
153 | + |
154 | + @property |
155 | + def label(self): |
156 | + return 'Edit OCI push rules for %s' % self.context.name |
157 | + |
158 | + page_title = 'Edit OCI push rules' |
159 | + |
160 | + @property |
161 | + def cancel_url(self): |
162 | + return canonical_url(self.context) |
163 | + |
164 | + def getRulesWidgets(self, rule): |
165 | + |
166 | + widgets_by_name = {widget.name: widget for widget in self.widgets} |
167 | + image_field_name = ( |
168 | + "field." + self._getFieldName("image_name", rule.id)) |
169 | + username_field_name = ( |
170 | + "field." + self._getFieldName("username", rule.id)) |
171 | + url_field_name = ( |
172 | + "field." + self._getFieldName("url", rule.id)) |
173 | + delete_field_name = ( |
174 | + "field." + self._getFieldName("delete", rule.id)) |
175 | + return { |
176 | + "image_name": widgets_by_name[image_field_name], |
177 | + "username": widgets_by_name[username_field_name], |
178 | + "url": widgets_by_name[url_field_name], |
179 | + "delete": widgets_by_name[delete_field_name], |
180 | + } |
181 | + |
182 | + def parseData(self, data): |
183 | + """Rearrange form data to make it easier to process.""" |
184 | + parsed_data = {} |
185 | + |
186 | + for field_name in sorted( |
187 | + name for name in data if name.split(".")[0] == "image_name"): |
188 | + _, rule_id = self._parseFieldName(field_name) |
189 | + image_field_name = self._getFieldName("image_name", rule_id) |
190 | + delete_field_name = self._getFieldName("delete", rule_id) |
191 | + if data.get(delete_field_name): |
192 | + action = "delete" |
193 | + else: |
194 | + action = "change" |
195 | + parsed_data.setdefault(rule_id, { |
196 | + "image_name": data.get(image_field_name), |
197 | + "action": action, |
198 | + }) |
199 | + |
200 | + return parsed_data |
201 | + |
202 | + def updatePushRulesFromData(self, parsed_data): |
203 | + rules_map = { |
204 | + rule.id: rule |
205 | + for rule in self.context.push_rules} |
206 | + for rule_id, parsed_rules in parsed_data.items(): |
207 | + rule = rules_map.get(rule_id) |
208 | + action = parsed_rules["action"] |
209 | + |
210 | + if action == "change": |
211 | + image_name = parsed_rules["image_name"] |
212 | + if not image_name: |
213 | + self.setFieldError( |
214 | + self._getFieldName( |
215 | + "image_name", rule_id), |
216 | + "Image name must be set.") |
217 | + else: |
218 | + if rule.image_name != image_name: |
219 | + rule.setNewImageName(image_name) |
220 | + elif action == "delete": |
221 | + rule.destroySelf() |
222 | + else: |
223 | + raise AssertionError("unknown action: %s" % action) |
224 | + |
225 | + @action("Save") |
226 | + def save(self, action, data): |
227 | + parsed_data = self.parseData(data) |
228 | + self.updatePushRulesFromData(parsed_data) |
229 | + |
230 | + if not self.errors: |
231 | + self.request.response.addNotification("Saved push rules") |
232 | + self.next_url = canonical_url(self.context) |
233 | + |
234 | + |
235 | class OCIRecipeRequestBuildsView(LaunchpadFormView): |
236 | """A view for requesting builds of an OCI recipe.""" |
237 | |
238 | @@ -322,6 +491,7 @@ class IOCIRecipeEditSchema(Interface): |
239 | "build_file", |
240 | "build_daily", |
241 | "require_virtualized", |
242 | + "push_rules", |
243 | ]) |
244 | |
245 | |
246 | @@ -453,6 +623,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin): |
247 | "git_ref", |
248 | "build_file", |
249 | "build_daily", |
250 | + "push_rules", |
251 | ) |
252 | custom_widget_git_ref = GitRefWidget |
253 | |
254 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py |
255 | index e25d463..24a9c6b 100644 |
256 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py |
257 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py |
258 | @@ -19,6 +19,8 @@ from fixtures import FakeLogger |
259 | import pytz |
260 | import soupmatchers |
261 | from testtools.matchers import ( |
262 | + Equals, |
263 | + MatchesDict, |
264 | MatchesSetwise, |
265 | MatchesStructure, |
266 | ) |
267 | @@ -35,11 +37,19 @@ from lp.oci.browser.ocirecipe import ( |
268 | OCIRecipeEditView, |
269 | OCIRecipeView, |
270 | ) |
271 | +from lp.oci.interfaces.ocipushrule import ( |
272 | + IOCIPushRuleSet, |
273 | + OCIPushRuleAlreadyExists, |
274 | + ) |
275 | from lp.oci.interfaces.ocirecipe import ( |
276 | CannotModifyOCIRecipeProcessor, |
277 | IOCIRecipeSet, |
278 | OCI_RECIPE_ALLOW_CREATE, |
279 | ) |
280 | +from lp.oci.interfaces.ociregistrycredentials import ( |
281 | + IOCIRegistryCredentialsSet, |
282 | + ) |
283 | +from lp.oci.tests.helpers import OCIConfigHelperMixin |
284 | from lp.services.database.constants import UTC_NOW |
285 | from lp.services.features.testing import FeatureFixture |
286 | from lp.services.propertycache import get_property_cache |
287 | @@ -831,6 +841,157 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView): |
288 | extract_text(find_main_content(browser.contents))) |
289 | |
290 | |
291 | +class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin, |
292 | + BaseTestOCIRecipeView): |
293 | + def setUp(self): |
294 | + super(TestOCIRecipeEditPushRulesView, self).setUp() |
295 | + self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu |
296 | + self.distroseries = self.factory.makeDistroSeries( |
297 | + distribution=self.ubuntu, name="shiny", displayname="Shiny") |
298 | + self.architectures = [] |
299 | + for processor, architecture in ("386", "i386"), ("amd64", "amd64"): |
300 | + das = self.factory.makeDistroArchSeries( |
301 | + distroseries=self.distroseries, architecturetag=architecture, |
302 | + processor=getUtility(IProcessorSet).getByName(processor)) |
303 | + das.addOrUpdateChroot(self.factory.makeLibraryFileAlias()) |
304 | + self.architectures.append(das) |
305 | + self.useFixture(FeatureFixture({ |
306 | + OCI_RECIPE_ALLOW_CREATE: "on", |
307 | + "oci.build_series.%s" % self.distroseries.distribution.name: |
308 | + self.distroseries.name, |
309 | + })) |
310 | + oci_project = self.factory.makeOCIProject( |
311 | + pillar=self.distroseries.distribution, |
312 | + ociprojectname="oci-project-name") |
313 | + self.recipe = self.factory.makeOCIRecipe( |
314 | + name="recipe-name", registrant=self.person, owner=self.person, |
315 | + oci_project=oci_project) |
316 | + |
317 | + self.setConfig() |
318 | + |
319 | + def test_edit_oci_push_rules(self): |
320 | + url = unicode(self.factory.getUniqueURL()) |
321 | + credentials = {'username': 'foo', 'password': 'bar'} |
322 | + registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
323 | + owner=self.person, |
324 | + url=url, |
325 | + credentials=credentials) |
326 | + image_name = self.factory.getUniqueUnicode() |
327 | + push_rule = getUtility(IOCIPushRuleSet).new( |
328 | + recipe=self.recipe, |
329 | + registry_credentials=registry_credentials, |
330 | + image_name=image_name) |
331 | + browser = self.getViewBrowser(self.recipe, user=self.person) |
332 | + browser.getLink("Edit push rules").click() |
333 | + # assert image name is displayed correctly |
334 | + with person_logged_in(self.person): |
335 | + self.assertEqual(image_name, browser.getControl( |
336 | + name="field.image_name.%d" % push_rule.id).value) |
337 | + |
338 | + # assert image name is required |
339 | + with person_logged_in(self.person): |
340 | + browser.getControl( |
341 | + name="field.image_name.%d" % push_rule.id).value = "" |
342 | + browser.getControl("Save").click() |
343 | + self.assertIn("Required input is missing", browser.contents) |
344 | + |
345 | + # set image name to valid string |
346 | + with person_logged_in(self.person): |
347 | + browser.getControl( |
348 | + name="field.image_name.%d" % push_rule.id).value = "image1" |
349 | + browser.getControl("Save").click() |
350 | + # and assert model changed |
351 | + with person_logged_in(self.person): |
352 | + self.assertEqual( |
353 | + push_rule.image_name, "image1") |
354 | + # Create a second push rule and test we call setNewImageName only |
355 | + # in cases where image name is different than the one on the model |
356 | + # otherwise we get the exception on rows the user doesn't actually |
357 | + # edit as the image name stays the same as it already is on the |
358 | + # model and setNewImageName will obviously find that rule in the |
359 | + # db with the same details |
360 | + second_rule = getUtility(IOCIPushRuleSet).new( |
361 | + recipe=self.recipe, |
362 | + registry_credentials=registry_credentials, |
363 | + image_name="second image") |
364 | + browser = self.getViewBrowser(self.recipe, user=self.person) |
365 | + browser.getLink("Edit push rules").click() |
366 | + with person_logged_in(self.person): |
367 | + browser.getControl( |
368 | + name="field.image_name.%d" % push_rule.id).value = "image2" |
369 | + browser.getControl("Save").click() |
370 | + with person_logged_in(self.person): |
371 | + self.assertEqual( |
372 | + push_rule.image_name, "image2") |
373 | + |
374 | + # Attempt to set the same name on the second rule |
375 | + # will result in expected exception |
376 | + browser = self.getViewBrowser(self.recipe, user=self.person) |
377 | + browser.getLink("Edit push rules").click() |
378 | + with person_logged_in(self.person): |
379 | + browser.getControl( |
380 | + name="field.image_name.%d" % second_rule.id).value = "image2" |
381 | + self.assertRaises(OCIPushRuleAlreadyExists, |
382 | + browser.getControl("Save").click) |
383 | + |
384 | + def test_delete_oci_push_rules(self): |
385 | + url = unicode(self.factory.getUniqueURL()) |
386 | + credentials = {'username': 'foo', 'password': 'bar'} |
387 | + registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
388 | + owner=self.person, |
389 | + url=url, |
390 | + credentials=credentials) |
391 | + image_name = self.factory.getUniqueUnicode() |
392 | + push_rule = getUtility(IOCIPushRuleSet).new( |
393 | + recipe=self.recipe, |
394 | + registry_credentials=registry_credentials, |
395 | + image_name=image_name) |
396 | + browser = self.getViewBrowser(self.recipe, user=self.person) |
397 | + browser.getLink("Edit push rules").click() |
398 | + with person_logged_in(self.person): |
399 | + browser.getControl( |
400 | + name="field.delete.%d" % push_rule.id).value = True |
401 | + browser.getControl("Save").click() |
402 | + |
403 | + with person_logged_in(self.person): |
404 | + self.assertIsNone( |
405 | + getUtility(IOCIPushRuleSet).getByID(push_rule.id)) |
406 | + |
407 | + def test_edit_oci_registry_creds(self): |
408 | + url = unicode(self.factory.getUniqueURL()) |
409 | + credentials = {'username': 'foo', 'password': 'bar'} |
410 | + image_name = self.factory.getUniqueUnicode() |
411 | + registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
412 | + owner=self.person, |
413 | + url=url, |
414 | + credentials=credentials) |
415 | + getUtility(IOCIPushRuleSet).new( |
416 | + recipe=self.recipe, |
417 | + registry_credentials=registry_credentials, |
418 | + image_name=image_name) |
419 | + browser = self.getViewBrowser(self.recipe, user=self.person) |
420 | + browser.getLink("Edit push rules").click() |
421 | + browser.getLink("Edit OCI registry credentials").click() |
422 | + |
423 | + browser.getControl(name="field.add_url").value = url |
424 | + browser.getControl(name="field.add_username").value = "new_username" |
425 | + browser.getControl(name="field.add_password").value = "password" |
426 | + browser.getControl(name="field.add_confirm_password" |
427 | + ).value = "password" |
428 | + |
429 | + browser.getControl("Save").click() |
430 | + with person_logged_in(self.person): |
431 | + creds = list(getUtility( |
432 | + IOCIRegistryCredentialsSet).findByOwner( |
433 | + self.person)) |
434 | + |
435 | + self.assertEqual(url, creds[1].url) |
436 | + self.assertThat( |
437 | + (creds[1]).getCredentials(), |
438 | + MatchesDict({"username": Equals("new_username"), |
439 | + "password": Equals("password")})) |
440 | + |
441 | + |
442 | class TestOCIProjectRecipesView(BaseTestOCIRecipeView): |
443 | def setUp(self): |
444 | super(TestOCIProjectRecipesView, self).setUp() |
445 | diff --git a/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt |
446 | new file mode 100644 |
447 | index 0000000..a1adb89 |
448 | --- /dev/null |
449 | +++ b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt |
450 | @@ -0,0 +1,41 @@ |
451 | +<html |
452 | + xmlns="http://www.w3.org/1999/xhtml" |
453 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
454 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
455 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
456 | + metal:use-macro="view/macro:page/main_only" |
457 | + i18n:domain="launchpad"> |
458 | +<body> |
459 | + |
460 | +<div metal:fill-slot="main"> |
461 | + <div metal:use-macro="context/@@launchpad_form/form"> |
462 | + <metal:formbody fill-slot="widgets"> |
463 | + <p condition="context/required:launchpad.Edit"> |
464 | + <a class="sprite edit" tal:attributes="href context/owner/fmt:url/+edit-oci-registry-credentials">Edit OCI registry credentials</a> |
465 | + </p> |
466 | + <table class="form"> |
467 | + <tr tal:repeat="rule context/push_rules"> |
468 | + <tal:rules_widgets |
469 | + define="rules_widgets python:view.getRulesWidgets(rule); |
470 | + parity python:'even' if repeat['rule'].even() else 'odd'"> |
471 | + <td tal:define="widget nocall:rules_widgets/image_name"> |
472 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
473 | + </td> |
474 | + <td tal:define="widget nocall:rules_widgets/username"> |
475 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
476 | + </td> |
477 | + <td tal:define="widget nocall:rules_widgets/url"> |
478 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
479 | + </td> |
480 | + <td tal:define="widget nocall:rules_widgets/delete"> |
481 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
482 | + </td> |
483 | + </tal:rules_widgets> |
484 | + </tr> |
485 | + </table> |
486 | + </metal:formbody> |
487 | +</div> |
488 | + |
489 | +</div> |
490 | +</body> |
491 | +</html> |
492 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt |
493 | index cf7c549..88a95b3 100644 |
494 | --- a/lib/lp/oci/templates/ocirecipe-index.pt |
495 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt |
496 | @@ -143,6 +143,11 @@ |
497 | This OCI recipe has no push rules defined yet. |
498 | </p> |
499 | |
500 | + <div tal:define="link context/menu:context/edit_push_rules" |
501 | + tal:condition="link/enabled"> |
502 | + <tal:edit-push-rules replace="structure link/fmt:link"/> |
503 | + </div> |
504 | + |
505 | </div> |
506 | |
507 | </body> |
508 | diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py |
509 | index 83e5757..a8ef6fc 100644 |
510 | --- a/lib/lp/registry/browser/person.py |
511 | +++ b/lib/lp/registry/browser/person.py |
512 | @@ -102,7 +102,10 @@ from zope.schema.vocabulary import ( |
513 | SimpleVocabulary, |
514 | ) |
515 | from zope.security.interfaces import Unauthorized |
516 | -from zope.security.proxy import removeSecurityProxy |
517 | +from zope.security.proxy import ( |
518 | + isinstance as zope_isinstance, |
519 | + removeSecurityProxy, |
520 | + ) |
521 | |
522 | from lp import _ |
523 | from lp.app.browser.launchpadform import ( |
524 | @@ -138,10 +141,12 @@ from lp.code.errors import InvalidNamespace |
525 | from lp.code.interfaces.branchnamespace import IBranchNamespaceSet |
526 | from lp.code.interfaces.gitlookup import IGitTraverser |
527 | from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet |
528 | +from lp.oci.interfaces.ocirecipe import IOCIRecipe |
529 | from lp.oci.interfaces.ociregistrycredentials import ( |
530 | IOCIRegistryCredentialsSet, |
531 | OCIRegistryCredentialsAlreadyExist, |
532 | ) |
533 | +from lp.oci.model.ocirecipe import OCIRecipe |
534 | from lp.registry.browser import BaseRdfView |
535 | from lp.registry.browser.branding import BrandingChangeView |
536 | from lp.registry.browser.menu import ( |
537 | @@ -199,7 +204,10 @@ from lp.registry.interfaces.teammembership import ( |
538 | ) |
539 | from lp.registry.interfaces.wikiname import IWikiNameSet |
540 | from lp.registry.mail.notification import send_direct_contact_email |
541 | -from lp.registry.model.person import get_recipients |
542 | +from lp.registry.model.person import ( |
543 | + get_recipients, |
544 | + Person, |
545 | + ) |
546 | from lp.services.config import config |
547 | from lp.services.database.decoratedresultset import DecoratedResultSet |
548 | from lp.services.database.sqlbase import flush_database_updates |
549 | @@ -3669,8 +3677,14 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView): |
550 | |
551 | @cachedproperty |
552 | def oci_registry_credentials(self): |
553 | - return list(getUtility( |
554 | - IOCIRegistryCredentialsSet).findByOwner(self.context)) |
555 | + if IPerson.providedBy(self.context): |
556 | + owner = self.context |
557 | + elif IOCIRecipe.providedBy(self.context): |
558 | + owner = self.context.owner |
559 | + else: |
560 | + raise ValueError("Invalid context for this view") |
561 | + |
562 | + return list(getUtility(IOCIRegistryCredentialsSet).findByOwner(owner)) |
563 | |
564 | schema = Interface |
565 | |
566 | @@ -3883,6 +3897,12 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView): |
567 | credentials.destroySelf() |
568 | |
569 | def addCredentials(self, parsed_add_credentials): |
570 | + if IPerson.providedBy(self.context): |
571 | + owner = self.context |
572 | + elif IOCIRecipe.providedBy(self.context): |
573 | + owner = self.context.owner |
574 | + else: |
575 | + raise ValueError("Invalid context for this view") |
576 | url = parsed_add_credentials["url"] |
577 | password = parsed_add_credentials["password"] |
578 | confirm_password = parsed_add_credentials["confirm_password"] |
579 | @@ -3902,7 +3922,7 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView): |
580 | 'password': password} |
581 | try: |
582 | getUtility(IOCIRegistryCredentialsSet).new( |
583 | - owner=self.context, |
584 | + owner=owner, |
585 | url=url, |
586 | credentials=credentials) |
587 | except OCIRegistryCredentialsAlreadyExist: |
588 | @@ -3915,7 +3935,7 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView): |
589 | credentials = {'username': username} |
590 | try: |
591 | getUtility(IOCIRegistryCredentialsSet).new( |
592 | - owner=self.context, |
593 | + owner=owner, |
594 | url=url, |
595 | credentials=credentials) |
596 | except OCIRegistryCredentialsAlreadyExist: |
597 | diff --git a/lib/lp/security.py b/lib/lp/security.py |
598 | index bb87821..696c55d 100644 |
599 | --- a/lib/lp/security.py |
600 | +++ b/lib/lp/security.py |
601 | @@ -3571,3 +3571,13 @@ class ViewOCIRegistryCredentials(AuthorizationBase): |
602 | class ViewOCIPushRule(AnonymousAuthorization): |
603 | """Anyone can view an `IOCIPushRule`.""" |
604 | usedfor = IOCIPushRule |
605 | + |
606 | + |
607 | +class OCIPushRuleEdit(AuthorizationBase): |
608 | + permission = 'launchpad.Edit' |
609 | + usedfor = IOCIPushRule |
610 | + |
611 | + def checkAuthenticated(self, user): |
612 | + return ( |
613 | + user.isOwner(self.obj.recipe) or |
614 | + user.in_commercial_admin or user.in_admin) |
Thanks for splitting the MP, ilasc. It helped a lot!
I have added a couple of comments, mostly about permission checking and a doubt the URL and implementation for the "Edit credentials" screen here and on /~/+edit- oci-registry- credentials.