Merge ~cjwatson/launchpad:oci-recipe-push-rules-add into launchpad:master
- Git
- lp:~cjwatson/launchpad
- oci-recipe-push-rules-add
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | f2ce0ac900cac32a41f01a37ec77242c3d2bb45e |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:oci-recipe-push-rules-add |
Merge into: | launchpad:master |
Diff against target: |
960 lines (+696/-56) 6 files modified
lib/lp/oci/browser/ocirecipe.py (+203/-32) lib/lp/oci/browser/tests/test_ocirecipe.py (+267/-5) lib/lp/oci/javascript/ocirecipe.edit.js (+38/-0) lib/lp/oci/templates/ocirecipe-edit-push-rules.pt (+126/-19) lib/lp/oci/vocabularies.py (+52/-0) lib/lp/oci/vocabularies.zcml (+10/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Thiago F. Pappacena (community) | Approve | ||
Review via email:
|
Commit message
Allow adding push rules on OCIRecipe:
Description of the change
This is https:/
Screenshot: https:/
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py | |||
2 | index 3dd764c..c2dceb2 100644 | |||
3 | --- a/lib/lp/oci/browser/ocirecipe.py | |||
4 | +++ b/lib/lp/oci/browser/ocirecipe.py | |||
5 | @@ -25,12 +25,18 @@ from lazr.restful.interface import ( | |||
6 | 25 | ) | 25 | ) |
7 | 26 | from zope.component import getUtility | 26 | from zope.component import getUtility |
8 | 27 | from zope.formlib.form import FormFields | 27 | from zope.formlib.form import FormFields |
9 | 28 | from zope.formlib.widget import ( | ||
10 | 29 | DisplayWidget, | ||
11 | 30 | renderElement, | ||
12 | 31 | ) | ||
13 | 28 | from zope.interface import Interface | 32 | from zope.interface import Interface |
14 | 29 | from zope.schema import ( | 33 | from zope.schema import ( |
15 | 30 | Bool, | 34 | Bool, |
16 | 31 | Choice, | 35 | Choice, |
17 | 32 | List, | 36 | List, |
18 | 37 | Password, | ||
19 | 33 | TextLine, | 38 | TextLine, |
20 | 39 | ValidationError, | ||
21 | 34 | ) | 40 | ) |
22 | 35 | 41 | ||
23 | 36 | from lp.app.browser.launchpadform import ( | 42 | from lp.app.browser.launchpadform import ( |
24 | @@ -44,7 +50,10 @@ from lp.app.errors import UnexpectedFormData | |||
25 | 44 | from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget | 50 | from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget |
26 | 45 | from lp.buildmaster.interfaces.processor import IProcessorSet | 51 | from lp.buildmaster.interfaces.processor import IProcessorSet |
27 | 46 | from lp.code.browser.widgets.gitref import GitRefWidget | 52 | from lp.code.browser.widgets.gitref import GitRefWidget |
29 | 47 | from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet | 53 | from lp.oci.interfaces.ocipushrule import ( |
30 | 54 | IOCIPushRuleSet, | ||
31 | 55 | OCIPushRuleAlreadyExists, | ||
32 | 56 | ) | ||
33 | 48 | from lp.oci.interfaces.ocirecipe import ( | 57 | from lp.oci.interfaces.ocirecipe import ( |
34 | 49 | IOCIRecipe, | 58 | IOCIRecipe, |
35 | 50 | IOCIRecipeSet, | 59 | IOCIRecipeSet, |
36 | @@ -56,6 +65,8 @@ from lp.oci.interfaces.ocirecipe import ( | |||
37 | 56 | ) | 65 | ) |
38 | 57 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet | 66 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
39 | 58 | from lp.oci.interfaces.ociregistrycredentials import ( | 67 | from lp.oci.interfaces.ociregistrycredentials import ( |
40 | 68 | IOCIRegistryCredentialsSet, | ||
41 | 69 | OCIRegistryCredentialsAlreadyExist, | ||
42 | 59 | user_can_edit_credentials_for_owner, | 70 | user_can_edit_credentials_for_owner, |
43 | 60 | ) | 71 | ) |
44 | 61 | from lp.services.features import getFeatureFlag | 72 | from lp.services.features import getFeatureFlag |
45 | @@ -72,6 +83,7 @@ from lp.services.webapp import ( | |||
46 | 72 | stepthrough, | 83 | stepthrough, |
47 | 73 | structured, | 84 | structured, |
48 | 74 | ) | 85 | ) |
49 | 86 | from lp.services.webapp.authorization import check_permission | ||
50 | 75 | from lp.services.webapp.batching import BatchNavigator | 87 | from lp.services.webapp.batching import BatchNavigator |
51 | 76 | from lp.services.webapp.breadcrumb import NameBreadcrumb | 88 | from lp.services.webapp.breadcrumb import NameBreadcrumb |
52 | 77 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin | 89 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
53 | @@ -264,7 +276,17 @@ def new_builds_notification_text(builds, already_pending=None): | |||
54 | 264 | return builds_text | 276 | return builds_text |
55 | 265 | 277 | ||
56 | 266 | 278 | ||
58 | 267 | class OCIRecipeEditPushRulesView(LaunchpadEditFormView): | 279 | class InvisibleCredentialsWidget(DisplayWidget): |
59 | 280 | """A widget that just displays a private icon. | ||
60 | 281 | |||
61 | 282 | This indicates invisible credentials. | ||
62 | 283 | """ | ||
63 | 284 | |||
64 | 285 | def __call__(self): | ||
65 | 286 | return renderElement("span", id=self.name, cssClass="sprite private") | ||
66 | 287 | |||
67 | 288 | |||
68 | 289 | class OCIRecipeEditPushRulesView(LaunchpadFormView): | ||
69 | 268 | """View for +ocirecipe-edit-push-rules.pt.""" | 290 | """View for +ocirecipe-edit-push-rules.pt.""" |
70 | 269 | 291 | ||
71 | 270 | class schema(Interface): | 292 | class schema(Interface): |
72 | @@ -311,41 +333,103 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView): | |||
73 | 311 | return field_type, rule_id | 333 | return field_type, rule_id |
74 | 312 | 334 | ||
75 | 313 | def setUpFields(self): | 335 | def setUpFields(self): |
80 | 314 | """See `LaunchpadEditFormView`.""" | 336 | super(OCIRecipeEditPushRulesView, self).setUpFields() |
81 | 315 | LaunchpadEditFormView.setUpFields(self) | 337 | image_name_fields = [] |
82 | 316 | 338 | url_fields = [] | |
83 | 317 | image_fields = [] | 339 | private_url_fields = [] |
84 | 340 | username_fields = [] | ||
85 | 341 | private_username_fields = [] | ||
86 | 342 | password_fields = [] | ||
87 | 318 | delete_fields = [] | 343 | delete_fields = [] |
88 | 319 | creds = [] | ||
89 | 320 | for elem in list(self.context.push_rules): | 344 | for elem in list(self.context.push_rules): |
91 | 321 | image_fields.append( | 345 | image_name_fields.append( |
92 | 322 | TextLine( | 346 | TextLine( |
93 | 323 | title=u'Image name', | ||
94 | 324 | __name__=self._getFieldName('image_name', elem.id), | 347 | __name__=self._getFieldName('image_name', elem.id), |
95 | 325 | default=elem.image_name, | 348 | default=elem.image_name, |
96 | 326 | required=True, readonly=False)) | 349 | required=True, readonly=False)) |
97 | 350 | if check_permission('launchpad.View', elem.registry_credentials): | ||
98 | 351 | url_fields.append( | ||
99 | 352 | TextLine( | ||
100 | 353 | __name__=self._getFieldName('url', elem.id), | ||
101 | 354 | default=elem.registry_credentials.url, | ||
102 | 355 | required=True, readonly=True)) | ||
103 | 356 | username_fields.append( | ||
104 | 357 | TextLine( | ||
105 | 358 | __name__=self._getFieldName('username', elem.id), | ||
106 | 359 | default=elem.registry_credentials.username, | ||
107 | 360 | required=True, readonly=True)) | ||
108 | 361 | else: | ||
109 | 362 | # XXX cjwatson 2020-08-27: Ideally we'd be able to just show | ||
110 | 363 | # the URL, and maybe the username too, but the | ||
111 | 364 | # launchpad.View security adapter for OCIRegistryCredentials | ||
112 | 365 | # doesn't currently allow that in some cases (e.g. a | ||
113 | 366 | # team-owned recipe with a push rule using credentials owned | ||
114 | 367 | # by another team member). In future it might make sense to | ||
115 | 368 | # add a launchpad.LimitedView adapter that grants access if | ||
116 | 369 | # the credentials are used by a push rule on one of the | ||
117 | 370 | # viewer's recipes. | ||
118 | 371 | private_url_fields.append( | ||
119 | 372 | TextLine( | ||
120 | 373 | __name__=self._getFieldName('url', elem.id), | ||
121 | 374 | default='', required=True, readonly=True)) | ||
122 | 375 | private_username_fields.append( | ||
123 | 376 | TextLine( | ||
124 | 377 | __name__=self._getFieldName('username', elem.id), | ||
125 | 378 | default='', required=True, readonly=True)) | ||
126 | 327 | delete_fields.append( | 379 | delete_fields.append( |
127 | 328 | Bool( | 380 | Bool( |
128 | 329 | title=u'Delete', | ||
129 | 330 | __name__=self._getFieldName('delete', elem.id), | 381 | __name__=self._getFieldName('delete', elem.id), |
130 | 331 | default=False, | 382 | default=False, |
131 | 332 | required=True, readonly=False)) | 383 | required=True, readonly=False)) |
148 | 333 | creds.append( | 384 | image_name_fields.append( |
149 | 334 | TextLine( | 385 | TextLine( |
150 | 335 | title=u'Username', | 386 | __name__=u'add_image_name', |
151 | 336 | __name__=self._getFieldName('username', elem.id), | 387 | required=False, readonly=False)) |
152 | 337 | default=elem.registry_credentials.username, | 388 | add_credentials = Choice( |
153 | 338 | required=True, readonly=True)) | 389 | __name__='add_credentials', |
154 | 339 | creds.append( | 390 | default='existing', values=('existing', 'new'), |
155 | 340 | TextLine( | 391 | required=False, readonly=False) |
156 | 341 | title=u'Registry URL', | 392 | existing_credentials = Choice( |
157 | 342 | __name__=self._getFieldName('url', elem.id), | 393 | vocabulary='OCIRegistryCredentials', |
158 | 343 | default=elem.registry_credentials.url, | 394 | required=False, |
159 | 344 | required=True, readonly=True)) | 395 | __name__=u'existing_credentials') |
160 | 345 | 396 | url_fields.append( | |
161 | 346 | self.form_fields += FormFields(*image_fields) | 397 | TextLine( |
162 | 347 | self.form_fields += FormFields(*creds) | 398 | __name__=u'add_url', |
163 | 348 | self.form_fields += FormFields(*delete_fields) | 399 | required=False, readonly=False)) |
164 | 400 | username_fields.append( | ||
165 | 401 | TextLine( | ||
166 | 402 | __name__=u'add_username', | ||
167 | 403 | required=False, readonly=False)) | ||
168 | 404 | password_fields.append( | ||
169 | 405 | Password( | ||
170 | 406 | __name__=u'add_password', | ||
171 | 407 | required=False, readonly=False)) | ||
172 | 408 | password_fields.append( | ||
173 | 409 | Password( | ||
174 | 410 | __name__=u'add_confirm_password', | ||
175 | 411 | required=False, readonly=False)) | ||
176 | 412 | |||
177 | 413 | self.form_fields = ( | ||
178 | 414 | FormFields(*image_name_fields) + | ||
179 | 415 | FormFields(*url_fields) + | ||
180 | 416 | FormFields( | ||
181 | 417 | *private_url_fields, | ||
182 | 418 | custom_widget=InvisibleCredentialsWidget) + | ||
183 | 419 | FormFields(*username_fields) + | ||
184 | 420 | FormFields( | ||
185 | 421 | *private_username_fields, | ||
186 | 422 | custom_widget=InvisibleCredentialsWidget) + | ||
187 | 423 | FormFields(*password_fields) + | ||
188 | 424 | FormFields(*delete_fields) + | ||
189 | 425 | FormFields(add_credentials, existing_credentials)) | ||
190 | 426 | |||
191 | 427 | def setUpWidgets(self, context=None): | ||
192 | 428 | """See `LaunchpadFormView`.""" | ||
193 | 429 | super(OCIRecipeEditPushRulesView, self).setUpWidgets(context=context) | ||
194 | 430 | for widget in self.widgets: | ||
195 | 431 | widget.display_label = False | ||
196 | 432 | widget.hint = None | ||
197 | 349 | 433 | ||
198 | 350 | @property | 434 | @property |
199 | 351 | def label(self): | 435 | def label(self): |
200 | @@ -357,28 +441,60 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView): | |||
201 | 357 | def cancel_url(self): | 441 | def cancel_url(self): |
202 | 358 | return canonical_url(self.context) | 442 | return canonical_url(self.context) |
203 | 359 | 443 | ||
206 | 360 | def getRulesWidgets(self, rule): | 444 | def getRuleWidgets(self, rule): |
205 | 361 | |||
207 | 362 | widgets_by_name = {widget.name: widget for widget in self.widgets} | 445 | widgets_by_name = {widget.name: widget for widget in self.widgets} |
208 | 446 | url_field_name = ( | ||
209 | 447 | "field." + self._getFieldName("url", rule.id)) | ||
210 | 363 | image_field_name = ( | 448 | image_field_name = ( |
211 | 364 | "field." + self._getFieldName("image_name", rule.id)) | 449 | "field." + self._getFieldName("image_name", rule.id)) |
212 | 365 | username_field_name = ( | 450 | username_field_name = ( |
213 | 366 | "field." + self._getFieldName("username", rule.id)) | 451 | "field." + self._getFieldName("username", rule.id)) |
214 | 367 | url_field_name = ( | ||
215 | 368 | "field." + self._getFieldName("url", rule.id)) | ||
216 | 369 | delete_field_name = ( | 452 | delete_field_name = ( |
217 | 370 | "field." + self._getFieldName("delete", rule.id)) | 453 | "field." + self._getFieldName("delete", rule.id)) |
218 | 371 | return { | 454 | return { |
219 | 455 | "url": widgets_by_name[url_field_name], | ||
220 | 372 | "image_name": widgets_by_name[image_field_name], | 456 | "image_name": widgets_by_name[image_field_name], |
221 | 373 | "username": widgets_by_name[username_field_name], | 457 | "username": widgets_by_name[username_field_name], |
222 | 374 | "url": widgets_by_name[url_field_name], | ||
223 | 375 | "delete": widgets_by_name[delete_field_name], | 458 | "delete": widgets_by_name[delete_field_name], |
224 | 376 | } | 459 | } |
225 | 377 | 460 | ||
226 | 461 | def getNewRuleWidgets(self): | ||
227 | 462 | widgets_by_name = {widget.name: widget for widget in self.widgets} | ||
228 | 463 | return { | ||
229 | 464 | "image_name": widgets_by_name["field.add_image_name"], | ||
230 | 465 | "existing_credentials": | ||
231 | 466 | widgets_by_name["field.existing_credentials"], | ||
232 | 467 | "url": widgets_by_name["field.add_url"], | ||
233 | 468 | "username": widgets_by_name["field.add_username"], | ||
234 | 469 | "password": widgets_by_name["field.add_password"], | ||
235 | 470 | "confirm_password": widgets_by_name["field.add_confirm_password"], | ||
236 | 471 | } | ||
237 | 472 | |||
238 | 378 | def parseData(self, data): | 473 | def parseData(self, data): |
239 | 379 | """Rearrange form data to make it easier to process.""" | 474 | """Rearrange form data to make it easier to process.""" |
240 | 380 | parsed_data = {} | 475 | parsed_data = {} |
242 | 381 | 476 | add_image_name = data.get("add_image_name") | |
243 | 477 | add_url = data.get("add_url") | ||
244 | 478 | add_username = data.get("add_username") | ||
245 | 479 | add_password = data.get("add_password") | ||
246 | 480 | add_confirm_password = data.get("add_confirm_password") | ||
247 | 481 | add_existing_credentials = data.get("existing_credentials") | ||
248 | 482 | |||
249 | 483 | # parse data from the Add new rule section of the form | ||
250 | 484 | if (add_url or add_username or add_password or | ||
251 | 485 | add_confirm_password or add_image_name or | ||
252 | 486 | add_existing_credentials): | ||
253 | 487 | parsed_data.setdefault(None, { | ||
254 | 488 | "image_name": add_image_name, | ||
255 | 489 | "url": add_url, | ||
256 | 490 | "username": add_username, | ||
257 | 491 | "password": add_password, | ||
258 | 492 | "confirm_password": add_confirm_password, | ||
259 | 493 | "existing_credentials": data["existing_credentials"], | ||
260 | 494 | "add_credentials": data["add_credentials"], | ||
261 | 495 | "action": "add", | ||
262 | 496 | }) | ||
263 | 497 | # parse data from the Edit existing rule section of the form | ||
264 | 382 | for field_name in sorted( | 498 | for field_name in sorted( |
265 | 383 | name for name in data if name.split(".")[0] == "image_name"): | 499 | name for name in data if name.split(".")[0] == "image_name"): |
266 | 384 | _, rule_id = self._parseFieldName(field_name) | 500 | _, rule_id = self._parseFieldName(field_name) |
267 | @@ -395,6 +511,59 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView): | |||
268 | 395 | 511 | ||
269 | 396 | return parsed_data | 512 | return parsed_data |
270 | 397 | 513 | ||
271 | 514 | def addNewRule(self, parsed_data): | ||
272 | 515 | add_data = parsed_data[None] | ||
273 | 516 | image_name = add_data.get("image_name") | ||
274 | 517 | url = add_data.get("url") | ||
275 | 518 | password = add_data.get("password") | ||
276 | 519 | confirm_password = add_data.get("confirm_password") | ||
277 | 520 | username = add_data.get("username") | ||
278 | 521 | existing_credentials = add_data.get("existing_credentials") | ||
279 | 522 | add_credentials = add_data.get("add_credentials") | ||
280 | 523 | |||
281 | 524 | if not image_name: | ||
282 | 525 | self.setFieldError("add_image_name", "Image name must be set.") | ||
283 | 526 | return | ||
284 | 527 | |||
285 | 528 | if add_credentials == "existing": | ||
286 | 529 | if not existing_credentials: | ||
287 | 530 | return | ||
288 | 531 | |||
289 | 532 | credentials = existing_credentials | ||
290 | 533 | |||
291 | 534 | elif add_credentials == "new": | ||
292 | 535 | if not url: | ||
293 | 536 | self.setFieldError("add_url", "Registry URL must be set.") | ||
294 | 537 | return | ||
295 | 538 | if password != confirm_password: | ||
296 | 539 | self.setFieldError( | ||
297 | 540 | "add_confirm_password", "Passwords do not match.") | ||
298 | 541 | return | ||
299 | 542 | |||
300 | 543 | credentials_set = getUtility(IOCIRegistryCredentialsSet) | ||
301 | 544 | try: | ||
302 | 545 | credentials = credentials_set.getOrCreate( | ||
303 | 546 | owner=self.context.owner, url=url, | ||
304 | 547 | credentials={'username': username, 'password': password}) | ||
305 | 548 | except OCIRegistryCredentialsAlreadyExist: | ||
306 | 549 | self.setFieldError( | ||
307 | 550 | "add_url", | ||
308 | 551 | "Credentials already exist with the same URL and " | ||
309 | 552 | "username.") | ||
310 | 553 | return | ||
311 | 554 | except ValidationError: | ||
312 | 555 | self.setFieldError("add_url", "Not a valid URL.") | ||
313 | 556 | return | ||
314 | 557 | |||
315 | 558 | try: | ||
316 | 559 | getUtility(IOCIPushRuleSet).new( | ||
317 | 560 | self.context, credentials, image_name) | ||
318 | 561 | except OCIPushRuleAlreadyExists: | ||
319 | 562 | self.setFieldError( | ||
320 | 563 | "add_image_name", | ||
321 | 564 | "A push rule already exists with the same URL, image name, " | ||
322 | 565 | "and credentials.") | ||
323 | 566 | |||
324 | 398 | def updatePushRulesFromData(self, parsed_data): | 567 | def updatePushRulesFromData(self, parsed_data): |
325 | 399 | rules_map = { | 568 | rules_map = { |
326 | 400 | rule.id: rule | 569 | rule.id: rule |
327 | @@ -415,6 +584,8 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView): | |||
328 | 415 | rule.setNewImageName(image_name) | 584 | rule.setNewImageName(image_name) |
329 | 416 | elif action == "delete": | 585 | elif action == "delete": |
330 | 417 | rule.destroySelf() | 586 | rule.destroySelf() |
331 | 587 | elif action == "add": | ||
332 | 588 | self.addNewRule(parsed_data) | ||
333 | 418 | else: | 589 | else: |
334 | 419 | raise AssertionError("unknown action: %s" % action) | 590 | raise AssertionError("unknown action: %s" % action) |
335 | 420 | 591 | ||
336 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
337 | index e2cdd77..8b17168 100644 | |||
338 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
339 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
340 | @@ -17,9 +17,12 @@ import re | |||
341 | 17 | 17 | ||
342 | 18 | from fixtures import FakeLogger | 18 | from fixtures import FakeLogger |
343 | 19 | import pytz | 19 | import pytz |
344 | 20 | from six.moves.urllib.parse import quote | ||
345 | 20 | import soupmatchers | 21 | import soupmatchers |
346 | 22 | from storm.locals import Store | ||
347 | 21 | from testtools.matchers import ( | 23 | from testtools.matchers import ( |
348 | 22 | Equals, | 24 | Equals, |
349 | 25 | Is, | ||
350 | 23 | MatchesDict, | 26 | MatchesDict, |
351 | 24 | MatchesSetwise, | 27 | MatchesSetwise, |
352 | 25 | MatchesStructure, | 28 | MatchesStructure, |
353 | @@ -887,12 +890,20 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin, | |||
354 | 887 | "oci.build_series.%s" % self.distroseries.distribution.name: | 890 | "oci.build_series.%s" % self.distroseries.distribution.name: |
355 | 888 | self.distroseries.name, | 891 | self.distroseries.name, |
356 | 889 | })) | 892 | })) |
358 | 890 | oci_project = self.factory.makeOCIProject( | 893 | self.oci_project = self.factory.makeOCIProject( |
359 | 891 | pillar=self.distroseries.distribution, | 894 | pillar=self.distroseries.distribution, |
360 | 892 | ociprojectname="oci-project-name") | 895 | ociprojectname="oci-project-name") |
361 | 896 | |||
362 | 897 | self.member = self.factory.makePerson() | ||
363 | 898 | self.team = self.factory.makeTeam(members=[self.person, self.member]) | ||
364 | 899 | |||
365 | 893 | self.recipe = self.factory.makeOCIRecipe( | 900 | self.recipe = self.factory.makeOCIRecipe( |
366 | 894 | name="recipe-name", registrant=self.person, owner=self.person, | 901 | name="recipe-name", registrant=self.person, owner=self.person, |
368 | 895 | oci_project=oci_project) | 902 | oci_project=self.oci_project) |
369 | 903 | |||
370 | 904 | self.team_owned_recipe = self.factory.makeOCIRecipe( | ||
371 | 905 | name="recipe-name", registrant=self.person, owner=self.team, | ||
372 | 906 | oci_project=self.oci_project) | ||
373 | 896 | 907 | ||
374 | 897 | self.setConfig() | 908 | self.setConfig() |
375 | 898 | 909 | ||
376 | @@ -1013,7 +1024,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin, | |||
377 | 1013 | "Image name", "td", text=image_name)))) | 1024 | "Image name", "td", text=image_name)))) |
378 | 1014 | 1025 | ||
379 | 1015 | def test_edit_oci_push_rules(self): | 1026 | def test_edit_oci_push_rules(self): |
381 | 1016 | url = unicode(self.factory.getUniqueURL()) | 1027 | url = self.factory.getUniqueURL() |
382 | 1017 | credentials = {'username': 'foo', 'password': 'bar'} | 1028 | credentials = {'username': 'foo', 'password': 'bar'} |
383 | 1018 | registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( | 1029 | registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
384 | 1019 | owner=self.person, | 1030 | owner=self.person, |
385 | @@ -1077,8 +1088,52 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin, | |||
386 | 1077 | self.assertRaises(OCIPushRuleAlreadyExists, | 1088 | self.assertRaises(OCIPushRuleAlreadyExists, |
387 | 1078 | browser.getControl("Save").click) | 1089 | browser.getControl("Save").click) |
388 | 1079 | 1090 | ||
389 | 1091 | def test_edit_oci_push_rules_non_owner_of_credentials(self): | ||
390 | 1092 | url = self.factory.getUniqueURL() | ||
391 | 1093 | credentials = {'username': 'foo', 'password': 'bar'} | ||
392 | 1094 | registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( | ||
393 | 1095 | owner=self.person, | ||
394 | 1096 | url=url, | ||
395 | 1097 | credentials=credentials) | ||
396 | 1098 | image_names = [self.factory.getUniqueUnicode() for _ in range(2)] | ||
397 | 1099 | push_rules = [ | ||
398 | 1100 | getUtility(IOCIPushRuleSet).new( | ||
399 | 1101 | recipe=self.team_owned_recipe, | ||
400 | 1102 | registry_credentials=registry_credentials, | ||
401 | 1103 | image_name=image_name) | ||
402 | 1104 | for image_name in image_names] | ||
403 | 1105 | Store.of(push_rules[-1]).flush() | ||
404 | 1106 | push_rule_ids = [push_rule.id for push_rule in push_rules] | ||
405 | 1107 | browser = self.getViewBrowser(self.team_owned_recipe, user=self.member) | ||
406 | 1108 | browser.getLink("Edit push rules").click() | ||
407 | 1109 | row = soupmatchers.Tag( | ||
408 | 1110 | "push rule row", "tr", attrs={"class": "push-rule"}) | ||
409 | 1111 | self.assertThat(browser.contents, soupmatchers.HTMLContains( | ||
410 | 1112 | soupmatchers.Within( | ||
411 | 1113 | row, | ||
412 | 1114 | soupmatchers.Tag( | ||
413 | 1115 | "username widget", "span", | ||
414 | 1116 | attrs={ | ||
415 | 1117 | "id": "field.username.%d" % push_rule_ids[0], | ||
416 | 1118 | "class": "sprite private", | ||
417 | 1119 | })), | ||
418 | 1120 | soupmatchers.Within( | ||
419 | 1121 | row, | ||
420 | 1122 | soupmatchers.Tag( | ||
421 | 1123 | "url widget", "span", | ||
422 | 1124 | attrs={ | ||
423 | 1125 | "id": "field.url.%d" % push_rule_ids[0], | ||
424 | 1126 | "class": "sprite private", | ||
425 | 1127 | })))) | ||
426 | 1128 | browser.getControl( | ||
427 | 1129 | name="field.image_name.%d" % push_rule_ids[0]).value = "image1" | ||
428 | 1130 | browser.getControl("Save").click() | ||
429 | 1131 | with person_logged_in(self.member): | ||
430 | 1132 | self.assertEqual("image1", push_rules[0].image_name) | ||
431 | 1133 | self.assertEqual(image_names[1], push_rules[1].image_name) | ||
432 | 1134 | |||
433 | 1080 | def test_delete_oci_push_rules(self): | 1135 | def test_delete_oci_push_rules(self): |
435 | 1081 | url = unicode(self.factory.getUniqueURL()) | 1136 | url = self.factory.getUniqueURL() |
436 | 1082 | credentials = {'username': 'foo', 'password': 'bar'} | 1137 | credentials = {'username': 'foo', 'password': 'bar'} |
437 | 1083 | registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( | 1138 | registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
438 | 1084 | owner=self.person, | 1139 | owner=self.person, |
439 | @@ -1100,8 +1155,215 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin, | |||
440 | 1100 | self.assertIsNone( | 1155 | self.assertIsNone( |
441 | 1101 | getUtility(IOCIPushRuleSet).getByID(push_rule.id)) | 1156 | getUtility(IOCIPushRuleSet).getByID(push_rule.id)) |
442 | 1102 | 1157 | ||
443 | 1158 | def test_add_oci_push_rules_validations(self): | ||
444 | 1159 | # Add new rule works when there are no rules in the DB. | ||
445 | 1160 | browser = self.getViewBrowser(self.recipe, user=self.person) | ||
446 | 1161 | browser.getLink("Edit push rules").click() | ||
447 | 1162 | |||
448 | 1163 | # Save does not error if there are no changes on the form. | ||
449 | 1164 | browser.getControl("Save").click() | ||
450 | 1165 | self.assertIn("Saved push rules", browser.contents) | ||
451 | 1166 | |||
452 | 1167 | # If only an image name is given but no registry URL, we fail with | ||
453 | 1168 | # "Registry URL must be set". | ||
454 | 1169 | browser.getLink("Edit push rules").click() | ||
455 | 1170 | browser.getControl(name="field.add_image_name").value = "imagename1" | ||
456 | 1171 | browser.getControl(name="field.add_credentials").value = "new" | ||
457 | 1172 | browser.getControl("Save").click() | ||
458 | 1173 | self.assertIn("Registry URL must be set", browser.contents) | ||
459 | 1174 | |||
460 | 1175 | # No image name entered on the form. We assume user is only editing | ||
461 | 1176 | # and we allow saving the form. | ||
462 | 1177 | browser.getControl(name="field.add_image_name").value = "" | ||
463 | 1178 | browser.getControl("Save").click() | ||
464 | 1179 | self.assertIn("Saved push rules", browser.contents) | ||
465 | 1180 | |||
466 | 1181 | def test_add_oci_push_rules_new_empty_credentials(self): | ||
467 | 1182 | # Supplying an image name and registry URL creates a credentials | ||
468 | 1183 | # object without username or password, and a valid push rule based | ||
469 | 1184 | # on that credentials object. | ||
470 | 1185 | url = self.factory.getUniqueURL() | ||
471 | 1186 | browser = self.getViewBrowser(self.recipe, user=self.person) | ||
472 | 1187 | browser.getLink("Edit push rules").click() | ||
473 | 1188 | browser.getControl(name="field.add_credentials").value = "new" | ||
474 | 1189 | browser.getControl(name="field.add_image_name").value = "imagename1" | ||
475 | 1190 | browser.getControl(name="field.add_url").value = url | ||
476 | 1191 | browser.getControl("Save").click() | ||
477 | 1192 | with person_logged_in(self.person): | ||
478 | 1193 | rules = list(removeSecurityProxy( | ||
479 | 1194 | getUtility(IOCIPushRuleSet).findByRecipe(self.recipe))) | ||
480 | 1195 | self.assertEqual(len(rules), 1) | ||
481 | 1196 | rule = rules[0] | ||
482 | 1197 | self.assertThat(rule, MatchesStructure( | ||
483 | 1198 | image_name=Equals("imagename1"), | ||
484 | 1199 | registry_url=Equals(url), | ||
485 | 1200 | registry_credentials=MatchesStructure( | ||
486 | 1201 | url=Equals(url), | ||
487 | 1202 | username=Is(None)))) | ||
488 | 1203 | |||
489 | 1204 | with person_logged_in(self.person): | ||
490 | 1205 | self.assertEqual( | ||
491 | 1206 | {"password": None}, rule.registry_credentials.getCredentials()) | ||
492 | 1207 | |||
493 | 1208 | def test_add_oci_push_rules_new_username_password(self): | ||
494 | 1209 | # Supplying an image name, registry URL, username, and password | ||
495 | 1210 | # creates a credentials object with the given username or password, | ||
496 | 1211 | # and a valid push rule based on that credentials object. | ||
497 | 1212 | url = self.factory.getUniqueURL() | ||
498 | 1213 | browser = self.getViewBrowser(self.recipe, user=self.person) | ||
499 | 1214 | browser.getLink("Edit push rules").click() | ||
500 | 1215 | browser.getControl(name="field.add_credentials").value = "new" | ||
501 | 1216 | browser.getControl(name="field.add_image_name").value = "imagename3" | ||
502 | 1217 | browser.getControl(name="field.add_url").value = url | ||
503 | 1218 | browser.getControl(name="field.add_username").value = "username" | ||
504 | 1219 | browser.getControl(name="field.add_password").value = "password" | ||
505 | 1220 | browser.getControl( | ||
506 | 1221 | name="field.add_confirm_password").value = "password" | ||
507 | 1222 | browser.getControl("Save").click() | ||
508 | 1223 | with person_logged_in(self.person): | ||
509 | 1224 | rules = list(removeSecurityProxy( | ||
510 | 1225 | getUtility(IOCIPushRuleSet).findByRecipe(self.recipe))) | ||
511 | 1226 | self.assertEqual(len(rules), 1) | ||
512 | 1227 | rule = rules[0] | ||
513 | 1228 | self.assertThat(rule, MatchesStructure( | ||
514 | 1229 | image_name=Equals("imagename3"), | ||
515 | 1230 | registry_url=Equals(url), | ||
516 | 1231 | registry_credentials=MatchesStructure.byEquality( | ||
517 | 1232 | url=url, | ||
518 | 1233 | username="username"))) | ||
519 | 1234 | with person_logged_in(self.person): | ||
520 | 1235 | self.assertEqual( | ||
521 | 1236 | {"username": "username", "password": "password"}, | ||
522 | 1237 | rule.registry_credentials.getCredentials()) | ||
523 | 1238 | |||
524 | 1239 | def test_add_oci_push_rules_existing_credentials_duplicate(self): | ||
525 | 1240 | # Adding a new push rule using existing credentials fails if a rule | ||
526 | 1241 | # with the same image name already exists. | ||
527 | 1242 | existing_rule = self.factory.makeOCIPushRule( | ||
528 | 1243 | recipe=self.recipe, | ||
529 | 1244 | registry_credentials=self.factory.makeOCIRegistryCredentials( | ||
530 | 1245 | owner=self.recipe.owner)) | ||
531 | 1246 | existing_image_name = existing_rule.image_name | ||
532 | 1247 | existing_registry_url = existing_rule.registry_url | ||
533 | 1248 | existing_username = existing_rule.username | ||
534 | 1249 | browser = self.getViewBrowser(self.recipe, user=self.person) | ||
535 | 1250 | browser.getLink("Edit push rules").click() | ||
536 | 1251 | browser.getControl(name="field.add_credentials").value = "existing" | ||
537 | 1252 | browser.getControl(name="field.add_image_name").value = ( | ||
538 | 1253 | existing_image_name) | ||
539 | 1254 | browser.getControl(name="field.existing_credentials").value = ( | ||
540 | 1255 | "%s %s" % (quote(existing_registry_url), quote(existing_username))) | ||
541 | 1256 | browser.getControl("Save").click() | ||
542 | 1257 | self.assertIn( | ||
543 | 1258 | "A push rule already exists with the same URL, " | ||
544 | 1259 | "image name, and credentials.", browser.contents) | ||
545 | 1260 | |||
546 | 1261 | def test_add_oci_push_rules_existing_credentials(self): | ||
547 | 1262 | # Previously added registry credentials can be chosen from the radio | ||
548 | 1263 | # widget when adding a new rule. | ||
549 | 1264 | # We correctly display the radio buttons widget when the | ||
550 | 1265 | # username is empty in registry credentials and | ||
551 | 1266 | # allow correctly adding new rule based on it | ||
552 | 1267 | existing_rule = self.factory.makeOCIPushRule( | ||
553 | 1268 | recipe=self.recipe, | ||
554 | 1269 | registry_credentials=self.factory.makeOCIRegistryCredentials( | ||
555 | 1270 | owner=self.recipe.owner, credentials={})) | ||
556 | 1271 | existing_registry_url = existing_rule.registry_url | ||
557 | 1272 | browser = self.getViewBrowser(self.recipe, user=self.person) | ||
558 | 1273 | browser.getLink("Edit push rules").click() | ||
559 | 1274 | browser.getControl(name="field.add_credentials").value = "existing" | ||
560 | 1275 | browser.getControl(name="field.add_image_name").value = "imagename2" | ||
561 | 1276 | browser.getControl(name="field.existing_credentials").value = ( | ||
562 | 1277 | quote(existing_registry_url)) | ||
563 | 1278 | browser.getControl("Save").click() | ||
564 | 1279 | with person_logged_in(self.person): | ||
565 | 1280 | rules = list(removeSecurityProxy( | ||
566 | 1281 | getUtility(IOCIPushRuleSet).findByRecipe(self.recipe))) | ||
567 | 1282 | self.assertEqual(len(rules), 2) | ||
568 | 1283 | rule = rules[1] | ||
569 | 1284 | self.assertThat(rule, MatchesStructure( | ||
570 | 1285 | image_name=Equals("imagename2"), | ||
571 | 1286 | registry_url=Equals(existing_registry_url), | ||
572 | 1287 | registry_credentials=MatchesStructure( | ||
573 | 1288 | url=Equals(existing_registry_url), | ||
574 | 1289 | username=Is(None)))) | ||
575 | 1290 | with person_logged_in(self.person): | ||
576 | 1291 | self.assertEqual({}, rule.registry_credentials.getCredentials()) | ||
577 | 1292 | |||
578 | 1293 | def test_add_oci_push_rules_team_owned(self): | ||
579 | 1294 | url = self.factory.getUniqueURL() | ||
580 | 1295 | browser = self.getViewBrowser(self.team_owned_recipe, user=self.member) | ||
581 | 1296 | browser.getLink("Edit push rules").click() | ||
582 | 1297 | browser.getControl( | ||
583 | 1298 | name="field.add_image_name").value = "imagename1" | ||
584 | 1299 | browser.getControl( | ||
585 | 1300 | name="field.add_url").value = url | ||
586 | 1301 | browser.getControl(name="field.add_credentials").value = "new" | ||
587 | 1302 | browser.getControl("Save").click() | ||
588 | 1303 | |||
589 | 1304 | with person_logged_in(self.member): | ||
590 | 1305 | rules = list(removeSecurityProxy( | ||
591 | 1306 | getUtility(IOCIPushRuleSet).findByRecipe( | ||
592 | 1307 | self.team_owned_recipe))) | ||
593 | 1308 | self.assertEqual(len(rules), 1) | ||
594 | 1309 | rule = rules[0] | ||
595 | 1310 | self.assertThat(rule, MatchesStructure( | ||
596 | 1311 | image_name=Equals(u'imagename1'), | ||
597 | 1312 | registry_url=Equals(url), | ||
598 | 1313 | registry_credentials=MatchesStructure( | ||
599 | 1314 | url=Equals(url), | ||
600 | 1315 | username=Is(None)))) | ||
601 | 1316 | |||
602 | 1317 | with person_logged_in(self.member): | ||
603 | 1318 | self.assertThat( | ||
604 | 1319 | rule.registry_credentials.getCredentials(), | ||
605 | 1320 | MatchesDict( | ||
606 | 1321 | {"password": Equals(None)})) | ||
607 | 1322 | |||
608 | 1323 | def test_edit_oci_push_rules_team_owned(self): | ||
609 | 1324 | url = self.factory.getUniqueURL() | ||
610 | 1325 | browser = self.getViewBrowser(self.team_owned_recipe, user=self.member) | ||
611 | 1326 | browser.getLink("Edit push rules").click() | ||
612 | 1327 | browser.getControl( | ||
613 | 1328 | name="field.add_image_name").value = "imagename1" | ||
614 | 1329 | browser.getControl( | ||
615 | 1330 | name="field.add_url").value = url | ||
616 | 1331 | browser.getControl(name="field.add_credentials").value = "new" | ||
617 | 1332 | browser.getControl("Save").click() | ||
618 | 1333 | |||
619 | 1334 | # push rules created by another team member (self.member) | ||
620 | 1335 | # can be edited by self.person | ||
621 | 1336 | browser = self.getViewBrowser(self.team_owned_recipe, user=self.person) | ||
622 | 1337 | browser.getLink("Edit push rules").click() | ||
623 | 1338 | with person_logged_in(self.person): | ||
624 | 1339 | rules = list(removeSecurityProxy( | ||
625 | 1340 | getUtility(IOCIPushRuleSet).findByRecipe( | ||
626 | 1341 | self.team_owned_recipe))) | ||
627 | 1342 | self.assertEqual(len(rules), 1) | ||
628 | 1343 | rule = rules[0] | ||
629 | 1344 | self.assertEqual("imagename1", browser.getControl( | ||
630 | 1345 | name="field.image_name.%d" % rule.id).value) | ||
631 | 1346 | |||
632 | 1347 | # set image name to valid string | ||
633 | 1348 | with person_logged_in(self.person): | ||
634 | 1349 | browser.getControl( | ||
635 | 1350 | name="field.image_name.%d" % rule.id).value = "image1" | ||
636 | 1351 | browser.getControl("Save").click() | ||
637 | 1352 | |||
638 | 1353 | # and assert model changed | ||
639 | 1354 | with person_logged_in(self.member): | ||
640 | 1355 | self.assertEqual( | ||
641 | 1356 | rule.image_name, "image1") | ||
642 | 1357 | |||
643 | 1358 | # self.member will see the new image name | ||
644 | 1359 | browser = self.getViewBrowser(self.team_owned_recipe, user=self.member) | ||
645 | 1360 | browser.getLink("Edit push rules").click() | ||
646 | 1361 | with person_logged_in(self.member): | ||
647 | 1362 | self.assertEqual("image1", browser.getControl( | ||
648 | 1363 | name="field.image_name.%d" % rule.id).value) | ||
649 | 1364 | |||
650 | 1103 | def test_edit_oci_registry_creds(self): | 1365 | def test_edit_oci_registry_creds(self): |
652 | 1104 | url = unicode(self.factory.getUniqueURL()) | 1366 | url = self.factory.getUniqueURL() |
653 | 1105 | credentials = {'username': 'foo', 'password': 'bar'} | 1367 | credentials = {'username': 'foo', 'password': 'bar'} |
654 | 1106 | image_name = self.factory.getUniqueUnicode() | 1368 | image_name = self.factory.getUniqueUnicode() |
655 | 1107 | registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( | 1369 | registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
656 | diff --git a/lib/lp/oci/javascript/ocirecipe.edit.js b/lib/lp/oci/javascript/ocirecipe.edit.js | |||
657 | 1108 | new file mode 100644 | 1370 | new file mode 100644 |
658 | index 0000000..8b3d82d | |||
659 | --- /dev/null | |||
660 | +++ b/lib/lp/oci/javascript/ocirecipe.edit.js | |||
661 | @@ -0,0 +1,38 @@ | |||
662 | 1 | /* Copyright 2015-2020 Canonical Ltd. This software is licensed under the | ||
663 | 2 | * GNU Affero General Public License version 3 (see the file LICENSE). | ||
664 | 3 | * | ||
665 | 4 | * @module Y.lp.oci.ocirecipe.edit | ||
666 | 5 | * @requires node, DOM | ||
667 | 6 | */ | ||
668 | 7 | YUI.add('lp.oci.ocirecipe.edit', function(Y) { | ||
669 | 8 | var module = Y.namespace('lp.oci.ocirecipe.edit'); | ||
670 | 9 | |||
671 | 10 | module.set_enabled = function(field_id, is_enabled) { | ||
672 | 11 | var field = Y.DOM.byId(field_id); | ||
673 | 12 | if (field !== null) { | ||
674 | 13 | field.disabled = !is_enabled; | ||
675 | 14 | } | ||
676 | 15 | }; | ||
677 | 16 | |||
678 | 17 | module.onclick_add_credentials = function(e) { | ||
679 | 18 | var value = ''; | ||
680 | 19 | Y.all('input[name="field.add_credentials"]').each(function(node) { | ||
681 | 20 | if (node.get('checked')) { | ||
682 | 21 | value = node.get('value'); | ||
683 | 22 | } | ||
684 | 23 | }); | ||
685 | 24 | module.set_enabled('field.existing_credentials', value === 'existing'); | ||
686 | 25 | module.set_enabled('field.add_url', value === 'new'); | ||
687 | 26 | module.set_enabled('field.add_username', value === 'new'); | ||
688 | 27 | module.set_enabled('field.add_password', value === 'new'); | ||
689 | 28 | module.set_enabled('field.add_confirm_password', value === 'new'); | ||
690 | 29 | }; | ||
691 | 30 | |||
692 | 31 | module.setup = function() { | ||
693 | 32 | Y.all('input[name="field.add_credentials"]').on( | ||
694 | 33 | 'click', module.onclick_add_credentials); | ||
695 | 34 | |||
696 | 35 | // Set the initial state. | ||
697 | 36 | module.onclick_add_credentials(); | ||
698 | 37 | }; | ||
699 | 38 | }, '0.1', {'requires': ['node', 'DOM']}); | ||
700 | diff --git a/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt | |||
701 | index ccfe716..eee7db7 100644 | |||
702 | --- a/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt | |||
703 | +++ b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt | |||
704 | @@ -7,35 +7,142 @@ | |||
705 | 7 | i18n:domain="launchpad"> | 7 | i18n:domain="launchpad"> |
706 | 8 | <body> | 8 | <body> |
707 | 9 | 9 | ||
708 | 10 | <metal:block fill-slot="head_epilogue"> | ||
709 | 11 | <style type="text/css"> | ||
710 | 12 | table.push-rules-table { | ||
711 | 13 | max-width: 60%; | ||
712 | 14 | margin-bottom: 1em; | ||
713 | 15 | } | ||
714 | 16 | table.push-rules-table tr.even { | ||
715 | 17 | background-color: #eee; | ||
716 | 18 | } | ||
717 | 19 | /* These add up to 100%. */ | ||
718 | 20 | tr .push-rule-url { | ||
719 | 21 | width: 35%; | ||
720 | 22 | } | ||
721 | 23 | tr .push-rule-image-name { | ||
722 | 24 | width: 20%; | ||
723 | 25 | } | ||
724 | 26 | tr .push-rule-username { | ||
725 | 27 | width: 10%; | ||
726 | 28 | } | ||
727 | 29 | tr .push-rule-password, tr .push-rule-confirm-password { | ||
728 | 30 | width: 15%; | ||
729 | 31 | } | ||
730 | 32 | tr .push-rule-delete { | ||
731 | 33 | width: 5%; | ||
732 | 34 | } | ||
733 | 35 | </style> | ||
734 | 36 | </metal:block> | ||
735 | 37 | |||
736 | 10 | <div metal:fill-slot="main"> | 38 | <div metal:fill-slot="main"> |
737 | 11 | <div metal:use-macro="context/@@launchpad_form/form"> | 39 | <div metal:use-macro="context/@@launchpad_form/form"> |
738 | 12 | <metal:formbody fill-slot="widgets"> | 40 | <metal:formbody fill-slot="widgets"> |
739 | 13 | <p condition="view/can_edit_credentials"> | 41 | <p condition="view/can_edit_credentials"> |
740 | 14 | <a class="sprite edit" tal:attributes="href context/owner/fmt:url/+edit-oci-registry-credentials">Edit OCI registry credentials</a> | 42 | <a class="sprite edit" tal:attributes="href context/owner/fmt:url/+edit-oci-registry-credentials">Edit OCI registry credentials</a> |
741 | 15 | </p> | 43 | </p> |
761 | 16 | <table class="form"> | 44 | <table class="listing push-rules-table"> |
762 | 17 | <tr tal:repeat="rule context/push_rules"> | 45 | <thead> |
763 | 18 | <tal:rules_widgets | 46 | <tr> |
764 | 19 | define="rules_widgets python:view.getRulesWidgets(rule); | 47 | <th class="push-rule-image-name">Image name</th> |
765 | 20 | parity python:'even' if repeat['rule'].even() else 'odd'"> | 48 | <th class="push-rule-url">Registry URL</th> |
766 | 21 | <td tal:define="widget nocall:rules_widgets/image_name"> | 49 | <th class="push-rule-username">Username</th> |
767 | 22 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | 50 | <th class="push-rule-password">Password</th> |
768 | 23 | </td> | 51 | <th class="push-rule-confirm-password">Confirm password</th> |
769 | 24 | <td tal:define="widget nocall:rules_widgets/username"> | 52 | <th class="push-rule-delete">Delete?</th> |
770 | 25 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | 53 | </tr> |
771 | 26 | </td> | 54 | </thead> |
772 | 27 | <td tal:define="widget nocall:rules_widgets/url"> | 55 | <tbody> |
773 | 28 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | 56 | <tal:rule repeat="rule view/push_rules"> |
774 | 29 | </td> | 57 | <tal:rule_widgets |
775 | 30 | <td tal:define="widget nocall:rules_widgets/delete"> | 58 | define="rule_widgets python:view.getRuleWidgets(rule); |
776 | 31 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | 59 | parity python:'even' if repeat['rule'].even() else 'odd'"> |
777 | 32 | </td> | 60 | <tr tal:attributes="class string:push-rule ${parity}"> |
778 | 33 | </tal:rules_widgets> | 61 | <td class="push-rule-image-name" |
779 | 34 | </tr> | 62 | tal:define="widget nocall:rule_widgets/image_name"> |
780 | 63 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
781 | 64 | </td> | ||
782 | 65 | <td class="push-rule-url" | ||
783 | 66 | tal:define="widget nocall:rule_widgets/url"> | ||
784 | 67 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
785 | 68 | </td> | ||
786 | 69 | <td class="push-rule-username" | ||
787 | 70 | tal:define="widget nocall:rule_widgets/username"> | ||
788 | 71 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
789 | 72 | </td> | ||
790 | 73 | <td colspan="2" /> | ||
791 | 74 | <td class="push-rule-delete" | ||
792 | 75 | tal:define="widget nocall:rule_widgets/delete"> | ||
793 | 76 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
794 | 77 | </td> | ||
795 | 78 | </tr> | ||
796 | 79 | </tal:rule_widgets> | ||
797 | 80 | </tal:rule> | ||
798 | 81 | <tal:new-rule | ||
799 | 82 | define="new_rule_widgets python:view.getNewRuleWidgets(); | ||
800 | 83 | parity python:'odd' if len(view.push_rules) % 2 | ||
801 | 84 | else 'even'"> | ||
802 | 85 | <tr tal:attributes="class parity"> | ||
803 | 86 | <td class="push-rule-image-name" | ||
804 | 87 | tal:define="widget nocall:new_rule_widgets/image_name"> | ||
805 | 88 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
806 | 89 | </td> | ||
807 | 90 | <td colspan="5" /> | ||
808 | 91 | </tr> | ||
809 | 92 | <tr tal:attributes="class parity"> | ||
810 | 93 | <td> | ||
811 | 94 | <label> | ||
812 | 95 | <input type="radio" name="field.add_credentials" | ||
813 | 96 | value="existing" checked="checked" /> | ||
814 | 97 | Use existing credentials: | ||
815 | 98 | </label> | ||
816 | 99 | </td> | ||
817 | 100 | <td colspan="4" | ||
818 | 101 | tal:define="widget nocall:new_rule_widgets/existing_credentials"> | ||
819 | 102 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
820 | 103 | </td> | ||
821 | 104 | <td /> | ||
822 | 105 | </tr> | ||
823 | 106 | <tr tal:attributes="class parity"> | ||
824 | 107 | <td> | ||
825 | 108 | <label> | ||
826 | 109 | <input type="radio" name="field.add_credentials" | ||
827 | 110 | value="new" /> | ||
828 | 111 | Add new credentials: | ||
829 | 112 | </label> | ||
830 | 113 | </td> | ||
831 | 114 | <td class="push-rule-url" | ||
832 | 115 | tal:define="widget nocall:new_rule_widgets/url"> | ||
833 | 116 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
834 | 117 | </td> | ||
835 | 118 | <td class="push-rule-username" | ||
836 | 119 | tal:define="widget nocall:new_rule_widgets/username"> | ||
837 | 120 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
838 | 121 | </td> | ||
839 | 122 | <td class="push-rule-password" | ||
840 | 123 | tal:define="widget nocall:new_rule_widgets/password"> | ||
841 | 124 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
842 | 125 | </td> | ||
843 | 126 | <td class="push-rule-confirm-password" | ||
844 | 127 | tal:define="widget nocall:new_rule_widgets/confirm_password"> | ||
845 | 128 | <metal:widget use-macro="context/@@launchpad_form/widget_div" /> | ||
846 | 129 | </td> | ||
847 | 130 | <td /> | ||
848 | 131 | </tr> | ||
849 | 132 | </tal:new-rule> | ||
850 | 133 | </tbody> | ||
851 | 35 | </table> | 134 | </table> |
852 | 36 | </metal:formbody> | 135 | </metal:formbody> |
853 | 37 | </div> | 136 | </div> |
854 | 38 | 137 | ||
855 | 138 | <script type="text/javascript"> | ||
856 | 139 | LPJS.use('lp.oci.ocirecipe.edit', function(Y) { | ||
857 | 140 | Y.on('domready', function(e) { | ||
858 | 141 | Y.lp.oci.ocirecipe.edit.setup(); | ||
859 | 142 | }, window); | ||
860 | 143 | }); | ||
861 | 144 | </script> | ||
862 | 145 | |||
863 | 39 | </div> | 146 | </div> |
864 | 40 | </body> | 147 | </body> |
865 | 41 | </html> | 148 | </html> |
866 | diff --git a/lib/lp/oci/vocabularies.py b/lib/lp/oci/vocabularies.py | |||
867 | index aa1fae3..e29fcd9 100644 | |||
868 | --- a/lib/lp/oci/vocabularies.py | |||
869 | +++ b/lib/lp/oci/vocabularies.py | |||
870 | @@ -8,10 +8,19 @@ from __future__ import absolute_import, print_function, unicode_literals | |||
871 | 8 | __metaclass__ = type | 8 | __metaclass__ = type |
872 | 9 | __all__ = [] | 9 | __all__ = [] |
873 | 10 | 10 | ||
874 | 11 | from six.moves.urllib.parse import ( | ||
875 | 12 | quote, | ||
876 | 13 | unquote, | ||
877 | 14 | ) | ||
878 | 15 | from zope.component import getUtility | ||
879 | 11 | from zope.interface import implementer | 16 | from zope.interface import implementer |
880 | 12 | from zope.schema.vocabulary import SimpleTerm | 17 | from zope.schema.vocabulary import SimpleTerm |
881 | 13 | 18 | ||
882 | 19 | from lp.oci.interfaces.ociregistrycredentials import ( | ||
883 | 20 | IOCIRegistryCredentialsSet, | ||
884 | 21 | ) | ||
885 | 14 | from lp.oci.model.ocirecipe import OCIRecipe | 22 | from lp.oci.model.ocirecipe import OCIRecipe |
886 | 23 | from lp.oci.model.ociregistrycredentials import OCIRegistryCredentials | ||
887 | 15 | from lp.services.webapp.vocabulary import ( | 24 | from lp.services.webapp.vocabulary import ( |
888 | 16 | IHugeVocabulary, | 25 | IHugeVocabulary, |
889 | 17 | StormVocabularyBase, | 26 | StormVocabularyBase, |
890 | @@ -35,6 +44,49 @@ class OCIRecipeDistroArchSeriesVocabulary(StormVocabularyBase): | |||
891 | 35 | return len(self.context.getAllowedArchitectures()) | 44 | return len(self.context.getAllowedArchitectures()) |
892 | 36 | 45 | ||
893 | 37 | 46 | ||
894 | 47 | class OCIRegistryCredentialsVocabulary(StormVocabularyBase): | ||
895 | 48 | |||
896 | 49 | _table = OCIRegistryCredentials | ||
897 | 50 | |||
898 | 51 | def toTerm(self, obj): | ||
899 | 52 | if obj.username: | ||
900 | 53 | token = "%s %s" % (quote(obj.url), quote(obj.username)) | ||
901 | 54 | title = "%s (%s)" % (obj.url, obj.username) | ||
902 | 55 | else: | ||
903 | 56 | token = quote(obj.url) | ||
904 | 57 | title = obj.url | ||
905 | 58 | |||
906 | 59 | return SimpleTerm(obj, token, title) | ||
907 | 60 | |||
908 | 61 | @property | ||
909 | 62 | def _entries(self): | ||
910 | 63 | return list(getUtility( | ||
911 | 64 | IOCIRegistryCredentialsSet).findByOwner(self.context.owner)) | ||
912 | 65 | |||
913 | 66 | def __contains__(self, value): | ||
914 | 67 | """See `IVocabulary`.""" | ||
915 | 68 | return value in self._entries | ||
916 | 69 | |||
917 | 70 | def __len__(self): | ||
918 | 71 | return len(self._entries) | ||
919 | 72 | |||
920 | 73 | def getTermByToken(self, token): | ||
921 | 74 | """See `IVocabularyTokenized`.""" | ||
922 | 75 | try: | ||
923 | 76 | if ' ' in token: | ||
924 | 77 | url, username = token.split(' ', 1) | ||
925 | 78 | url = unquote(url) | ||
926 | 79 | username = unquote(username) | ||
927 | 80 | else: | ||
928 | 81 | username = None | ||
929 | 82 | url = unquote(token) | ||
930 | 83 | for obj in self._entries: | ||
931 | 84 | if obj.url == url and obj.username == username: | ||
932 | 85 | return self.toTerm(obj) | ||
933 | 86 | except ValueError: | ||
934 | 87 | raise LookupError(token) | ||
935 | 88 | |||
936 | 89 | |||
937 | 38 | @implementer(IHugeVocabulary) | 90 | @implementer(IHugeVocabulary) |
938 | 39 | class OCIRecipeVocabulary(StormVocabularyBase): | 91 | class OCIRecipeVocabulary(StormVocabularyBase): |
939 | 40 | """All OCI Recipes of a given OCI project.""" | 92 | """All OCI Recipes of a given OCI project.""" |
940 | diff --git a/lib/lp/oci/vocabularies.zcml b/lib/lp/oci/vocabularies.zcml | |||
941 | index 1a6b75c..24a57d8 100644 | |||
942 | --- a/lib/lp/oci/vocabularies.zcml | |||
943 | +++ b/lib/lp/oci/vocabularies.zcml | |||
944 | @@ -16,6 +16,16 @@ | |||
945 | 16 | </class> | 16 | </class> |
946 | 17 | 17 | ||
947 | 18 | <securedutility | 18 | <securedutility |
948 | 19 | name="OCIRegistryCredentials" | ||
949 | 20 | component="lp.oci.vocabularies.OCIRegistryCredentialsVocabulary" | ||
950 | 21 | provides="zope.schema.interfaces.IVocabularyFactory"> | ||
951 | 22 | <allow interface="zope.schema.interfaces.IVocabularyFactory" /> | ||
952 | 23 | </securedutility> | ||
953 | 24 | |||
954 | 25 | <class class="lp.oci.vocabularies.OCIRegistryCredentialsVocabulary"> | ||
955 | 26 | <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" /> | ||
956 | 27 | </class> | ||
957 | 28 | <securedutility | ||
958 | 19 | name="OCIRecipe" | 29 | name="OCIRecipe" |
959 | 20 | component="lp.oci.vocabularies.OCIRecipeVocabulary" | 30 | component="lp.oci.vocabularies.OCIRecipeVocabulary" |
960 | 21 | provides="zope.schema.interfaces.IVocabularyFactory"> | 31 | provides="zope.schema.interfaces.IVocabularyFactory"> |
LGTM (and nice visual!). I've added just a couple of comments.