← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rockstar/launchpad/spr-admin into lp:launchpad/devel

 

Paul Hummer has proposed merging lp:~rockstar/launchpad/spr-admin into lp:launchpad/devel with lp:~rockstar/launchpad/bug-602333 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


This branch makes a link available on source package recipe builds that allows admins and bazaar experts to delete the build.  Julian isn't really thrilled about this change, but we're still in beta, we need to be better at getting rid of broken builds, so, yeah, that's why we did this.  Patch is (hopefully) pretty self explanatory.
-- 
https://code.launchpad.net/~rockstar/launchpad/spr-admin/+merge/29780
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rockstar/launchpad/spr-admin into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py	2010-07-08 08:01:07 +0000
+++ lib/canonical/launchpad/security.py	2010-07-13 10:03:29 +0000
@@ -1142,6 +1142,15 @@
     usedfor = ICodeImportMachine
 
 
+class DeleteSourcePackageRecipeBuilds(OnlyBazaarExpertsAndAdmins):
+    """Control who can delete SourcePackageRecipeBuilds.
+
+    Access is restricted to members of ~bazaar-experts and Launchpad admins.
+    """
+    permission = 'launchpad.Edit'
+    usedfor = ISourcePackageRecipeBuild
+
+
 class AdminDistributionTranslations(AuthorizationBase):
     """Class for deciding who can administer distribution translations.
 

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2010-07-09 10:22:32 +0000
+++ lib/lp/code/browser/configure.zcml	2010-07-13 10:03:29 +0000
@@ -1083,11 +1083,16 @@
         attribute_to_parent="recipe"
         path_expression="string:+build/${id}"
         rootsite="code" />
+    <browser:menus
+        classes="SourcePackageRecipeBuildContextMenu"
+        module="lp.code.browser.sourcepackagerecipebuild"/>
 
     <browser:navigation
         module="lp.code.browser.sourcepackagerecipe"
-        classes="SourcePackageRecipeNavigation
-        SourcePackageRecipeBuildNavigation" />
+        classes="SourcePackageRecipeNavigation" />
+    <browser:navigation
+        module="lp.code.browser.sourcepackagerecipebuild"
+        classes="SourcePackageRecipeBuildNavigation" />
 
     <facet facet="branches">
 
@@ -1115,10 +1120,16 @@
             layer="canonical.launchpad.layers.CodeLayer"/>
         <browser:page
             for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"
-            class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeBuildView"
+            class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildView"
             name="+index"
             template="../templates/sourcepackagerecipebuild-index.pt"
             permission="launchpad.View"/>
+        <browser:page
+            for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"
+            class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildDeleteView"
+            name="+delete"
+            template="../../app/templates/generic-edit.pt"
+            permission="launchpad.View"/>
         <browser:menus
             classes="
                 SourcePackageRecipeNavigationMenu

=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py	2010-07-02 01:12:15 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py	2010-07-13 10:03:29 +0000
@@ -7,7 +7,6 @@
 
 __all__ = [
     'SourcePackageRecipeAddView',
-    'SourcePackageRecipeBuildView',
     'SourcePackageRecipeContextMenu',
     'SourcePackageRecipeEditView',
     'SourcePackageRecipeNavigationMenu',
@@ -29,7 +28,6 @@
 
 from canonical.database.constants import UTC_NOW
 from canonical.launchpad.browser.launchpad import Hierarchy
-from canonical.launchpad.browser.librarian import FileNavigationMixin
 from canonical.launchpad.interfaces import ILaunchBag
 from canonical.launchpad.webapp import (
     action, canonical_url, ContextMenu, custom_widget,
@@ -39,18 +37,23 @@
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from canonical.launchpad.webapp.sorting import sorted_dotted_numbers
 from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget
+<<<<<<< TREE
 from lp.buildmaster.interfaces.buildbase import BuildStatus
 from lp.code.errors import BuildAlreadyPending, ForbiddenInstruction
+=======
+from lp.code.errors import ForbiddenInstruction
+>>>>>>> MERGE-SOURCE
 from lp.code.interfaces.branch import NoSuchBranch
 from lp.code.interfaces.sourcepackagerecipe import (
     ISourcePackageRecipe, ISourcePackageRecipeSource, MINIMAL_RECIPE_TEXT)
 from lp.code.interfaces.sourcepackagerecipebuild import (
-    ISourcePackageRecipeBuild, ISourcePackageRecipeBuildSource)
+    ISourcePackageRecipeBuildSource)
 from lp.soyuz.browser.archive import make_archive_vocabulary
 from lp.soyuz.interfaces.archive import (
     IArchiveSet)
 from lp.registry.interfaces.distroseries import IDistroSeriesSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+<<<<<<< TREE
 from lp.services.job.interfaces.job import JobStatus
 
 RECIPE_BETA_MESSAGE = structured(
@@ -60,6 +63,8 @@
     '<a href="http://bugs.edge.launchpad.net/launchpad-code";>'
     'file a bug</a>.  We\'ll be happy to fix any problems you encounter.')
 
+=======
+>>>>>>> MERGE-SOURCE
 
 class IRecipesForPerson(Interface):
     """A marker interface for source package recipe sets."""
@@ -193,6 +198,7 @@
     terms.reverse()
     return SimpleVocabulary(terms)
 
+
 def target_ppas_vocabulary(context):
     """Return a vocabulary of ppas that the current user can target."""
     ppas = getUtility(IArchiveSet).getPPAsForUser(getUtility(ILaunchBag).user)
@@ -264,69 +270,6 @@
 
 
 
-class SourcePackageRecipeBuildNavigation(Navigation, FileNavigationMixin):
-
-    usedfor = ISourcePackageRecipeBuild
-
-
-class SourcePackageRecipeBuildView(LaunchpadView):
-    """Default view of a SourcePackageRecipeBuild."""
-
-    @property
-    def status(self):
-        """A human-friendly status string."""
-        if (self.context.buildstate == BuildStatus.NEEDSBUILD
-            and self.eta is None):
-            return 'No suitable builders'
-        return {
-            BuildStatus.NEEDSBUILD: 'Pending build',
-            BuildStatus.FULLYBUILT: 'Successful build',
-            BuildStatus.MANUALDEPWAIT: (
-                'Could not build because of missing dependencies'),
-            BuildStatus.CHROOTWAIT: (
-                'Could not build because of chroot problem'),
-            BuildStatus.SUPERSEDED: (
-                'Could not build because source package was superseded'),
-            BuildStatus.FAILEDTOUPLOAD: 'Could not be uploaded correctly',
-            }.get(self.context.buildstate, self.context.buildstate.title)
-
-    @property
-    def eta(self):
-        """The datetime when the build job is estimated to complete.
-
-        This is the BuildQueue.estimated_duration plus the
-        Job.date_started or BuildQueue.getEstimatedJobStartTime.
-        """
-        if self.context.buildqueue_record is None:
-            return None
-        queue_record = self.context.buildqueue_record
-        if queue_record.job.status == JobStatus.WAITING:
-            start_time = queue_record.getEstimatedJobStartTime()
-            if start_time is None:
-                return None
-        else:
-            start_time = queue_record.job.date_started
-        duration = queue_record.estimated_duration
-        return start_time + duration
-
-    @property
-    def date(self):
-        """The date when the build completed or is estimated to complete."""
-        if self.estimate:
-            return self.eta
-        return self.context.datebuilt
-
-    @property
-    def estimate(self):
-        """If true, the date value is an estimate."""
-        if self.context.datebuilt is not None:
-            return False
-        return self.eta is not None
-
-    def binary_builds(self):
-        return list(self.context.binary_builds)
-
-
 class ISourcePackageAddEditSchema(Interface):
     """Schema for adding or editing a recipe."""
 

=== added file 'lib/lp/code/browser/sourcepackagerecipebuild.py'
--- lib/lp/code/browser/sourcepackagerecipebuild.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/sourcepackagerecipebuild.py	2010-07-13 10:03:29 +0000
@@ -0,0 +1,120 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""SourcePackageRecipeBuild views."""
+
+__metaclass__ = type
+
+__all__ = [
+    'SourcePackageRecipeBuildContextMenu',
+    'SourcePackageRecipeBuildNavigation',
+    'SourcePackageRecipeBuildView',
+    ]
+
+from zope.interface import Interface
+
+from canonical.launchpad.browser.librarian import FileNavigationMixin
+from canonical.launchpad.webapp import (
+    action, canonical_url, ContextMenu, enabled_with_permission,
+    LaunchpadView, LaunchpadFormView, Link, Navigation)
+
+from lp.buildmaster.interfaces.buildbase import BuildStatus
+from lp.code.interfaces.sourcepackagerecipebuild import (
+    ISourcePackageRecipeBuild)
+from lp.services.job.interfaces.job import JobStatus
+
+
+class SourcePackageRecipeBuildNavigation(Navigation, FileNavigationMixin):
+
+    usedfor = ISourcePackageRecipeBuild
+
+
+class SourcePackageRecipeBuildContextMenu(ContextMenu):
+    """Navigation menu for sourcepackagerecipe build."""
+
+    usedfor = ISourcePackageRecipeBuild
+
+    facet = 'branches'
+
+    links = ('delete',)
+
+    @enabled_with_permission('launchpad.Edit')
+    def delete(self):
+        return Link('+delete', 'Delete build', icon='trash-icon')
+
+
+class SourcePackageRecipeBuildView(LaunchpadView):
+    """Default view of a SourcePackageRecipeBuild."""
+
+    @property
+    def status(self):
+        """A human-friendly status string."""
+        if (self.context.buildstate == BuildStatus.NEEDSBUILD
+            and self.eta is None):
+            return 'No suitable builders'
+        return {
+            BuildStatus.NEEDSBUILD: 'Pending build',
+            BuildStatus.FULLYBUILT: 'Successful build',
+            BuildStatus.MANUALDEPWAIT: (
+                'Could not build because of missing dependencies'),
+            BuildStatus.CHROOTWAIT: (
+                'Could not build because of chroot problem'),
+            BuildStatus.SUPERSEDED: (
+                'Could not build because source package was superseded'),
+            BuildStatus.FAILEDTOUPLOAD: 'Could not be uploaded correctly',
+            }.get(self.context.buildstate, self.context.buildstate.title)
+
+    @property
+    def eta(self):
+        """The datetime when the build job is estimated to complete.
+
+        This is the BuildQueue.estimated_duration plus the
+        Job.date_started or BuildQueue.getEstimatedJobStartTime.
+        """
+        if self.context.buildqueue_record is None:
+            return None
+        queue_record = self.context.buildqueue_record
+        if queue_record.job.status == JobStatus.WAITING:
+            start_time = queue_record.getEstimatedJobStartTime()
+            if start_time is None:
+                return None
+        else:
+            start_time = queue_record.job.date_started
+        duration = queue_record.estimated_duration
+        return start_time + duration
+
+    @property
+    def date(self):
+        """The date when the build completed or is estimated to complete."""
+        if self.estimate:
+            return self.eta
+        return self.context.datebuilt
+
+    @property
+    def estimate(self):
+        """If true, the date value is an estimate."""
+        if self.context.datebuilt is not None:
+            return False
+        return self.eta is not None
+
+    def binary_builds(self):
+        return list(self.context.binary_builds)
+
+
+class SourcePackageRecipeBuildDeleteView(LaunchpadFormView):
+    """Default view of a SourcePackageRecipeBuild."""
+
+    class schema(Interface):
+        """Schema for deleting a build."""
+
+    page_title = label = "Delete"
+
+    @property
+    def cancel_url(self):
+        return canonical_url(self.context)
+
+    @action('Delete build', name='delete')
+    def request_action(self, action, data):
+        """Delete the build."""
+        self.next_url = canonical_url(self.context.recipe)
+        self.context.destroySelf()

