launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #26710
[Merge] ~pappacena/launchpad:ocirecipe-edit-info-type-ui into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ocirecipe-edit-info-type-ui into launchpad:master with ~pappacena/launchpad:ocirecipe-privacy-banners-ui as a prerequisite.
Commit message:
Allowing selection of information types
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/399993
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ocirecipe-edit-info-type-ui into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 4f0a3d9..13b1b27 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -56,7 +56,11 @@ from lp.app.browser.tales import (
)
from lp.app.errors import UnexpectedFormData
from lp.app.validators.validation import validate_oci_branch_name
-from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
+from lp.app.vocabularies import InformationTypeVocabulary
+from lp.app.widgets.itemswidgets import (
+ LabeledMultiCheckBoxWidget,
+ LaunchpadRadioWidgetWithDescription,
+ )
from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.code.browser.widgets.gitref import GitRefWidget
from lp.oci.interfaces.ocipushrule import (
@@ -744,6 +748,7 @@ class IOCIRecipeEditSchema(Interface):
use_template(IOCIRecipe, include=[
"name",
"owner",
+ "information_type",
"description",
"git_ref",
"build_file",
@@ -760,6 +765,26 @@ class OCIRecipeFormMixin:
custom_widget_build_args = CustomWidgetFactory(
TextAreaWidget, height=5, width=100)
+ custom_widget_information_type = CustomWidgetFactory(
+ LaunchpadRadioWidgetWithDescription,
+ vocabulary=InformationTypeVocabulary(types=[]))
+
+ def setUpInformationTypeWidget(self):
+ info_type_widget = self.widgets['information_type']
+ info_type_widget.vocabulary = InformationTypeVocabulary(
+ types=self.getInformationTypesToShow())
+
+ def getInformationTypesToShow(self):
+ """Get the information types to display on the edit form.
+
+ We display a customised set of information types: anything allowed
+ by the OCI recipe's model, plus the current type.
+ """
+ allowed_types = set(self.context.getAllowedInformationTypes(self.user))
+ if IOCIRecipe.providedBy(self.context):
+ allowed_types.add(self.context.information_type)
+ return allowed_types
+
def createBuildArgsField(self):
"""Create a form field for OCIRecipe.build_args attribute."""
if IOCIRecipe.providedBy(self.context):
@@ -833,6 +858,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
field_names = (
"name",
"owner",
+ "information_type",
"description",
"git_ref",
"build_file",
@@ -898,6 +924,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
def setUpWidgets(self):
"""See `LaunchpadFormView`."""
super(OCIRecipeAddView, self).setUpWidgets()
+ self.setUpInformationTypeWidget()
self.widgets["processors"].widget_class = "processors"
self.setUpGitRefWidget()
# disable the official recipe button if the user doesn't have
@@ -1017,6 +1044,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
field_names = (
"owner",
"name",
+ "information_type",
"description",
"git_ref",
"build_file",
@@ -1056,6 +1084,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
def setUpWidgets(self):
"""See `LaunchpadFormView`."""
super(OCIRecipeEditView, self).setUpWidgets()
+ self.setUpInformationTypeWidget()
self.setUpGitRefWidget()
# disable the official recipe button if the user doesn't have
# permissions to change it
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 88ec7da..7104610 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -57,6 +57,7 @@ from lp.oci.interfaces.ociregistrycredentials import (
IOCIRegistryCredentialsSet,
)
from lp.oci.tests.helpers import OCIConfigHelperMixin
+from lp.registry.enums import BranchSharingPolicy
from lp.registry.interfaces.person import IPersonSet
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
@@ -231,6 +232,31 @@ class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
"Official recipe:\nNo",
MatchesTagText(content, "official-recipe"))
+ def test_create_new_available_information_types(self):
+ public_pillar = self.factory.makeProduct(owner=self.person)
+ private_pillar = self.factory.makeProduct(
+ owner=self.person,
+ information_type=InformationType.PROPRIETARY,
+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
+ public_oci_project = self.factory.makeOCIProject(
+ registrant=self.person, pillar=public_pillar)
+ private_oci_project = self.factory.makeOCIProject(
+ registrant=self.person, pillar=private_pillar)
+
+ # Public pillar.
+ browser = self.getViewBrowser(
+ public_oci_project, view_name="+new-recipe", user=self.person)
+ self.assertContentEqual(
+ ['PUBLIC', 'PUBLICSECURITY', 'PRIVATESECURITY', 'USERDATA'],
+ browser.getControl(name="field.information_type").options)
+
+ # Proprietary pillar.
+ browser = self.getViewBrowser(
+ private_oci_project, view_name="+new-recipe", user=self.person)
+ self.assertContentEqual(
+ ['PROPRIETARY'],
+ browser.getControl(name="field.information_type").options)
+
def test_create_new_recipe_invalid_format(self):
oci_project = self.factory.makeOCIProject()
[git_ref] = self.factory.makeGitRefs(
@@ -600,12 +626,7 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
for name in disabled])
self.assertThat(processors_control.controls, MatchesSetwise(*matchers))
- def test_edit_private_recipe_shows_banner(self):
- recipe = self.factory.makeOCIRecipe(
- registrant=self.person, owner=self.person,
- information_type=InformationType.USERDATA)
- browser = self.getViewBrowser(recipe, user=self.person)
- browser.getLink("Edit OCI recipe").click()
+ def assertShowsPrivateBanner(self, browser):
banners = find_tags_by_class(
browser.contents, "private_banner_container")
self.assertEqual(1, len(banners))
@@ -613,6 +634,14 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
'The information on this page is private.',
extract_text(banners[0]))
+ def test_edit_private_recipe_shows_banner(self):
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person,
+ information_type=InformationType.USERDATA)
+ browser = self.getViewBrowser(recipe, user=self.person)
+ browser.getLink("Edit OCI recipe").click()
+ self.assertShowsPrivateBanner(browser)
+
def test_edit_recipe(self):
oci_project = self.factory.makeOCIProject()
oci_project_display = oci_project.display_name
@@ -677,6 +706,44 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
browser.getControl("Update OCI recipe").click()
self.assertIn("Branch does not match format", browser.contents)
+ def test_edit_can_make_recipe_private(self):
+ pillar = self.factory.makeProduct(
+ owner=self.person,
+ information_type=InformationType.PUBLIC,
+ branch_sharing_policy=BranchSharingPolicy.PUBLIC_OR_PROPRIETARY)
+ oci_project = self.factory.makeOCIProject(
+ registrant=self.person, pillar=pillar)
+ [git_ref] = self.factory.makeGitRefs(
+ owner=self.person,
+ paths=['/refs/heads/v2.0-20.04'])
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person,
+ oci_project=oci_project, git_ref=git_ref,
+ information_type=InformationType.PUBLIC)
+
+ browser = self.getViewBrowser(recipe, '+edit', user=self.person)
+
+ # Make sure we are showing all available information types:
+ info_type_field = browser.getControl(name="field.information_type")
+ self.assertContentEqual([
+ 'PUBLIC', 'PUBLICSECURITY', 'PRIVATESECURITY', 'USERDATA',
+ 'PROPRIETARY'],
+ info_type_field.options)
+
+ info_type_field.value = InformationType.PROPRIETARY.name
+ browser.getControl("Update OCI recipe").click()
+ self.assertShowsPrivateBanner(browser)
+
+ def test_edit_recipe_on_public_pillar_information_types(self):
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person)
+ browser = self.getViewBrowser(recipe, '+edit', user=self.person)
+
+ info_type_field = browser.getControl(name="field.information_type")
+ self.assertContentEqual(
+ ['PUBLIC', 'PUBLICSECURITY', 'PRIVATESECURITY', 'USERDATA'],
+ info_type_field.options)
+
def test_edit_recipe_sets_date_last_modified(self):
# Editing an OCI recipe sets the date_last_modified property.
date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index c72394e..c8fb968 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -341,6 +341,12 @@ class IOCIRecipeView(Interface):
"""Get an OCIRecipeBuildRequest object for the given job_id.
"""
+ def getAllowedInformationTypes(user):
+ """Get a list of acceptable `InformationType`s for this OCI recipe.
+
+ If the user is a Launchpad admin, any type is acceptable.
+ """
+
def userCanBeSubscribed(user):
"""Checks if a user can be subscribed to the current OCI recipe."""
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 6128452..3b7fff2 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -285,6 +285,10 @@ class OCIRecipe(Storm, WebhookTargetMixin):
reconcile_access_for_artifacts([self], self.information_type,
[self.pillar])
+ def getAllowedInformationTypes(self, user):
+ """See `IOCIRecipe`."""
+ return self.oci_project.getAllowedInformationTypes(user)
+
def visibleByUser(self, user):
"""See `IOCIRecipe`."""
if self.information_type in PUBLIC_INFORMATION_TYPES:
diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt
index 668c33d..fe1c899 100644
--- a/lib/lp/oci/templates/ocirecipe-new.pt
+++ b/lib/lp/oci/templates/ocirecipe-new.pt
@@ -20,6 +20,9 @@
<tal:widget define="widget nocall:view/widgets/owner">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
+ <tal:widget define="widget nocall:view/widgets/information_type">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
<tal:widget define="widget nocall:view/widgets/description">
<metal:block use-macro="context/@@launchpad_form/widget_row" />
</tal:widget>
diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
index 00a6563..bb0a785 100644
--- a/lib/lp/registry/interfaces/ociproject.py
+++ b/lib/lp/registry/interfaces/ociproject.py
@@ -101,6 +101,13 @@ class IOCIProjectView(IHasGitRepositories, Interface):
def getUnofficialRecipes(visible_by_user=None):
"""Gets the unofficial recipes for this OCI project."""
+ def getAllowedInformationTypes(user):
+ """Get a list of acceptable `InformationType`s for for OCI recipes
+ of this OCI project.
+
+ If the user is a Launchpad admin, any type is acceptable.
+ """
+
def getDefaultGitRepository(person):
"""Returns the default git repository for the given user under the
namespace of this OCI project"""
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index 079df1e..e98b3ea 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -32,8 +32,17 @@ from zope.component import getUtility
from zope.interface import implementer
from zope.security.proxy import removeSecurityProxy
+from lp.app.enums import (
+ PRIVATE_INFORMATION_TYPES,
+ PUBLIC_INFORMATION_TYPES,
+ )
+from lp.app.interfaces.services import IService
from lp.bugs.model.bugtarget import BugTargetBase
from lp.code.interfaces.gitnamespace import IGitNamespaceSet
+from lp.code.model.branchnamespace import (
+ BRANCH_POLICY_ALLOWED_TYPES,
+ BRANCH_POLICY_REQUIRED_GRANTS,
+ )
from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.ociproject import (
@@ -43,6 +52,7 @@ from lp.registry.interfaces.ociproject import (
from lp.registry.interfaces.ociprojectname import IOCIProjectNameSet
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.role import IPersonRoles
from lp.registry.interfaces.series import SeriesStatus
from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
from lp.registry.model.ociprojectname import OCIProjectName
@@ -71,6 +81,17 @@ def oci_project_modified(oci_project, event):
removeSecurityProxy(oci_project).date_last_modified = UTC_NOW
+def user_has_special_oci_access(user):
+ """Admins have special access.
+
+ :param user: An `IPerson` or None.
+ """
+ if user is None:
+ return False
+ roles = IPersonRoles(user)
+ return roles.in_admin
+
+
@implementer(IOCIProject)
class OCIProject(BugTargetBase, StormBase):
"""See `IOCIProject` and `IOCIProjectSet`."""
@@ -252,6 +273,22 @@ class OCIProject(BugTargetBase, StormBase):
recipe = removeSecurityProxy(recipe)
recipe._official = status
+ def getAllowedInformationTypes(self, user):
+ """See `IOCIRecipe`."""
+ if user_has_special_oci_access(user):
+ # Admins can set any type.
+ return set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES)
+ required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
+ self.pillar.branch_sharing_policy]
+ if (required_grant is not None
+ and not getUtility(IService, 'sharing').checkPillarAccess(
+ [self.pillar], required_grant, self.registrant)
+ and (user is None
+ or not getUtility(IService, 'sharing').checkPillarAccess(
+ [self.pillar], required_grant, user))):
+ return []
+ return BRANCH_POLICY_ALLOWED_TYPES[self.pillar.branch_sharing_policy]
+
def getDefaultGitRepository(self, person):
namespace = getUtility(IGitNamespaceSet).get(person, oci_project=self)
return namespace.getByName(self.name)
Follow ups