launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29685
[Merge] ~lgp171188/launchpad:add-distribution-code-admin-model-and-ui into launchpad:master
Guruprasad has proposed merging ~lgp171188/launchpad:add-distribution-code-admin-model-and-ui into launchpad:master.
Commit message:
Add IDistribution.code_admin
And let the distribution code admin call setDefaultRepository()
on the distribution source packages
LP: #1992500
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1992500 in Launchpad itself: "Permission requirement for setDefaultRepository() is too broad"
https://bugs.launchpad.net/launchpad/+bug/1992500
For more details, see:
https://code.launchpad.net/~lgp171188/launchpad/+git/launchpad/+merge/437481
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/launchpad:add-distribution-code-admin-model-and-ui into launchpad:master.
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index f78c20c..cab022e 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2022 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2023 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
@@ -2347,7 +2347,15 @@ class GitRepositorySet:
"Cannot set a default Git repository for a person, only "
"for a project or a package."
)
- if not check_permission("launchpad.Edit", target):
+ if not (
+ check_permission("launchpad.Edit", target)
+ or (
+ IDistributionSourcePackage.providedBy(target)
+ and getUtility(ILaunchBag).user.inTeam(
+ target.distribution.code_admin
+ )
+ )
+ ):
raise Unauthorized(
"You cannot set the default Git repository for %s."
% target.display_name
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index b21e131..48c1cd0 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2022 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2023 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for Git repositories."""
@@ -5059,6 +5059,26 @@ class TestGitRepositorySet(TestCaseWithFactory):
),
)
+ def test_distribution_code_admin_calls_setDefaultRepository_for_dsp(self):
+ person = self.factory.makePerson()
+ distribution = self.factory.makeDistribution()
+ dsp = self.factory.makeDistributionSourcePackage(
+ distribution=distribution
+ )
+ repo1 = self.factory.makeGitRepository(target=dsp)
+ repo2 = self.factory.makeGitRepository(target=dsp)
+
+ with person_logged_in(distribution.owner):
+ distribution.code_admin = person
+ self.repository_set.setDefaultRepository(dsp, repo1)
+
+ self.assertEqual(repo1, self.repository_set.getDefaultRepository(dsp))
+
+ with person_logged_in(person):
+ self.repository_set.setDefaultRepository(dsp, repo2)
+
+ self.assertEqual(repo2, self.repository_set.getDefaultRepository(dsp))
+
def test_setDefaultRepositoryForOwner_replaces_old(self):
# If another repository is already the target owner default,
# setting it overwrites.
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index fc7b805..6fd29fb 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2022 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2009-2023 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -2342,6 +2342,13 @@
template="../../app/templates/generic-edit.pt"
/>
<browser:page
+ name="+select-code-admins"
+ for="lp.registry.interfaces.distribution.IDistribution"
+ class="lp.registry.browser.distribution.DistributionChangeCodeAdminView"
+ permission="launchpad.Edit"
+ template="../../app/templates/generic-edit.pt"
+ />
+ <browser:page
name="+pubconf"
for="lp.registry.interfaces.distribution.IDistribution"
class="lp.registry.browser.distribution.DistributionPublisherConfigView"
diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
index 7180941..c51d677 100644
--- a/lib/lp/registry/browser/distribution.py
+++ b/lib/lp/registry/browser/distribution.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2023 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Browser views for distributions."""
@@ -9,6 +9,7 @@ __all__ = [
"DistributionArchiveMirrorsRSSView",
"DistributionArchiveMirrorsView",
"DistributionArchivesView",
+ "DistributionChangeCodeAdminView",
"DistributionChangeMembersView",
"DistributionChangeMirrorAdminView",
"DistributionChangeOCIProjectAdminView",
@@ -442,6 +443,7 @@ class DistributionOverviewMenu(ApplicationMenu, DistributionLinksMixin):
"mirror_admin",
"oci_project_admin",
"security_admin",
+ "code_admin",
"reassign",
"addseries",
"series",
@@ -543,6 +545,11 @@ class DistributionOverviewMenu(ApplicationMenu, DistributionLinksMixin):
text = "Change security admins"
return Link("+select-security-admins", text, icon="edit")
+ @enabled_with_permission("launchpad.Edit")
+ def code_admin(self):
+ text = "Change code admins"
+ return Link("+select-code-admins", text, icon="edit")
+
def search(self):
text = "Search packages"
return Link("+search", text, icon="search")
@@ -869,6 +876,26 @@ class DistributionView(PillarViewMixin, HasAnnouncementsView, FeedsMixin):
step_title="Select a new security administrator",
)
+ @property
+ def code_admin_widget(self):
+ if canWrite(self.context, "code_admin"):
+ empty_value = " Specify a code admininstrator"
+ else:
+ empty_value = "None"
+
+ return InlinePersonEditPickerWidget(
+ self.context,
+ IDistribution["code_admin"],
+ format_link(
+ self.context.code_admin,
+ empty_value=empty_value,
+ ),
+ header="Change the code administrator",
+ edit_view="+select-code-admins",
+ null_display_value=empty_value,
+ step_title="Select a new code administrator",
+ )
+
def linkedMilestonesForSeries(self, series):
"""Return a string of linkified milestones in the series."""
# Listify to remove repeated queries.
@@ -1350,6 +1377,18 @@ class DistributionChangeSecurityAdminView(RegistryEditFormView):
)
+class DistributionChangeCodeAdminView(RegistryEditFormView):
+ """A view to change the code administrator."""
+
+ schema = IDistribution
+ field_names = ["code_admin"]
+
+ @property
+ def label(self):
+ """See `LaunchpadFormView`."""
+ return "Change the %s code administrator" % (self.context.displayname)
+
+
class DistributionChangeMembersView(RegistryEditFormView):
"""A view to change the members team."""
diff --git a/lib/lp/registry/browser/tests/distribution-views.rst b/lib/lp/registry/browser/tests/distribution-views.rst
index c4d47a8..2edfb50 100644
--- a/lib/lp/registry/browser/tests/distribution-views.rst
+++ b/lib/lp/registry/browser/tests/distribution-views.rst
@@ -316,6 +316,56 @@ Only admins and owners can access the view.
False
+Changing a distribution code administrator
+------------------------------------------
+
+The +select-code-admins view allows the owner or admin to change the
+code administrator.
+
+ >>> login("admin@xxxxxxxxxxxxx")
+ >>> view = create_view(distribution, "+select-code-admins")
+ >>> print(view.label)
+ Change the YoUbuntu code administrator
+
+ >>> print(view.page_title)
+ Change the YoUbuntu code administrator
+
+ >>> print(view.cancel_url)
+ http://launchpad.test/youbuntu
+
+The view accepts the code_admin field.
+
+ >>> print(distribution.code_admin)
+ None
+
+ >>> view.field_names
+ ['code_admin']
+
+ >>> form = {
+ ... "field.code_admin": "no-priv",
+ ... "field.actions.change": "Change",
+ ... }
+ >>> view = create_initialized_view(
+ ... distribution, "+select-code-admins", form=form
+ ... )
+ >>> view.errors
+ []
+ >>> print(view.next_url)
+ http://launchpad.test/youbuntu
+
+ >>> print(distribution.code_admin.name)
+ no-priv
+
+Only admins and owners can access the view.
+
+ >>> check_permission("launchpad.Edit", view)
+ True
+
+ >>> login("no-priv@xxxxxxxxxxxxx")
+ >>> check_permission("launchpad.Edit", view)
+ False
+
+
Changing a distribution members team
------------------------------------
diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
index 9da6880..22178f0 100644
--- a/lib/lp/registry/configure.zcml
+++ b/lib/lp/registry/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2009-2023 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -1850,6 +1850,7 @@
set_attributes="
bug_reported_acknowledgement
bug_reporting_guidelines
+ code_admin
default_traversal_policy
description
development_series_alias
diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
index 2547403..2d3fcef 100644
--- a/lib/lp/registry/interfaces/distribution.py
+++ b/lib/lp/registry/interfaces/distribution.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2023 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interfaces including and related to IDistribution."""
@@ -688,6 +688,16 @@ class IDistributionView(
)
)
+ code_admin = exported(
+ ReferenceChoice(
+ title=_("Code Administrator"),
+ description=_("The distribution source code administrator."),
+ required=False,
+ vocabulary="ValidPersonOrTeam",
+ schema=IPerson,
+ ),
+ )
+
def getVulnerabilitiesVisibleToUser(user):
"""Return the vulnerabilities visible to the given user."""
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index 80433cc..f8816c4 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2023 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Database classes for implementing distribution items."""
@@ -773,6 +773,13 @@ class Distribution(
notNull=False,
default=None,
)
+ code_admin = ForeignKey(
+ dbName="code_admin",
+ foreignKey="Person",
+ storm_validator=validate_public_person,
+ notNull=False,
+ default=None,
+ )
@cachedproperty
def main_archive(self):
diff --git a/lib/lp/registry/stories/webservice/xx-distribution.rst b/lib/lp/registry/stories/webservice/xx-distribution.rst
index 79a1450..de764b4 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution.rst
+++ b/lib/lp/registry/stories/webservice/xx-distribution.rst
@@ -29,6 +29,7 @@ And for every distribution we publish most of its attributes.
bug_reporting_guidelines: None
bug_supervisor_link: None
cdimage_mirrors_collection_link: 'http://.../ubuntu/cdimage_mirrors'
+ code_admin_link: None
commercial_subscription_is_due: False
commercial_subscription_link: None
current_series_link: 'http://.../ubuntu/hoary'
diff --git a/lib/lp/registry/templates/distribution-details.pt b/lib/lp/registry/templates/distribution-details.pt
index 434989b..cc4a419 100644
--- a/lib/lp/registry/templates/distribution-details.pt
+++ b/lib/lp/registry/templates/distribution-details.pt
@@ -36,6 +36,11 @@
<dt>Security admins:</dt>
<dd tal:content="structure view/security_admin_widget" />
</dl>
+
+ <dl id="code-admins">
+ <dt>Code admins:</dt>
+ <dd tal:content="structure view/code_admin_widget" />
+ </dl>
</div>
<dl id="uploaders" style="clear:left">
diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py
index 96f9558..d9c3b1e 100644
--- a/lib/lp/registry/tests/test_distribution.py
+++ b/lib/lp/registry/tests/test_distribution.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2022 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2023 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for Distribution."""
@@ -2146,6 +2146,100 @@ class TestDistributionWebservice(OCIConfigHelperMixin, TestCaseWithFactory):
)
self.assertEqual(401, response.status)
+ def test_distribution_code_admin_unset(self):
+ with person_logged_in(self.person):
+ distro = self.factory.makeDistribution(owner=self.person)
+ distro_url = api_url(distro)
+
+ response = self.webservice.get(distro_url)
+ json_body = response.jsonBody()
+ self.assertEqual(200, response.status)
+ self.assertIsNone(json_body["code_admin_link"])
+
+ def test_distribution_code_admin_set(self):
+ with person_logged_in(self.person):
+ distro = self.factory.makeDistribution(owner=self.person)
+ distro_url = api_url(distro)
+ person = self.factory.makePerson()
+ distro.code_admin = person
+ person_url = "http://api.launchpad.test/devel" + api_url(person)
+
+ response = self.webservice.get(distro_url)
+ json_body = response.jsonBody()
+ self.assertEqual(200, response.status)
+ self.assertEqual(
+ person_url,
+ json_body["code_admin_link"],
+ )
+
+ def test_admin_can_set_distribution_code_admin(self):
+ with admin_logged_in():
+ distro = self.factory.makeDistribution()
+ person = self.factory.makePerson()
+ person_url = "http://api.launchpad.test/devel" + api_url(person)
+ self.assertIsNone(distro.code_admin)
+ admin_user = getUtility(IPersonSet).getByEmail(
+ "admin@xxxxxxxxxxxxx"
+ )
+ distro_url = api_url(distro)
+
+ webservice = webservice_for_person(
+ admin_user,
+ permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel",
+ )
+
+ response = webservice.patch(
+ distro_url,
+ "application/json",
+ json.dumps({"code_admin_link": person_url}),
+ )
+ self.assertEqual(209, response.status)
+ with admin_logged_in():
+ self.assertEqual(person, distro.code_admin)
+
+ def test_distribution_owner_can_set_code_admin(self):
+ with person_logged_in(self.person):
+ distro = self.factory.makeDistribution(owner=self.person)
+ self.assertIsNone(distro.code_admin)
+ distro_url = api_url(distro)
+ person_url = "http://api.launchpad.test/devel" + api_url(
+ self.person
+ )
+
+ response = self.webservice.patch(
+ distro_url,
+ "application/json",
+ json.dumps(
+ {
+ "code_admin_link": person_url,
+ }
+ ),
+ )
+ self.assertEqual(209, response.status)
+
+ with person_logged_in(self.person):
+ self.assertEqual(self.person, distro.code_admin)
+
+ def test_others_cannot_set_distribution_code_admin(self):
+ with person_logged_in(self.person):
+ distro = self.factory.makeDistribution()
+ distro_url = api_url(distro)
+ person_url = "http://api.launchpad.test/devel" + api_url(
+ self.person
+ )
+
+ response = self.webservice.patch(
+ distro_url,
+ "application/json",
+ json.dumps(
+ {
+ "code_admin_link": person_url,
+ }
+ ),
+ )
+ self.assertEqual(401, response.status)
+
class TestDistributionVulnerabilities(TestCaseWithFactory):
@@ -2346,6 +2440,26 @@ class TestDistributionVulnerabilities(TestCaseWithFactory):
),
)
+ def test_set_code_admin_permissions(self):
+ distribution = self.factory.makeDistribution()
+ person = self.factory.makePerson()
+ person2 = self.factory.makePerson()
+ code_admin_team = self.factory.makeTeam(members=[person])
+
+ with person_logged_in(distribution.owner):
+ distribution.code_admin = code_admin_team
+ self.assertEqual(code_admin_team, distribution.code_admin)
+
+ with admin_logged_in():
+ distribution.code_admin = None
+ self.assertIsNone(distribution.code_admin)
+
+ with person_logged_in(person2), ExpectedException(Unauthorized):
+ distribution.code_admin = code_admin_team
+
+ with anonymous_logged_in(), ExpectedException(Unauthorized):
+ distribution.code_admin = code_admin_team
+
class TestDistributionVulnerabilitiesWebService(TestCaseWithFactory):
Follow ups