← Back to team overview

launchpad-reviewers team mailing list archive

[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>