=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2010-07-02 11:11:32 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2010-07-13 10:03:29 +0000
@@ -21,9 +21,9 @@
     DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
 from lp.buildmaster.interfaces.buildbase import BuildStatus
 from lp.code.browser.sourcepackagerecipe import (
-    SourcePackageRecipeView, SourcePackageRecipeRequestBuildsView,
-    SourcePackageRecipeBuildView
-)
+    SourcePackageRecipeView, SourcePackageRecipeRequestBuildsView)
+from lp.code.browser.sourcepackagerecipebuild import (
+    SourcePackageRecipeBuildView)
 from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.soyuz.model.processor import ProcessorFamily

=== added file 'lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py	2010-07-13 10:03:29 +0000
@@ -0,0 +1,90 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+# pylint: disable-msg=F0401,E1002
+
+"""Tests for the source package recipe view classes and templates."""
+
+__metaclass__ = type
+
+from mechanize import LinkNotFoundError
+import transaction
+from zope.component import getUtility
+
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.webapp import canonical_url
+from canonical.testing import DatabaseFunctionalLayer
+from lp.soyuz.model.processor import ProcessorFamily
+from lp.testing import BrowserTestCase, logout
+
+
+class TestSourcePackageRecipeBuild(BrowserTestCase):
+    """Create some sample data for recipe tests."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        """Provide useful defaults."""
+        super(TestSourcePackageRecipeBuild, self).setUp()
+        self.chef = self.factory.makePerson(
+            displayname='Master Chef', name='chef', password='test')
+        self.user = self.chef
+        self.ppa = self.factory.makeArchive(
+            displayname='Secret PPA', owner=self.chef, name='ppa')
+        self.squirrel = self.factory.makeDistroSeries(
+            displayname='Secret Squirrel', name='secret', version='100.04',
+            distribution=self.ppa.distribution)
+        self.squirrel.nominatedarchindep = self.squirrel.newArch(
+            'i386', ProcessorFamily.get(1), False, self.chef,
+            supports_virtualized=True)
+
+    def makeRecipeBuild(self):
+        """Create and return a specific recipe."""
+        chocolate = self.factory.makeProduct(name='chocolate')
+        cake_branch = self.factory.makeProductBranch(
+            owner=self.chef, name='cake', product=chocolate)
+        recipe = self.factory.makeSourcePackageRecipe(
+            owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe',
+            description=u'This recipe builds a foo for disto bar, with my'
+            ' Secret Squirrel changes.', branches=[cake_branch],
+            daily_build_archive=self.ppa)
+        build = self.factory.makeSourcePackageRecipeBuild(
+            recipe=recipe)
+        return build
+
+    def test_delete_build(self):
+        """An admin can delete a build."""
+        experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
+        build = self.makeRecipeBuild()
+        transaction.commit()
+        build_url = canonical_url(build)
+        recipe = build.recipe
+        next_url = canonical_url(recipe)
+        logout()
+
+        browser = self.getUserBrowser(build_url, user=experts)
+        browser.getLink('Delete build').click()
+
+        self.assertEqual(
+            browser.getLink('Cancel').url,
+            build_url)
+
+        browser.getControl('Delete build').click()
+
+        self.assertEqual(
+            browser.url,
+            next_url)
+        self.assertEqual(
+            recipe.getBuilds().count(),
+            0)
+
+    def test_delete_build_not_admin(self):
+        """No one but admins can delete a build."""
+        build = self.makeRecipeBuild()
+        transaction.commit()
+        build_url = canonical_url(build)
+        logout()
+
+        browser = self.getUserBrowser(build_url, user=self.chef)
+        self.assertRaises(
+            LinkNotFoundError,
+            browser.getLink, 'Delete build')

=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py	2010-06-30 02:27:10 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py	2010-07-13 10:03:29 +0000
@@ -309,6 +309,7 @@
         if build.status == BuildStatus.FULLYBUILT:
             build.notify()
 
+
 class SourcePackageRecipeBuildJob(BuildFarmJobOldDerived, Storm):
     classProvides(ISourcePackageRecipeBuildJobSource)
     implements(ISourcePackageRecipeBuildJob)

=== modified file 'lib/lp/code/templates/sourcepackagerecipebuild-index.pt'
--- lib/lp/code/templates/sourcepackagerecipebuild-index.pt	2010-05-05 19:16:24 +0000
+++ lib/lp/code/templates/sourcepackagerecipebuild-index.pt	2010-07-13 10:03:29 +0000
@@ -158,6 +158,16 @@
         (<span tal:replace="file/content/filesize/fmt:bytes" />)
       </li>
     </ul>
+
+    <div
+      style="margin-top: 1.5em"
+      tal:define="context_menu view/context/menu:context;
+                  link context_menu/delete"
+      tal:condition="link/enabled"
+      >
+      <a tal:replace="structure link/fmt:link" />
+    </div>
+
   </metal:macro>
 
   <metal:macro define-macro="buildlog">


Follow ups