launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27247
[Merge] ~cjwatson/launchpad:charm-request-builds-ui into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:charm-request-builds-ui into launchpad:master.
Commit message:
Add a basic request-builds view for charm recipes
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/405208
This doesn't yet support architecture selection, but should otherwise be minimally usable.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:charm-request-builds-ui into launchpad:master.
diff --git a/lib/lp/charms/browser/charmrecipe.py b/lib/lp/charms/browser/charmrecipe.py
index 2345ec1..1f6a9d8 100644
--- a/lib/lp/charms/browser/charmrecipe.py
+++ b/lib/lp/charms/browser/charmrecipe.py
@@ -9,10 +9,12 @@ __metaclass__ = type
__all__ = [
"CharmRecipeAddView",
"CharmRecipeAdminView",
+ "CharmRecipeContextMenu",
"CharmRecipeDeleteView",
"CharmRecipeEditView",
"CharmRecipeNavigation",
"CharmRecipeNavigationMenu",
+ "CharmRecipeRequestBuildsView",
"CharmRecipeURL",
"CharmRecipeView",
]
@@ -26,8 +28,13 @@ from zope.interface import (
implementer,
Interface,
)
+from zope.schema import (
+ Dict,
+ TextLine,
+ )
from zope.security.interfaces import Unauthorized
+from lp import _
from lp.app.browser.launchpadform import (
action,
LaunchpadEditFormView,
@@ -52,6 +59,7 @@ from lp.services.propertycache import cachedproperty
from lp.services.utils import seconds_since_epoch
from lp.services.webapp import (
canonical_url,
+ ContextMenu,
enabled_with_permission,
LaunchpadView,
Link,
@@ -139,6 +147,20 @@ class CharmRecipeNavigationMenu(NavigationMenu):
return Link("+delete", "Delete charm recipe", icon="trash-icon")
+class CharmRecipeContextMenu(ContextMenu):
+ """Context menu for charm recipes."""
+
+ usedfor = ICharmRecipe
+
+ facet = "overview"
+
+ links = ("request_builds",)
+
+ @enabled_with_permission("launchpad.Edit")
+ def request_builds(self):
+ return Link("+request-builds", "Request builds", icon="add")
+
+
class CharmRecipeView(LaunchpadView):
"""Default view of a charm recipe."""
@@ -462,3 +484,40 @@ class CharmRecipeDeleteView(BaseCharmRecipeEditView):
owner = self.context.owner
self.context.destroySelf()
self.next_url = canonical_url(owner, view_name="+charm-recipes")
+
+
+class CharmRecipeRequestBuildsView(LaunchpadFormView):
+ """A view for requesting builds of a charm recipe."""
+
+ @property
+ def label(self):
+ return "Request builds for %s" % self.context.name
+
+ page_title = "Request builds"
+
+ class schema(Interface):
+ """Schema for requesting a build."""
+
+ channels = Dict(
+ title="Source snap channels", key_type=TextLine(), required=True,
+ description=ICharmRecipe["auto_build_channels"].description)
+
+ custom_widget_channels = CharmRecipeBuildChannelsWidget
+
+ @property
+ def cancel_url(self):
+ return canonical_url(self.context)
+
+ @property
+ def initial_values(self):
+ """See `LaunchpadFormView`."""
+ return {
+ "channels": self.context.auto_build_channels,
+ }
+
+ @action("Request builds", name="request")
+ def request_action(self, action, data):
+ self.context.requestBuilds(self.user, channels=data["channels"])
+ self.request.response.addNotification(
+ _("Builds will be dispatched soon."))
+ self.next_url = self.cancel_url
diff --git a/lib/lp/charms/browser/configure.zcml b/lib/lp/charms/browser/configure.zcml
index 72be851..868316b 100644
--- a/lib/lp/charms/browser/configure.zcml
+++ b/lib/lp/charms/browser/configure.zcml
@@ -13,7 +13,9 @@
urldata="lp.charms.browser.charmrecipe.CharmRecipeURL" />
<browser:menus
module="lp.charms.browser.charmrecipe"
- classes="CharmRecipeNavigationMenu" />
+ classes="
+ CharmRecipeNavigationMenu
+ CharmRecipeContextMenu" />
<browser:navigation
module="lp.charms.browser.charmrecipe"
classes="CharmRecipeNavigation" />
@@ -44,6 +46,12 @@
permission="launchpad.Edit"
name="+delete"
template="../../app/templates/generic-edit.pt" />
+ <browser:page
+ for="lp.charms.interfaces.charmrecipe.ICharmRecipe"
+ class="lp.charms.browser.charmrecipe.CharmRecipeRequestBuildsView"
+ permission="launchpad.Edit"
+ name="+request-builds"
+ template="../templates/charmrecipe-request-builds.pt" />
<adapter
provides="lp.services.webapp.interfaces.IBreadcrumb"
for="lp.charms.interfaces.charmrecipe.ICharmRecipe"
diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py
index c6130cb..8b95ff8 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipe.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipe.py
@@ -17,7 +17,9 @@ from fixtures import FakeLogger
import pytz
import soupmatchers
from testtools.matchers import (
+ AfterPreprocessing,
Equals,
+ Is,
MatchesListwise,
MatchesStructure,
)
@@ -37,7 +39,10 @@ from lp.charms.browser.charmrecipe import (
CharmRecipeEditView,
CharmRecipeView,
)
-from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
+from lp.charms.interfaces.charmrecipe import (
+ CHARM_RECIPE_ALLOW_CREATE,
+ CharmRecipeBuildRequestStatus,
+ )
from lp.registry.enums import PersonVisibility
from lp.services.database.constants import UTC_NOW
from lp.services.features.testing import FeatureFixture
@@ -65,8 +70,8 @@ from lp.testing.matchers import (
from lp.testing.pages import (
extract_text,
find_main_content,
- find_tags_by_class,
find_tag_by_id,
+ find_tags_by_class,
)
from lp.testing.publication import test_traverse
from lp.testing.views import (
@@ -666,3 +671,78 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
view = create_initialized_view(recipe, "+index")
self.assertEqual(
"track/stable/fix-123, track/edge/fix-123", view.store_channels)
+
+
+class TestCharmRecipeRequestBuildsView(BaseTestCharmRecipeView):
+
+ def setUp(self):
+ super(TestCharmRecipeRequestBuildsView, self).setUp()
+ self.project = self.factory.makeProduct(
+ name="test-project", displayname="Test Project")
+ self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+ self.distroseries = self.factory.makeDistroSeries(
+ distribution=self.ubuntu)
+ self.architectures = []
+ for processor, architecture in ("386", "i386"), ("amd64", "amd64"):
+ das = self.factory.makeDistroArchSeries(
+ distroseries=self.distroseries, architecturetag=architecture,
+ processor=getUtility(IProcessorSet).getByName(processor))
+ das.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
+ self.architectures.append(das)
+ self.recipe = self.factory.makeCharmRecipe(
+ registrant=self.person, owner=self.person, project=self.project,
+ name="charm-name")
+
+ def test_request_builds_page(self):
+ # The +request-builds page is sensible.
+ self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+ Request builds for charm-name
+ Test Project
+ charm-name
+ Request builds
+ Source snap channels:
+ charmcraft
+ core
+ core18
+ core20
+ The channels to use for build tools when building the charm
+ recipe.
+ or
+ Cancel
+ """,
+ self.getMainText(self.recipe, "+request-builds", user=self.person))
+
+ def test_request_builds_not_owner(self):
+ # A user without launchpad.Edit cannot request builds.
+ self.assertRaises(
+ Unauthorized, self.getViewBrowser, self.recipe, "+request-builds")
+
+ def test_request_builds_action(self):
+ # Requesting a build creates a pending build request.
+ browser = self.getViewBrowser(
+ self.recipe, "+request-builds", user=self.person)
+ browser.getControl("Request builds").click()
+
+ login_person(self.person)
+ [request] = self.recipe.pending_build_requests
+ self.assertThat(removeSecurityProxy(request), MatchesStructure(
+ recipe=Equals(self.recipe),
+ status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+ error_message=Is(None),
+ builds=AfterPreprocessing(list, Equals([])),
+ _job=MatchesStructure(
+ requester=Equals(self.person),
+ channels=Equals({}),
+ architectures=Is(None))))
+
+ def test_request_builds_channels(self):
+ # Selecting different channels creates a build request using those
+ # channels.
+ browser = self.getViewBrowser(
+ self.recipe, "+request-builds", user=self.person)
+ browser.getControl(name="field.channels.core").value = "edge"
+ browser.getControl("Request builds").click()
+
+ login_person(self.person)
+ [request] = self.recipe.pending_build_requests
+ self.assertEqual({"core": "edge"}, request.channels)
diff --git a/lib/lp/charms/templates/charmrecipe-index.pt b/lib/lp/charms/templates/charmrecipe-index.pt
index 55b191d..b7da253 100644
--- a/lib/lp/charms/templates/charmrecipe-index.pt
+++ b/lib/lp/charms/templates/charmrecipe-index.pt
@@ -167,6 +167,10 @@
<p tal:condition="not: view/builds_and_requests">
This charm recipe has not been built yet.
</p>
+ <div tal:define="link context/menu:context/request_builds"
+ tal:condition="link/enabled">
+ <tal:request-builds replace="structure link/fmt:link"/>
+ </div>
</div>
</body>
diff --git a/lib/lp/charms/templates/charmrecipe-request-builds.pt b/lib/lp/charms/templates/charmrecipe-request-builds.pt
new file mode 100644
index 0000000..1be5383
--- /dev/null
+++ b/lib/lp/charms/templates/charmrecipe-request-builds.pt
@@ -0,0 +1,23 @@
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="view/macro:page/main_only"
+ i18n:domain="launchpad">
+<body>
+
+<div metal:fill-slot="main">
+ <div metal:use-macro="context/@@launchpad_form/form">
+ <metal:formbody fill-slot="widgets">
+ <table class="form">
+ <tal:widget define="widget nocall:view/widgets/channels">
+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
+ </tal:widget>
+ </table>
+ </metal:formbody>
+ </div>
+</div>
+
+</body>
+</html>