Merge ~ilasc/launchpad:person-oci-registry-credentials-edit into launchpad:master

Proposed by Ioana Lasc
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)
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 OCIRegistryCredentials on person page

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Really good job!

I've just added a few comments, mostly about coding style that could improve a bit code readability.

Revision history for this message
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.

Revision history for this message
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.

review: Approve
Revision history for this message
Ioana Lasc (ilasc) wrote :

Thiago that last suggestion was good - agreed and added.

Revision history for this message
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!

review: Needs Information
Revision history for this message
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.

Revision history for this message
Tom Wardill (twom) wrote :

Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py
2index 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."""
14diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
15index 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)
37diff --git a/lib/lp/oci/model/ociregistrycredentials.py b/lib/lp/oci/model/ociregistrycredentials.py
38index 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)
58diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
59index 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"
75diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
76index 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'
408diff --git a/lib/lp/registry/browser/tests/test_person.py b/lib/lp/registry/browser/tests/test_person.py
409index 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
598diff --git a/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt b/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt
599new file mode 100644
600index 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>
663diff --git a/lib/lp/registry/templates/person-ociregistrycredentials.pt b/lib/lp/registry/templates/person-ociregistrycredentials.pt
664index 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>

Subscribers

People subscribed via source and target branches

to status/vote changes: