Merge ~ilasc/launchpad:person-oci-registry-credentials-edit into launchpad:master
- Git
- lp:~ilasc/launchpad
- person-oci-registry-credentials-edit
- Merge into master
Status: | Merged |
---|---|
Approved by: | Ioana Lasc |
Approved revision: | e69186d4fb331a4444c411ea9c999bb4e2ec1055 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ilasc/launchpad:person-oci-registry-credentials-edit |
Merge into: | launchpad:master |
Diff against target: |
677 lines (+540/-4) 8 files modified
lib/lp/oci/interfaces/ocipushrule.py (+3/-0) lib/lp/oci/model/ocipushrule.py (+7/-1) lib/lp/oci/model/ociregistrycredentials.py (+5/-3) lib/lp/registry/browser/configure.zcml (+6/-0) lib/lp/registry/browser/person.py (+298/-0) lib/lp/registry/browser/tests/test_person.py (+158/-0) lib/lp/registry/templates/person-edit-ociregistrycredentials.pt (+59/-0) lib/lp/registry/templates/person-ociregistrycredentials.pt (+4/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tom Wardill (community) | Approve | ||
Thiago F. Pappacena (community) | Approve | ||
Review via email: mp+385825@code.launchpad.net |
Commit message
Add edit screen for OCIRegistryCred
Description of the change
Thiago F. Pappacena (pappacena) wrote : | # |
Ioana Lasc (ilasc) wrote : | # |
Thanks Thiago, I added replies to each inline comment and will commit all changes in a few minutes - I agree with all comments and addressed accordingly in the code.
Thiago F. Pappacena (pappacena) wrote : | # |
Added a reply, but other than that it LGTM.
Maybe twom would like to take a quick look here too.
Ioana Lasc (ilasc) wrote : | # |
Thiago that last suggestion was good - agreed and added.
Tom Wardill (twom) wrote : | # |
Some comments inline, mostly indentation and such.
The use of removeSecurityProxy in both the tests and the especially the view context seems a bit strange to me, is it required?
I can't see why you don't have the right privileges to update the objects without it. That said, my understanding of how all that works is a bit hazy!
Ioana Lasc (ilasc) wrote : | # |
Thanks Tom! Indeed there were a couple of places on the view where access to the person object in the context didn't require removeSecurityProxy (setting owner=self.context) but it is necessary everywhere else.
Preview Diff
1 | diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py |
2 | index b889cdd..45aeb3b 100644 |
3 | --- a/lib/lp/oci/interfaces/ocipushrule.py |
4 | +++ b/lib/lp/oci/interfaces/ocipushrule.py |
5 | @@ -127,5 +127,8 @@ class IOCIPushRuleSet(Interface): |
6 | def new(recipe, registry_credentials, image_name): |
7 | """Create an `IOCIPushRule`.""" |
8 | |
9 | + def findByRegistryCredentials(self, credentials): |
10 | + """Find matching `IOCIPushRule` by credentials.""" |
11 | + |
12 | def getByID(id): |
13 | """Get a single `IOCIPushRule` by its ID.""" |
14 | diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py |
15 | index dbdb22e..0354aad 100644 |
16 | --- a/lib/lp/oci/model/ocipushrule.py |
17 | +++ b/lib/lp/oci/model/ocipushrule.py |
18 | @@ -69,7 +69,7 @@ class OCIPushRule(Storm): |
19 | |
20 | def destroySelf(self): |
21 | """See `IOCIPushRule`.""" |
22 | - IStore(OCIPushRule).get(self.id).remove() |
23 | + IStore(OCIPushRule).remove(self) |
24 | |
25 | |
26 | @implementer(IOCIPushRuleSet) |
27 | @@ -90,3 +90,9 @@ class OCIPushRuleSet: |
28 | def getByID(self, id): |
29 | """See `IOCIPushRuleSet`.""" |
30 | return IStore(OCIPushRule).get(OCIPushRule, id) |
31 | + |
32 | + def findByRegistryCredentials(self, credentials): |
33 | + store = IStore(OCIPushRule) |
34 | + return store.find( |
35 | + OCIPushRule, |
36 | + OCIPushRule.registry_credentials == credentials) |
37 | diff --git a/lib/lp/oci/model/ociregistrycredentials.py b/lib/lp/oci/model/ociregistrycredentials.py |
38 | index 49c740f..d86b140 100644 |
39 | --- a/lib/lp/oci/model/ociregistrycredentials.py |
40 | +++ b/lib/lp/oci/model/ociregistrycredentials.py |
41 | @@ -122,11 +122,13 @@ class OCIRegistryCredentials(Storm): |
42 | def username(self): |
43 | return self._credentials.get('username') |
44 | |
45 | + @username.setter |
46 | + def username(self, value): |
47 | + self._credentials['username'] = value |
48 | + |
49 | def destroySelf(self): |
50 | """See `IOCIRegistryCredentials`.""" |
51 | - store = IStore(OCIRegistryCredentials) |
52 | - store.find( |
53 | - OCIRegistryCredentials, OCIRegistryCredentials.id == self).remove() |
54 | + IStore(OCIRegistryCredentials).remove(self) |
55 | |
56 | |
57 | @implementer(IOCIRegistryCredentialsSet) |
58 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
59 | index b8f00d5..3707a8e 100644 |
60 | --- a/lib/lp/registry/browser/configure.zcml |
61 | +++ b/lib/lp/registry/browser/configure.zcml |
62 | @@ -1238,6 +1238,12 @@ |
63 | template="../templates/person-ociregistrycredentials.pt" |
64 | /> |
65 | <browser:page |
66 | + for="lp.registry.interfaces.person.IPerson" |
67 | + class="lp.registry.browser.person.PersonEditOCIRegistryCredentialsView" |
68 | + permission="launchpad.Edit" |
69 | + name="+edit-oci-registry-credentials" |
70 | + template="../templates/person-edit-ociregistrycredentials.pt" /> |
71 | + <browser:page |
72 | name="+livefs" |
73 | for="lp.registry.interfaces.person.IPerson" |
74 | class="lp.registry.browser.person.PersonLiveFSView" |
75 | diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py |
76 | index 535cf4d..e46cce4 100644 |
77 | --- a/lib/lp/registry/browser/person.py |
78 | +++ b/lib/lp/registry/browser/person.py |
79 | @@ -7,6 +7,7 @@ __metaclass__ = type |
80 | __all__ = [ |
81 | 'BeginTeamClaimView', |
82 | 'CommonMenuLinks', |
83 | + 'PersonEditOCIRegistryCredentialsView', |
84 | 'EmailToPersonView', |
85 | 'PeopleSearchView', |
86 | 'PersonAccountAdministerView', |
87 | @@ -90,7 +91,9 @@ from zope.interface import ( |
88 | from zope.interface.exceptions import Invalid |
89 | from zope.publisher.interfaces import NotFound |
90 | from zope.schema import ( |
91 | + Bool, |
92 | Choice, |
93 | + Password, |
94 | Text, |
95 | TextLine, |
96 | ) |
97 | @@ -134,8 +137,10 @@ from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin |
98 | from lp.code.errors import InvalidNamespace |
99 | from lp.code.interfaces.branchnamespace import IBranchNamespaceSet |
100 | from lp.code.interfaces.gitlookup import IGitTraverser |
101 | +from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet |
102 | from lp.oci.interfaces.ociregistrycredentials import ( |
103 | IOCIRegistryCredentialsSet, |
104 | + OCIRegistryCredentialsAlreadyExist, |
105 | ) |
106 | from lp.registry.browser import BaseRdfView |
107 | from lp.registry.browser.branding import BrandingChangeView |
108 | @@ -3659,6 +3664,299 @@ class PersonOCIRegistryCredentialsView(LaunchpadView): |
109 | return len(self.oci_registry_credentials) > 0 |
110 | |
111 | |
112 | +class PersonEditOCIRegistryCredentialsView(LaunchpadFormView): |
113 | + """View for Person:+edit-oci-registry-credentials.""" |
114 | + |
115 | + @cachedproperty |
116 | + def oci_registry_credentials(self): |
117 | + return list(getUtility( |
118 | + IOCIRegistryCredentialsSet).findByOwner(self.context)) |
119 | + |
120 | + schema = Interface |
121 | + |
122 | + def _getFieldName(self, name, credentials_id): |
123 | + """Get the combined field name for an `OCIRegistryCredentials` ID. |
124 | + |
125 | + In order to be able to render a table, we encode the credentials ID |
126 | + in the form field name. |
127 | + """ |
128 | + return "%s.%d" % (name, credentials_id) |
129 | + |
130 | + def getEditFieldsRow(self, credentials=None): |
131 | + id = getattr(credentials, 'id', None) |
132 | + owner = Choice( |
133 | + title=u'Owner', |
134 | + vocabulary=( |
135 | + 'AllUserTeamsParticipationPlusSelfSimpleDisplay'), |
136 | + default=credentials.owner.name, |
137 | + __name__=self._getFieldName('owner', id)) |
138 | + |
139 | + username = TextLine( |
140 | + title=u'Username', |
141 | + __name__=self._getFieldName('username', id), |
142 | + default=credentials.username, |
143 | + required=False, readonly=False) |
144 | + |
145 | + password = Password( |
146 | + title=u'Password', |
147 | + __name__=self._getFieldName('password', id), |
148 | + default=None, |
149 | + required=False, readonly=False) |
150 | + |
151 | + confirm_password = Password( |
152 | + title=u'Confirm password', |
153 | + __name__=self._getFieldName('confirm_password', id), |
154 | + default=None, |
155 | + required=False, readonly=False) |
156 | + |
157 | + url = TextLine( |
158 | + title=u'Registry URL', |
159 | + __name__=self._getFieldName('url', id), |
160 | + default=credentials.url, |
161 | + required=True, readonly=False) |
162 | + |
163 | + delete = Bool( |
164 | + title=u'Delete', |
165 | + __name__=self._getFieldName('delete', id), |
166 | + default=False, |
167 | + required=True, readonly=False) |
168 | + |
169 | + return owner, username, password, confirm_password, url, delete |
170 | + |
171 | + def getAddFieldsRow(self): |
172 | + add_url = TextLine( |
173 | + title=u'Registry URL', |
174 | + __name__=u'add_url', |
175 | + required=False, readonly=False) |
176 | + add_username = TextLine( |
177 | + title=u'Username', |
178 | + __name__=u'add_username', |
179 | + required=False, readonly=False) |
180 | + add_password = Password( |
181 | + title=u'Password', |
182 | + __name__=u'add_password', |
183 | + required=False, readonly=False) |
184 | + add_confirm_password = Password( |
185 | + title=u'Confirm password', |
186 | + __name__=u'add_confirm_password', |
187 | + required=False, readonly=False) |
188 | + |
189 | + return add_url, add_username, add_password, add_confirm_password |
190 | + |
191 | + def _parseFieldName(self, field_name): |
192 | + """Parse a combined field name as described in `_getFieldName`. |
193 | + |
194 | + :raises UnexpectedFormData: if the field name cannot be parsed or |
195 | + the `OCIRegistryCredentials` cannot be found. |
196 | + """ |
197 | + field_bits = field_name.split(".") |
198 | + if len(field_bits) != 2: |
199 | + raise UnexpectedFormData( |
200 | + "Cannot parse field name: %s" % field_name) |
201 | + field_type = field_bits[0] |
202 | + try: |
203 | + credentials_id = int(field_bits[1]) |
204 | + except ValueError: |
205 | + raise UnexpectedFormData( |
206 | + "Cannot parse field name: %s" % field_name) |
207 | + return field_type, credentials_id |
208 | + |
209 | + def setUpFields(self): |
210 | + """See `LaunchpadFormView`.""" |
211 | + LaunchpadFormView.setUpFields(self) |
212 | + |
213 | + for elem in self.oci_registry_credentials: |
214 | + fields = self.getEditFieldsRow(elem) |
215 | + self.form_fields += FormFields(*fields) |
216 | + |
217 | + add_fields = self.getAddFieldsRow() |
218 | + self.form_fields += FormFields(*add_fields) |
219 | + |
220 | + @property |
221 | + def label(self): |
222 | + return 'Edit OCI registry credentials' |
223 | + |
224 | + @property |
225 | + def cancel_url(self): |
226 | + return canonical_url(self.context) |
227 | + |
228 | + def getCredentialsWidgets(self, credentials): |
229 | + widgets_by_name = {widget.name: widget for widget in self.widgets} |
230 | + owner_field_name = ( |
231 | + "field." + self._getFieldName("owner", credentials.id)) |
232 | + username_field_name = ( |
233 | + "field." + self._getFieldName("username", credentials.id)) |
234 | + password_field_name = ( |
235 | + "field." + self._getFieldName("password", credentials.id)) |
236 | + confirm_password_field_name = ( |
237 | + "field." + self._getFieldName("confirm_password", |
238 | + credentials.id)) |
239 | + url_field_name = "field." + self._getFieldName("url", credentials.id) |
240 | + delete_field_name = ( |
241 | + "field." + self._getFieldName("delete", credentials.id)) |
242 | + return { |
243 | + "owner": widgets_by_name[owner_field_name], |
244 | + "username": widgets_by_name[username_field_name], |
245 | + "password": widgets_by_name[password_field_name], |
246 | + "confirm_password": widgets_by_name[confirm_password_field_name], |
247 | + "url": widgets_by_name[url_field_name], |
248 | + "delete": widgets_by_name[delete_field_name] |
249 | + } |
250 | + |
251 | + def parseData(self, data): |
252 | + """Rearrange form data to make it easier to process.""" |
253 | + parsed_data = {} |
254 | + add_url = data["add_url"] |
255 | + add_username = data["add_username"] |
256 | + add_password = data["add_password"] |
257 | + add_confirm_password = data["add_confirm_password"] |
258 | + if add_url or add_username or add_password or add_confirm_password: |
259 | + parsed_data.setdefault(None, { |
260 | + "username": add_username, |
261 | + "password": add_password, |
262 | + "confirm_password": add_confirm_password, |
263 | + "url": add_url, |
264 | + "action": "add", |
265 | + }) |
266 | + for field_name in ( |
267 | + name for name in data if name.split(".")[0] == "owner"): |
268 | + _, credentials_id = self._parseFieldName(field_name) |
269 | + owner_field_name = self._getFieldName( |
270 | + "owner", credentials_id) |
271 | + username_field_name = self._getFieldName( |
272 | + "username", credentials_id) |
273 | + password_field_name = self._getFieldName( |
274 | + "password", credentials_id) |
275 | + confirm_password_field_name = self._getFieldName( |
276 | + "confirm_password", credentials_id) |
277 | + url_field_name = self._getFieldName("url", credentials_id) |
278 | + delete_field_name = self._getFieldName("delete", credentials_id) |
279 | + if data.get(delete_field_name): |
280 | + action = "delete" |
281 | + else: |
282 | + action = "change" |
283 | + parsed_data.setdefault(credentials_id, { |
284 | + "username": data.get(username_field_name), |
285 | + "password": data.get(password_field_name), |
286 | + "confirm_password": data.get(confirm_password_field_name), |
287 | + "url": data.get(url_field_name), |
288 | + "owner": data.get(owner_field_name), |
289 | + "action": action, |
290 | + }) |
291 | + |
292 | + return parsed_data |
293 | + |
294 | + def changeCredentials(self, parsed_credentials, credentials): |
295 | + username = parsed_credentials["username"] |
296 | + password = parsed_credentials["password"] |
297 | + confirm_password = parsed_credentials["confirm_password"] |
298 | + owner = parsed_credentials["owner"] |
299 | + if password or confirm_password: |
300 | + if password != confirm_password: |
301 | + self.setFieldError( |
302 | + self._getFieldName( |
303 | + "confirm_password", credentials.id), |
304 | + "Passwords do not match.") |
305 | + else: |
306 | + credentials.setCredentials( |
307 | + {"username": username, |
308 | + "password": password}) |
309 | + credentials.url = parsed_credentials["url"] |
310 | + elif username != credentials.username: |
311 | + removeSecurityProxy(credentials).username = username |
312 | + credentials.url = parsed_credentials["url"] |
313 | + elif parsed_credentials["url"] != credentials.url: |
314 | + credentials.url = parsed_credentials["url"] |
315 | + if owner != credentials.owner: |
316 | + credentials.owner = owner |
317 | + |
318 | + def deleteCredentials(self, credentials): |
319 | + push_rule_set = getUtility(IOCIPushRuleSet) |
320 | + if not push_rule_set.findByRegistryCredentials( |
321 | + credentials).is_empty(): |
322 | + self.setFieldError( |
323 | + self._getFieldName( |
324 | + "delete", credentials.id), |
325 | + "These credentials cannot be deleted as there are " |
326 | + "push rules defined that still use them.") |
327 | + else: |
328 | + credentials.destroySelf() |
329 | + |
330 | + def addCredentials(self, parsed_add_credentials): |
331 | + url = parsed_add_credentials["url"] |
332 | + password = parsed_add_credentials["password"] |
333 | + confirm_password = parsed_add_credentials["confirm_password"] |
334 | + username = parsed_add_credentials["username"] |
335 | + if url: |
336 | + if password or confirm_password: |
337 | + if not password == confirm_password: |
338 | + self.setFieldError( |
339 | + "add_password", |
340 | + "Please make sure the new " |
341 | + "password matches the " |
342 | + "confirm password field.") |
343 | + return |
344 | + |
345 | + credentials = { |
346 | + 'username': username, |
347 | + 'password': password} |
348 | + try: |
349 | + getUtility(IOCIRegistryCredentialsSet).new( |
350 | + owner=self.context, |
351 | + url=url, |
352 | + credentials=credentials) |
353 | + except OCIRegistryCredentialsAlreadyExist: |
354 | + self.setFieldError( |
355 | + "add_url", |
356 | + "Credentials already exist " |
357 | + "with the same URL and " |
358 | + "username.") |
359 | + else: |
360 | + credentials = {'username': username} |
361 | + try: |
362 | + getUtility(IOCIRegistryCredentialsSet).new( |
363 | + owner=self.context, |
364 | + url=url, |
365 | + credentials=credentials) |
366 | + except OCIRegistryCredentialsAlreadyExist: |
367 | + self.setFieldError( |
368 | + "add_url", |
369 | + "Credentials already exist " |
370 | + "with the same URL and username.") |
371 | + else: |
372 | + self.setFieldError( |
373 | + "add_url", |
374 | + "Registry URL cannot be empty.") |
375 | + |
376 | + def updateCredentialsFromData(self, parsed_data): |
377 | + credentials_map = { |
378 | + credentials.id: credentials |
379 | + for credentials in self.oci_registry_credentials} |
380 | + |
381 | + for credentials_id, parsed_credentials in parsed_data.items(): |
382 | + credentials = credentials_map.get(credentials_id) |
383 | + action = parsed_credentials["action"] |
384 | + |
385 | + if action == "change": |
386 | + self.changeCredentials(parsed_credentials, credentials) |
387 | + elif action == "delete": |
388 | + self.deleteCredentials(credentials) |
389 | + elif action == "add": |
390 | + parsed_add_credentials = parsed_data[credentials] |
391 | + self.addCredentials(parsed_add_credentials) |
392 | + else: |
393 | + raise AssertionError("unknown action: %s" % action) |
394 | + |
395 | + @action("Save") |
396 | + def save(self, action, data): |
397 | + parsed_data = self.parseData(data) |
398 | + self.updateCredentialsFromData(parsed_data) |
399 | + |
400 | + if not self.errors: |
401 | + self.request.response.addNotification("Saved credentials") |
402 | + self.next_url = canonical_url(self.context) |
403 | + |
404 | + |
405 | class PersonLiveFSView(LaunchpadView): |
406 | """Default view for the list of live filesystems owned by a person.""" |
407 | page_title = 'LiveFS' |
408 | diff --git a/lib/lp/registry/browser/tests/test_person.py b/lib/lp/registry/browser/tests/test_person.py |
409 | index 1264643..cc35863 100644 |
410 | --- a/lib/lp/registry/browser/tests/test_person.py |
411 | +++ b/lib/lp/registry/browser/tests/test_person.py |
412 | @@ -19,6 +19,7 @@ from testtools.matchers import ( |
413 | DocTestMatches, |
414 | Equals, |
415 | LessThan, |
416 | + MatchesDict, |
417 | Not, |
418 | ) |
419 | from testtools.testcase import ExpectedException |
420 | @@ -34,6 +35,7 @@ from lp.app.errors import NotFoundError |
421 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
422 | from lp.blueprints.enums import SpecificationImplementationStatus |
423 | from lp.buildmaster.enums import BuildStatus |
424 | +from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet |
425 | from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE |
426 | from lp.oci.interfaces.ociregistrycredentials import ( |
427 | IOCIRegistryCredentialsSet, |
428 | @@ -53,6 +55,7 @@ from lp.registry.model.karma import KarmaCategory |
429 | from lp.registry.model.milestone import milestone_sort_key |
430 | from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache |
431 | from lp.services.config import config |
432 | +from lp.services.database.interfaces import IStore |
433 | from lp.services.features.testing import FeatureFixture |
434 | from lp.services.identity.interfaces.account import AccountStatus |
435 | from lp.services.identity.interfaces.emailaddress import IEmailAddressSet |
436 | @@ -1350,6 +1353,161 @@ class TestPersonOCIRegistryCredentialsView(BrowserTestCase, |
437 | view.oci_registry_credentials[0].getCredentials()['username']) |
438 | self.assertEqual(url, view.oci_registry_credentials[0].url) |
439 | |
440 | + def test_edit_oci_registry_creds_on_person_page(self): |
441 | + url = unicode(self.factory.getUniqueURL()) |
442 | + newurl = unicode(self.factory.getUniqueURL()) |
443 | + third_url = unicode(self.factory.getUniqueURL()) |
444 | + credentials = {'username': 'foo', 'password': 'bar'} |
445 | + registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
446 | + owner=self.user, |
447 | + url=url, |
448 | + credentials=credentials) |
449 | + |
450 | + browser = self.getViewBrowser( |
451 | + self.user, view_name='+oci-registry-credentials', user=self.user) |
452 | + browser.getLink("Edit OCI registry credentials").click() |
453 | + |
454 | + # Change only the username |
455 | + registry_credentials_id = removeSecurityProxy(registry_credentials).id |
456 | + username_control = browser.getControl( |
457 | + name="field.username.%d" % registry_credentials_id) |
458 | + username_control.value = 'different_username' |
459 | + browser.getControl("Save").click() |
460 | + with person_logged_in(self.user): |
461 | + self.assertThat( |
462 | + registry_credentials.getCredentials(), |
463 | + MatchesDict( |
464 | + {"username": Equals("different_username"), |
465 | + "password": Equals("bar")})) |
466 | + |
467 | + # change only the registry url |
468 | + browser = self.getViewBrowser( |
469 | + self.user, view_name='+oci-registry-credentials', user=self.user) |
470 | + browser.getLink("Edit OCI registry credentials").click() |
471 | + url_control = browser.getControl( |
472 | + name="field.url.%d" % registry_credentials_id) |
473 | + url_control.value = newurl |
474 | + browser.getControl("Save").click() |
475 | + with person_logged_in(self.user): |
476 | + self.assertEqual(newurl, registry_credentials.url) |
477 | + |
478 | + # change only the password |
479 | + browser = self.getViewBrowser( |
480 | + self.user, view_name='+oci-registry-credentials', user=self.user) |
481 | + browser.getLink("Edit OCI registry credentials").click() |
482 | + password_control = browser.getControl( |
483 | + name="field.password.%d" % registry_credentials_id) |
484 | + password_control.value = 'newpassword' |
485 | + |
486 | + browser.getControl("Save").click() |
487 | + self.assertIn("Passwords do not match.", browser.contents) |
488 | + |
489 | + # change all fields with one edit action |
490 | + username_control = browser.getControl( |
491 | + name="field.username.%d" % registry_credentials_id) |
492 | + username_control.value = 'third_different_username' |
493 | + url_control = browser.getControl( |
494 | + name="field.url.%d" % registry_credentials_id) |
495 | + url_control.value = third_url |
496 | + password_control = browser.getControl( |
497 | + name="field.password.%d" % registry_credentials_id) |
498 | + password_control.value = 'third_newpassword' |
499 | + confirm_password_control = browser.getControl( |
500 | + name="field.confirm_password.%d" % registry_credentials_id) |
501 | + confirm_password_control.value = 'third_newpassword' |
502 | + browser.getControl("Save").click() |
503 | + with person_logged_in(self.user): |
504 | + self.assertThat( |
505 | + registry_credentials.getCredentials(), |
506 | + MatchesDict( |
507 | + {"username": Equals("third_different_username"), |
508 | + "password": Equals("third_newpassword")})) |
509 | + self.assertEqual(third_url, registry_credentials.url) |
510 | + |
511 | + def test_add_oci_registry_creds_on_person_page(self): |
512 | + url = unicode(self.factory.getUniqueURL()) |
513 | + credentials = {'username': 'foo', 'password': 'bar'} |
514 | + image_name = self.factory.getUniqueUnicode() |
515 | + registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
516 | + owner=self.user, |
517 | + url=url, |
518 | + credentials=credentials) |
519 | + getUtility(IOCIPushRuleSet).new( |
520 | + recipe=self.recipe, |
521 | + registry_credentials=registry_credentials, |
522 | + image_name=image_name) |
523 | + |
524 | + browser = self.getViewBrowser( |
525 | + self.user, view_name='+oci-registry-credentials', user=self.user) |
526 | + browser.getLink("Edit OCI registry credentials").click() |
527 | + |
528 | + browser.getControl(name="field.add_url").value = url |
529 | + browser.getControl(name="field.add_username").value = "new_username" |
530 | + browser.getControl(name="field.add_password").value = "password" |
531 | + browser.getControl( |
532 | + name="field.add_confirm_password").value = "password" |
533 | + browser.getControl("Save").click() |
534 | + |
535 | + with person_logged_in(self.user): |
536 | + creds = list(getUtility( |
537 | + IOCIRegistryCredentialsSet).findByOwner( |
538 | + self.user)) |
539 | + self.assertEqual(url, creds[1].url) |
540 | + self.assertThat( |
541 | + removeSecurityProxy(creds[1]).getCredentials(), |
542 | + MatchesDict({"username": Equals("new_username"), |
543 | + "password": Equals("password")})) |
544 | + |
545 | + def test_delete_oci_registry_creds_on_person_page(self): |
546 | + # Test that we do not delete creds when there are |
547 | + # push rules defined to use them |
548 | + url = unicode(self.factory.getUniqueURL()) |
549 | + credentials = {'username': 'foo', 'password': 'bar'} |
550 | + registry_credentials = getUtility(IOCIRegistryCredentialsSet).new( |
551 | + owner=self.person, |
552 | + url=url, |
553 | + credentials=credentials) |
554 | + IStore(registry_credentials).flush() |
555 | + registry_credentials_id = removeSecurityProxy(registry_credentials).id |
556 | + image_name = self.factory.getUniqueUnicode() |
557 | + push_rule = getUtility(IOCIPushRuleSet).new( |
558 | + recipe=self.recipe, |
559 | + registry_credentials=registry_credentials, |
560 | + image_name=image_name) |
561 | + |
562 | + browser = self.getViewBrowser( |
563 | + self.person, view_name='+oci-registry-credentials', |
564 | + user=self.person) |
565 | + browser.getLink("Edit OCI registry credentials").click() |
566 | + # assert full rule is displayed |
567 | + self.assertEqual(url, browser.getControl( |
568 | + name="field.url.%d" % registry_credentials_id).value) |
569 | + self.assertEqual(credentials.get('username'), browser.getControl( |
570 | + name="field.username.%d" % registry_credentials_id).value) |
571 | + |
572 | + # mark one line of credentials for delete |
573 | + delete_control = browser.getControl( |
574 | + name="field.delete.%d" % registry_credentials_id) |
575 | + delete_control.getControl('Delete').selected = True |
576 | + browser.getControl("Save").click() |
577 | + self.assertIn("These credentials cannot be deleted as there are " |
578 | + "push rules defined that still use them.", |
579 | + browser.contents) |
580 | + |
581 | + # make sure we don't have any push rules defined to use |
582 | + # the credentials we want to remove |
583 | + with person_logged_in(self.person): |
584 | + removeSecurityProxy(push_rule).destroySelf() |
585 | + |
586 | + delete_control = browser.getControl( |
587 | + name="field.delete.%d" % registry_credentials_id) |
588 | + delete_control.getControl('Delete').selected = True |
589 | + browser.getControl("Save").click() |
590 | + credentials_set = getUtility(IOCIRegistryCredentialsSet) |
591 | + with person_logged_in(self.person): |
592 | + self.assertEqual( |
593 | + 0, credentials_set.findByOwner(self.person).count()) |
594 | + |
595 | |
596 | class TestPersonLiveFSView(BrowserTestCase): |
597 | layer = DatabaseFunctionalLayer |
598 | diff --git a/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt b/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt |
599 | new file mode 100644 |
600 | index 0000000..89483da |
601 | --- /dev/null |
602 | +++ b/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt |
603 | @@ -0,0 +1,59 @@ |
604 | +<html |
605 | + xmlns="http://www.w3.org/1999/xhtml" |
606 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
607 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
608 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
609 | + metal:use-macro="view/macro:page/main_only" |
610 | + i18n:domain="launchpad"> |
611 | +<body> |
612 | + |
613 | +<div metal:fill-slot="main"> |
614 | + <div metal:use-macro="context/@@launchpad_form/form"> |
615 | + <metal:formbody fill-slot="widgets"> |
616 | + |
617 | + <table class="form"> |
618 | + <tr tal:repeat="credentials view/oci_registry_credentials"> |
619 | + <tal:credentials_widgets |
620 | + define="credentials_widgets python:view.getCredentialsWidgets(credentials); |
621 | + parity python:'even' if repeat['credentials'].even() else 'odd'"> |
622 | + <td tal:define="widget nocall:credentials_widgets/url"> |
623 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
624 | + </td> |
625 | + <td tal:define="widget nocall:credentials_widgets/owner"> |
626 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
627 | + </td> |
628 | + <td tal:define="widget nocall:credentials_widgets/username"> |
629 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
630 | + </td> |
631 | + <td tal:define="widget nocall:credentials_widgets/password"> |
632 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
633 | + </td> |
634 | + <td tal:define="widget nocall:credentials_widgets/confirm_password"> |
635 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
636 | + </td> |
637 | + <td tal:define="widget nocall:credentials_widgets/delete"> |
638 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
639 | + </td> |
640 | + </tal:credentials_widgets> |
641 | + </tr> |
642 | + <tr> |
643 | + <td tal:define="widget nocall:view/widgets/add_url"> |
644 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
645 | + </td> |
646 | + <td tal:define="widget nocall:view/widgets/add_username"> |
647 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
648 | + </td> |
649 | + <td tal:define="widget nocall:view/widgets/add_password"> |
650 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
651 | + </td> |
652 | + <td tal:define="widget nocall:view/widgets/add_confirm_password"> |
653 | + <metal:widget use-macro="context/@@launchpad_form/widget_div" /> |
654 | + </td> |
655 | + </tr> |
656 | + </table> |
657 | + |
658 | + </metal:formbody> |
659 | + </div> |
660 | +</div> |
661 | +</body> |
662 | +</html> |
663 | diff --git a/lib/lp/registry/templates/person-ociregistrycredentials.pt b/lib/lp/registry/templates/person-ociregistrycredentials.pt |
664 | index 612056e..bec71e1 100644 |
665 | --- a/lib/lp/registry/templates/person-ociregistrycredentials.pt |
666 | +++ b/lib/lp/registry/templates/person-ociregistrycredentials.pt |
667 | @@ -12,6 +12,10 @@ |
668 | <span tal:replace="context/title"/> |
669 | has not set any credentials yet. |
670 | </p> |
671 | + <p condition="context/required:launchpad.Edit"> |
672 | + <a class="sprite edit" tal:attributes="href context/fmt:url/+edit-oci-registry-credentials">Edit OCI registry credentials</a> |
673 | + </p> |
674 | + |
675 | <table id="oci-credentials" class="listing" tal:condition="view/has_credentials"> |
676 | <thead> |
677 | <tr> |
Really good job!
I've just added a few comments, mostly about coding style that could improve a bit code readability.