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
diff --git a/lib/lp/oci/interfaces/ocipushrule.py b/lib/lp/oci/interfaces/ocipushrule.py
index b889cdd..45aeb3b 100644
--- a/lib/lp/oci/interfaces/ocipushrule.py
+++ b/lib/lp/oci/interfaces/ocipushrule.py
@@ -127,5 +127,8 @@ class IOCIPushRuleSet(Interface):
127 def new(recipe, registry_credentials, image_name):127 def new(recipe, registry_credentials, image_name):
128 """Create an `IOCIPushRule`."""128 """Create an `IOCIPushRule`."""
129129
130 def findByRegistryCredentials(self, credentials):
131 """Find matching `IOCIPushRule` by credentials."""
132
130 def getByID(id):133 def getByID(id):
131 """Get a single `IOCIPushRule` by its ID."""134 """Get a single `IOCIPushRule` by its ID."""
diff --git a/lib/lp/oci/model/ocipushrule.py b/lib/lp/oci/model/ocipushrule.py
index dbdb22e..0354aad 100644
--- a/lib/lp/oci/model/ocipushrule.py
+++ b/lib/lp/oci/model/ocipushrule.py
@@ -69,7 +69,7 @@ class OCIPushRule(Storm):
6969
70 def destroySelf(self):70 def destroySelf(self):
71 """See `IOCIPushRule`."""71 """See `IOCIPushRule`."""
72 IStore(OCIPushRule).get(self.id).remove()72 IStore(OCIPushRule).remove(self)
7373
7474
75@implementer(IOCIPushRuleSet)75@implementer(IOCIPushRuleSet)
@@ -90,3 +90,9 @@ class OCIPushRuleSet:
90 def getByID(self, id):90 def getByID(self, id):
91 """See `IOCIPushRuleSet`."""91 """See `IOCIPushRuleSet`."""
92 return IStore(OCIPushRule).get(OCIPushRule, id)92 return IStore(OCIPushRule).get(OCIPushRule, id)
93
94 def findByRegistryCredentials(self, credentials):
95 store = IStore(OCIPushRule)
96 return store.find(
97 OCIPushRule,
98 OCIPushRule.registry_credentials == credentials)
diff --git a/lib/lp/oci/model/ociregistrycredentials.py b/lib/lp/oci/model/ociregistrycredentials.py
index 49c740f..d86b140 100644
--- a/lib/lp/oci/model/ociregistrycredentials.py
+++ b/lib/lp/oci/model/ociregistrycredentials.py
@@ -122,11 +122,13 @@ class OCIRegistryCredentials(Storm):
122 def username(self):122 def username(self):
123 return self._credentials.get('username')123 return self._credentials.get('username')
124124
125 @username.setter
126 def username(self, value):
127 self._credentials['username'] = value
128
125 def destroySelf(self):129 def destroySelf(self):
126 """See `IOCIRegistryCredentials`."""130 """See `IOCIRegistryCredentials`."""
127 store = IStore(OCIRegistryCredentials)131 IStore(OCIRegistryCredentials).remove(self)
128 store.find(
129 OCIRegistryCredentials, OCIRegistryCredentials.id == self).remove()
130132
131133
132@implementer(IOCIRegistryCredentialsSet)134@implementer(IOCIRegistryCredentialsSet)
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index b8f00d5..3707a8e 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -1238,6 +1238,12 @@
1238 template="../templates/person-ociregistrycredentials.pt"1238 template="../templates/person-ociregistrycredentials.pt"
1239 />1239 />
1240 <browser:page1240 <browser:page
1241 for="lp.registry.interfaces.person.IPerson"
1242 class="lp.registry.browser.person.PersonEditOCIRegistryCredentialsView"
1243 permission="launchpad.Edit"
1244 name="+edit-oci-registry-credentials"
1245 template="../templates/person-edit-ociregistrycredentials.pt" />
1246 <browser:page
1241 name="+livefs"1247 name="+livefs"
1242 for="lp.registry.interfaces.person.IPerson"1248 for="lp.registry.interfaces.person.IPerson"
1243 class="lp.registry.browser.person.PersonLiveFSView"1249 class="lp.registry.browser.person.PersonLiveFSView"
diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
index 535cf4d..e46cce4 100644
--- a/lib/lp/registry/browser/person.py
+++ b/lib/lp/registry/browser/person.py
@@ -7,6 +7,7 @@ __metaclass__ = type
7__all__ = [7__all__ = [
8 'BeginTeamClaimView',8 'BeginTeamClaimView',
9 'CommonMenuLinks',9 'CommonMenuLinks',
10 'PersonEditOCIRegistryCredentialsView',
10 'EmailToPersonView',11 'EmailToPersonView',
11 'PeopleSearchView',12 'PeopleSearchView',
12 'PersonAccountAdministerView',13 'PersonAccountAdministerView',
@@ -90,7 +91,9 @@ from zope.interface import (
90from zope.interface.exceptions import Invalid91from zope.interface.exceptions import Invalid
91from zope.publisher.interfaces import NotFound92from zope.publisher.interfaces import NotFound
92from zope.schema import (93from zope.schema import (
94 Bool,
93 Choice,95 Choice,
96 Password,
94 Text,97 Text,
95 TextLine,98 TextLine,
96 )99 )
@@ -134,8 +137,10 @@ from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
134from lp.code.errors import InvalidNamespace137from lp.code.errors import InvalidNamespace
135from lp.code.interfaces.branchnamespace import IBranchNamespaceSet138from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
136from lp.code.interfaces.gitlookup import IGitTraverser139from lp.code.interfaces.gitlookup import IGitTraverser
140from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
137from lp.oci.interfaces.ociregistrycredentials import (141from lp.oci.interfaces.ociregistrycredentials import (
138 IOCIRegistryCredentialsSet,142 IOCIRegistryCredentialsSet,
143 OCIRegistryCredentialsAlreadyExist,
139 )144 )
140from lp.registry.browser import BaseRdfView145from lp.registry.browser import BaseRdfView
141from lp.registry.browser.branding import BrandingChangeView146from lp.registry.browser.branding import BrandingChangeView
@@ -3659,6 +3664,299 @@ class PersonOCIRegistryCredentialsView(LaunchpadView):
3659 return len(self.oci_registry_credentials) > 03664 return len(self.oci_registry_credentials) > 0
36603665
36613666
3667class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
3668 """View for Person:+edit-oci-registry-credentials."""
3669
3670 @cachedproperty
3671 def oci_registry_credentials(self):
3672 return list(getUtility(
3673 IOCIRegistryCredentialsSet).findByOwner(self.context))
3674
3675 schema = Interface
3676
3677 def _getFieldName(self, name, credentials_id):
3678 """Get the combined field name for an `OCIRegistryCredentials` ID.
3679
3680 In order to be able to render a table, we encode the credentials ID
3681 in the form field name.
3682 """
3683 return "%s.%d" % (name, credentials_id)
3684
3685 def getEditFieldsRow(self, credentials=None):
3686 id = getattr(credentials, 'id', None)
3687 owner = Choice(
3688 title=u'Owner',
3689 vocabulary=(
3690 'AllUserTeamsParticipationPlusSelfSimpleDisplay'),
3691 default=credentials.owner.name,
3692 __name__=self._getFieldName('owner', id))
3693
3694 username = TextLine(
3695 title=u'Username',
3696 __name__=self._getFieldName('username', id),
3697 default=credentials.username,
3698 required=False, readonly=False)
3699
3700 password = Password(
3701 title=u'Password',
3702 __name__=self._getFieldName('password', id),
3703 default=None,
3704 required=False, readonly=False)
3705
3706 confirm_password = Password(
3707 title=u'Confirm password',
3708 __name__=self._getFieldName('confirm_password', id),
3709 default=None,
3710 required=False, readonly=False)
3711
3712 url = TextLine(
3713 title=u'Registry URL',
3714 __name__=self._getFieldName('url', id),
3715 default=credentials.url,
3716 required=True, readonly=False)
3717
3718 delete = Bool(
3719 title=u'Delete',
3720 __name__=self._getFieldName('delete', id),
3721 default=False,
3722 required=True, readonly=False)
3723
3724 return owner, username, password, confirm_password, url, delete
3725
3726 def getAddFieldsRow(self):
3727 add_url = TextLine(
3728 title=u'Registry URL',
3729 __name__=u'add_url',
3730 required=False, readonly=False)
3731 add_username = TextLine(
3732 title=u'Username',
3733 __name__=u'add_username',
3734 required=False, readonly=False)
3735 add_password = Password(
3736 title=u'Password',
3737 __name__=u'add_password',
3738 required=False, readonly=False)
3739 add_confirm_password = Password(
3740 title=u'Confirm password',
3741 __name__=u'add_confirm_password',
3742 required=False, readonly=False)
3743
3744 return add_url, add_username, add_password, add_confirm_password
3745
3746 def _parseFieldName(self, field_name):
3747 """Parse a combined field name as described in `_getFieldName`.
3748
3749 :raises UnexpectedFormData: if the field name cannot be parsed or
3750 the `OCIRegistryCredentials` cannot be found.
3751 """
3752 field_bits = field_name.split(".")
3753 if len(field_bits) != 2:
3754 raise UnexpectedFormData(
3755 "Cannot parse field name: %s" % field_name)
3756 field_type = field_bits[0]
3757 try:
3758 credentials_id = int(field_bits[1])
3759 except ValueError:
3760 raise UnexpectedFormData(
3761 "Cannot parse field name: %s" % field_name)
3762 return field_type, credentials_id
3763
3764 def setUpFields(self):
3765 """See `LaunchpadFormView`."""
3766 LaunchpadFormView.setUpFields(self)
3767
3768 for elem in self.oci_registry_credentials:
3769 fields = self.getEditFieldsRow(elem)
3770 self.form_fields += FormFields(*fields)
3771
3772 add_fields = self.getAddFieldsRow()
3773 self.form_fields += FormFields(*add_fields)
3774
3775 @property
3776 def label(self):
3777 return 'Edit OCI registry credentials'
3778
3779 @property
3780 def cancel_url(self):
3781 return canonical_url(self.context)
3782
3783 def getCredentialsWidgets(self, credentials):
3784 widgets_by_name = {widget.name: widget for widget in self.widgets}
3785 owner_field_name = (
3786 "field." + self._getFieldName("owner", credentials.id))
3787 username_field_name = (
3788 "field." + self._getFieldName("username", credentials.id))
3789 password_field_name = (
3790 "field." + self._getFieldName("password", credentials.id))
3791 confirm_password_field_name = (
3792 "field." + self._getFieldName("confirm_password",
3793 credentials.id))
3794 url_field_name = "field." + self._getFieldName("url", credentials.id)
3795 delete_field_name = (
3796 "field." + self._getFieldName("delete", credentials.id))
3797 return {
3798 "owner": widgets_by_name[owner_field_name],
3799 "username": widgets_by_name[username_field_name],
3800 "password": widgets_by_name[password_field_name],
3801 "confirm_password": widgets_by_name[confirm_password_field_name],
3802 "url": widgets_by_name[url_field_name],
3803 "delete": widgets_by_name[delete_field_name]
3804 }
3805
3806 def parseData(self, data):
3807 """Rearrange form data to make it easier to process."""
3808 parsed_data = {}
3809 add_url = data["add_url"]
3810 add_username = data["add_username"]
3811 add_password = data["add_password"]
3812 add_confirm_password = data["add_confirm_password"]
3813 if add_url or add_username or add_password or add_confirm_password:
3814 parsed_data.setdefault(None, {
3815 "username": add_username,
3816 "password": add_password,
3817 "confirm_password": add_confirm_password,
3818 "url": add_url,
3819 "action": "add",
3820 })
3821 for field_name in (
3822 name for name in data if name.split(".")[0] == "owner"):
3823 _, credentials_id = self._parseFieldName(field_name)
3824 owner_field_name = self._getFieldName(
3825 "owner", credentials_id)
3826 username_field_name = self._getFieldName(
3827 "username", credentials_id)
3828 password_field_name = self._getFieldName(
3829 "password", credentials_id)
3830 confirm_password_field_name = self._getFieldName(
3831 "confirm_password", credentials_id)
3832 url_field_name = self._getFieldName("url", credentials_id)
3833 delete_field_name = self._getFieldName("delete", credentials_id)
3834 if data.get(delete_field_name):
3835 action = "delete"
3836 else:
3837 action = "change"
3838 parsed_data.setdefault(credentials_id, {
3839 "username": data.get(username_field_name),
3840 "password": data.get(password_field_name),
3841 "confirm_password": data.get(confirm_password_field_name),
3842 "url": data.get(url_field_name),
3843 "owner": data.get(owner_field_name),
3844 "action": action,
3845 })
3846
3847 return parsed_data
3848
3849 def changeCredentials(self, parsed_credentials, credentials):
3850 username = parsed_credentials["username"]
3851 password = parsed_credentials["password"]
3852 confirm_password = parsed_credentials["confirm_password"]
3853 owner = parsed_credentials["owner"]
3854 if password or confirm_password:
3855 if password != confirm_password:
3856 self.setFieldError(
3857 self._getFieldName(
3858 "confirm_password", credentials.id),
3859 "Passwords do not match.")
3860 else:
3861 credentials.setCredentials(
3862 {"username": username,
3863 "password": password})
3864 credentials.url = parsed_credentials["url"]
3865 elif username != credentials.username:
3866 removeSecurityProxy(credentials).username = username
3867 credentials.url = parsed_credentials["url"]
3868 elif parsed_credentials["url"] != credentials.url:
3869 credentials.url = parsed_credentials["url"]
3870 if owner != credentials.owner:
3871 credentials.owner = owner
3872
3873 def deleteCredentials(self, credentials):
3874 push_rule_set = getUtility(IOCIPushRuleSet)
3875 if not push_rule_set.findByRegistryCredentials(
3876 credentials).is_empty():
3877 self.setFieldError(
3878 self._getFieldName(
3879 "delete", credentials.id),
3880 "These credentials cannot be deleted as there are "
3881 "push rules defined that still use them.")
3882 else:
3883 credentials.destroySelf()
3884
3885 def addCredentials(self, parsed_add_credentials):
3886 url = parsed_add_credentials["url"]
3887 password = parsed_add_credentials["password"]
3888 confirm_password = parsed_add_credentials["confirm_password"]
3889 username = parsed_add_credentials["username"]
3890 if url:
3891 if password or confirm_password:
3892 if not password == confirm_password:
3893 self.setFieldError(
3894 "add_password",
3895 "Please make sure the new "
3896 "password matches the "
3897 "confirm password field.")
3898 return
3899
3900 credentials = {
3901 'username': username,
3902 'password': password}
3903 try:
3904 getUtility(IOCIRegistryCredentialsSet).new(
3905 owner=self.context,
3906 url=url,
3907 credentials=credentials)
3908 except OCIRegistryCredentialsAlreadyExist:
3909 self.setFieldError(
3910 "add_url",
3911 "Credentials already exist "
3912 "with the same URL and "
3913 "username.")
3914 else:
3915 credentials = {'username': username}
3916 try:
3917 getUtility(IOCIRegistryCredentialsSet).new(
3918 owner=self.context,
3919 url=url,
3920 credentials=credentials)
3921 except OCIRegistryCredentialsAlreadyExist:
3922 self.setFieldError(
3923 "add_url",
3924 "Credentials already exist "
3925 "with the same URL and username.")
3926 else:
3927 self.setFieldError(
3928 "add_url",
3929 "Registry URL cannot be empty.")
3930
3931 def updateCredentialsFromData(self, parsed_data):
3932 credentials_map = {
3933 credentials.id: credentials
3934 for credentials in self.oci_registry_credentials}
3935
3936 for credentials_id, parsed_credentials in parsed_data.items():
3937 credentials = credentials_map.get(credentials_id)
3938 action = parsed_credentials["action"]
3939
3940 if action == "change":
3941 self.changeCredentials(parsed_credentials, credentials)
3942 elif action == "delete":
3943 self.deleteCredentials(credentials)
3944 elif action == "add":
3945 parsed_add_credentials = parsed_data[credentials]
3946 self.addCredentials(parsed_add_credentials)
3947 else:
3948 raise AssertionError("unknown action: %s" % action)
3949
3950 @action("Save")
3951 def save(self, action, data):
3952 parsed_data = self.parseData(data)
3953 self.updateCredentialsFromData(parsed_data)
3954
3955 if not self.errors:
3956 self.request.response.addNotification("Saved credentials")
3957 self.next_url = canonical_url(self.context)
3958
3959
3662class PersonLiveFSView(LaunchpadView):3960class PersonLiveFSView(LaunchpadView):
3663 """Default view for the list of live filesystems owned by a person."""3961 """Default view for the list of live filesystems owned by a person."""
3664 page_title = 'LiveFS'3962 page_title = 'LiveFS'
diff --git a/lib/lp/registry/browser/tests/test_person.py b/lib/lp/registry/browser/tests/test_person.py
index 1264643..cc35863 100644
--- a/lib/lp/registry/browser/tests/test_person.py
+++ b/lib/lp/registry/browser/tests/test_person.py
@@ -19,6 +19,7 @@ from testtools.matchers import (
19 DocTestMatches,19 DocTestMatches,
20 Equals,20 Equals,
21 LessThan,21 LessThan,
22 MatchesDict,
22 Not,23 Not,
23 )24 )
24from testtools.testcase import ExpectedException25from testtools.testcase import ExpectedException
@@ -34,6 +35,7 @@ from lp.app.errors import NotFoundError
34from lp.app.interfaces.launchpad import ILaunchpadCelebrities35from lp.app.interfaces.launchpad import ILaunchpadCelebrities
35from lp.blueprints.enums import SpecificationImplementationStatus36from lp.blueprints.enums import SpecificationImplementationStatus
36from lp.buildmaster.enums import BuildStatus37from lp.buildmaster.enums import BuildStatus
38from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
37from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE39from lp.oci.interfaces.ocirecipe import OCI_RECIPE_ALLOW_CREATE
38from lp.oci.interfaces.ociregistrycredentials import (40from lp.oci.interfaces.ociregistrycredentials import (
39 IOCIRegistryCredentialsSet,41 IOCIRegistryCredentialsSet,
@@ -53,6 +55,7 @@ from lp.registry.model.karma import KarmaCategory
53from lp.registry.model.milestone import milestone_sort_key55from lp.registry.model.milestone import milestone_sort_key
54from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache56from lp.scripts.garbo import PopulateLatestPersonSourcePackageReleaseCache
55from lp.services.config import config57from lp.services.config import config
58from lp.services.database.interfaces import IStore
56from lp.services.features.testing import FeatureFixture59from lp.services.features.testing import FeatureFixture
57from lp.services.identity.interfaces.account import AccountStatus60from lp.services.identity.interfaces.account import AccountStatus
58from lp.services.identity.interfaces.emailaddress import IEmailAddressSet61from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
@@ -1350,6 +1353,161 @@ class TestPersonOCIRegistryCredentialsView(BrowserTestCase,
1350 view.oci_registry_credentials[0].getCredentials()['username'])1353 view.oci_registry_credentials[0].getCredentials()['username'])
1351 self.assertEqual(url, view.oci_registry_credentials[0].url)1354 self.assertEqual(url, view.oci_registry_credentials[0].url)
13521355
1356 def test_edit_oci_registry_creds_on_person_page(self):
1357 url = unicode(self.factory.getUniqueURL())
1358 newurl = unicode(self.factory.getUniqueURL())
1359 third_url = unicode(self.factory.getUniqueURL())
1360 credentials = {'username': 'foo', 'password': 'bar'}
1361 registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
1362 owner=self.user,
1363 url=url,
1364 credentials=credentials)
1365
1366 browser = self.getViewBrowser(
1367 self.user, view_name='+oci-registry-credentials', user=self.user)
1368 browser.getLink("Edit OCI registry credentials").click()
1369
1370 # Change only the username
1371 registry_credentials_id = removeSecurityProxy(registry_credentials).id
1372 username_control = browser.getControl(
1373 name="field.username.%d" % registry_credentials_id)
1374 username_control.value = 'different_username'
1375 browser.getControl("Save").click()
1376 with person_logged_in(self.user):
1377 self.assertThat(
1378 registry_credentials.getCredentials(),
1379 MatchesDict(
1380 {"username": Equals("different_username"),
1381 "password": Equals("bar")}))
1382
1383 # change only the registry url
1384 browser = self.getViewBrowser(
1385 self.user, view_name='+oci-registry-credentials', user=self.user)
1386 browser.getLink("Edit OCI registry credentials").click()
1387 url_control = browser.getControl(
1388 name="field.url.%d" % registry_credentials_id)
1389 url_control.value = newurl
1390 browser.getControl("Save").click()
1391 with person_logged_in(self.user):
1392 self.assertEqual(newurl, registry_credentials.url)
1393
1394 # change only the password
1395 browser = self.getViewBrowser(
1396 self.user, view_name='+oci-registry-credentials', user=self.user)
1397 browser.getLink("Edit OCI registry credentials").click()
1398 password_control = browser.getControl(
1399 name="field.password.%d" % registry_credentials_id)
1400 password_control.value = 'newpassword'
1401
1402 browser.getControl("Save").click()
1403 self.assertIn("Passwords do not match.", browser.contents)
1404
1405 # change all fields with one edit action
1406 username_control = browser.getControl(
1407 name="field.username.%d" % registry_credentials_id)
1408 username_control.value = 'third_different_username'
1409 url_control = browser.getControl(
1410 name="field.url.%d" % registry_credentials_id)
1411 url_control.value = third_url
1412 password_control = browser.getControl(
1413 name="field.password.%d" % registry_credentials_id)
1414 password_control.value = 'third_newpassword'
1415 confirm_password_control = browser.getControl(
1416 name="field.confirm_password.%d" % registry_credentials_id)
1417 confirm_password_control.value = 'third_newpassword'
1418 browser.getControl("Save").click()
1419 with person_logged_in(self.user):
1420 self.assertThat(
1421 registry_credentials.getCredentials(),
1422 MatchesDict(
1423 {"username": Equals("third_different_username"),
1424 "password": Equals("third_newpassword")}))
1425 self.assertEqual(third_url, registry_credentials.url)
1426
1427 def test_add_oci_registry_creds_on_person_page(self):
1428 url = unicode(self.factory.getUniqueURL())
1429 credentials = {'username': 'foo', 'password': 'bar'}
1430 image_name = self.factory.getUniqueUnicode()
1431 registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
1432 owner=self.user,
1433 url=url,
1434 credentials=credentials)
1435 getUtility(IOCIPushRuleSet).new(
1436 recipe=self.recipe,
1437 registry_credentials=registry_credentials,
1438 image_name=image_name)
1439
1440 browser = self.getViewBrowser(
1441 self.user, view_name='+oci-registry-credentials', user=self.user)
1442 browser.getLink("Edit OCI registry credentials").click()
1443
1444 browser.getControl(name="field.add_url").value = url
1445 browser.getControl(name="field.add_username").value = "new_username"
1446 browser.getControl(name="field.add_password").value = "password"
1447 browser.getControl(
1448 name="field.add_confirm_password").value = "password"
1449 browser.getControl("Save").click()
1450
1451 with person_logged_in(self.user):
1452 creds = list(getUtility(
1453 IOCIRegistryCredentialsSet).findByOwner(
1454 self.user))
1455 self.assertEqual(url, creds[1].url)
1456 self.assertThat(
1457 removeSecurityProxy(creds[1]).getCredentials(),
1458 MatchesDict({"username": Equals("new_username"),
1459 "password": Equals("password")}))
1460
1461 def test_delete_oci_registry_creds_on_person_page(self):
1462 # Test that we do not delete creds when there are
1463 # push rules defined to use them
1464 url = unicode(self.factory.getUniqueURL())
1465 credentials = {'username': 'foo', 'password': 'bar'}
1466 registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
1467 owner=self.person,
1468 url=url,
1469 credentials=credentials)
1470 IStore(registry_credentials).flush()
1471 registry_credentials_id = removeSecurityProxy(registry_credentials).id
1472 image_name = self.factory.getUniqueUnicode()
1473 push_rule = getUtility(IOCIPushRuleSet).new(
1474 recipe=self.recipe,
1475 registry_credentials=registry_credentials,
1476 image_name=image_name)
1477
1478 browser = self.getViewBrowser(
1479 self.person, view_name='+oci-registry-credentials',
1480 user=self.person)
1481 browser.getLink("Edit OCI registry credentials").click()
1482 # assert full rule is displayed
1483 self.assertEqual(url, browser.getControl(
1484 name="field.url.%d" % registry_credentials_id).value)
1485 self.assertEqual(credentials.get('username'), browser.getControl(
1486 name="field.username.%d" % registry_credentials_id).value)
1487
1488 # mark one line of credentials for delete
1489 delete_control = browser.getControl(
1490 name="field.delete.%d" % registry_credentials_id)
1491 delete_control.getControl('Delete').selected = True
1492 browser.getControl("Save").click()
1493 self.assertIn("These credentials cannot be deleted as there are "
1494 "push rules defined that still use them.",
1495 browser.contents)
1496
1497 # make sure we don't have any push rules defined to use
1498 # the credentials we want to remove
1499 with person_logged_in(self.person):
1500 removeSecurityProxy(push_rule).destroySelf()
1501
1502 delete_control = browser.getControl(
1503 name="field.delete.%d" % registry_credentials_id)
1504 delete_control.getControl('Delete').selected = True
1505 browser.getControl("Save").click()
1506 credentials_set = getUtility(IOCIRegistryCredentialsSet)
1507 with person_logged_in(self.person):
1508 self.assertEqual(
1509 0, credentials_set.findByOwner(self.person).count())
1510
13531511
1354class TestPersonLiveFSView(BrowserTestCase):1512class TestPersonLiveFSView(BrowserTestCase):
1355 layer = DatabaseFunctionalLayer1513 layer = DatabaseFunctionalLayer
diff --git a/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt b/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt
1356new file mode 1006441514new file mode 100644
index 0000000..89483da
--- /dev/null
+++ b/lib/lp/registry/templates/person-edit-ociregistrycredentials.pt
@@ -0,0 +1,59 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8<body>
9
10<div metal:fill-slot="main">
11 <div metal:use-macro="context/@@launchpad_form/form">
12 <metal:formbody fill-slot="widgets">
13
14 <table class="form">
15 <tr tal:repeat="credentials view/oci_registry_credentials">
16 <tal:credentials_widgets
17 define="credentials_widgets python:view.getCredentialsWidgets(credentials);
18 parity python:'even' if repeat['credentials'].even() else 'odd'">
19 <td tal:define="widget nocall:credentials_widgets/url">
20 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
21 </td>
22 <td tal:define="widget nocall:credentials_widgets/owner">
23 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
24 </td>
25 <td tal:define="widget nocall:credentials_widgets/username">
26 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
27 </td>
28 <td tal:define="widget nocall:credentials_widgets/password">
29 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
30 </td>
31 <td tal:define="widget nocall:credentials_widgets/confirm_password">
32 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
33 </td>
34 <td tal:define="widget nocall:credentials_widgets/delete">
35 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
36 </td>
37 </tal:credentials_widgets>
38 </tr>
39 <tr>
40 <td tal:define="widget nocall:view/widgets/add_url">
41 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
42 </td>
43 <td tal:define="widget nocall:view/widgets/add_username">
44 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
45 </td>
46 <td tal:define="widget nocall:view/widgets/add_password">
47 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
48 </td>
49 <td tal:define="widget nocall:view/widgets/add_confirm_password">
50 <metal:widget use-macro="context/@@launchpad_form/widget_div" />
51 </td>
52 </tr>
53 </table>
54
55 </metal:formbody>
56 </div>
57</div>
58</body>
59</html>
diff --git a/lib/lp/registry/templates/person-ociregistrycredentials.pt b/lib/lp/registry/templates/person-ociregistrycredentials.pt
index 612056e..bec71e1 100644
--- a/lib/lp/registry/templates/person-ociregistrycredentials.pt
+++ b/lib/lp/registry/templates/person-ociregistrycredentials.pt
@@ -12,6 +12,10 @@
12 <span tal:replace="context/title"/>12 <span tal:replace="context/title"/>
13 has not set any credentials yet.13 has not set any credentials yet.
14 </p>14 </p>
15 <p condition="context/required:launchpad.Edit">
16 <a class="sprite edit" tal:attributes="href context/fmt:url/+edit-oci-registry-credentials">Edit OCI registry credentials</a>
17 </p>
18
15 <table id="oci-credentials" class="listing" tal:condition="view/has_credentials">19 <table id="oci-credentials" class="listing" tal:condition="view/has_credentials">
16 <thead>20 <thead>
17 <tr>21 <tr>

Subscribers

People subscribed via source and target branches

to status/vote changes: