Merge lp:~rockstar/launchpad/spr-admin into lp:launchpad

Proposed by Paul Hummer
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 11144
Proposed branch: lp:~rockstar/launchpad/spr-admin
Merge into: lp:launchpad
Prerequisite: lp:~rockstar/launchpad/bug-602333
Diff against target: 514 lines (+277/-76)
10 files modified
lib/canonical/launchpad/security.py (+9/-0)
lib/lp/code/browser/configure.zcml (+14/-3)
lib/lp/code/browser/sourcepackagerecipe.py (+4/-69)
lib/lp/code/browser/sourcepackagerecipebuild.py (+121/-0)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+3/-3)
lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py (+91/-0)
lib/lp/code/interfaces/sourcepackagerecipebuild.py (+3/-0)
lib/lp/code/model/sourcepackagerecipebuild.py (+12/-1)
lib/lp/code/model/tests/test_sourcepackagerecipebuild.py (+10/-0)
lib/lp/code/templates/sourcepackagerecipebuild-index.pt (+10/-0)
To merge this branch: bzr merge lp:~rockstar/launchpad/spr-admin
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+29780@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

Oh yeah, the other thing I did was to move lp.code.browser.sourcepackagerecipe stuff that was really sourcepackagerecipebuild stuff to lp.code.browser.sourcepackagerecipebuild.

Revision history for this message
Aaron Bentley (abentley) wrote :

Some minor formatting stuff to fix, as discussed.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2010-07-13 10:57:31 +0000
3+++ lib/canonical/launchpad/security.py 2010-07-16 13:31:52 +0000
4@@ -1142,6 +1142,15 @@
5 usedfor = ICodeImportMachine
6
7
8+class DeleteSourcePackageRecipeBuilds(OnlyBazaarExpertsAndAdmins):
9+ """Control who can delete SourcePackageRecipeBuilds.
10+
11+ Access is restricted to members of ~bazaar-experts and Launchpad admins.
12+ """
13+ permission = 'launchpad.Edit'
14+ usedfor = ISourcePackageRecipeBuild
15+
16+
17 class AdminDistributionTranslations(AuthorizationBase):
18 """Class for deciding who can administer distribution translations.
19
20
21=== modified file 'lib/lp/code/browser/configure.zcml'
22--- lib/lp/code/browser/configure.zcml 2010-07-09 10:22:32 +0000
23+++ lib/lp/code/browser/configure.zcml 2010-07-16 13:31:52 +0000
24@@ -1083,11 +1083,16 @@
25 attribute_to_parent="recipe"
26 path_expression="string:+build/${id}"
27 rootsite="code" />
28+ <browser:menus
29+ classes="SourcePackageRecipeBuildContextMenu"
30+ module="lp.code.browser.sourcepackagerecipebuild"/>
31
32 <browser:navigation
33 module="lp.code.browser.sourcepackagerecipe"
34- classes="SourcePackageRecipeNavigation
35- SourcePackageRecipeBuildNavigation" />
36+ classes="SourcePackageRecipeNavigation" />
37+ <browser:navigation
38+ module="lp.code.browser.sourcepackagerecipebuild"
39+ classes="SourcePackageRecipeBuildNavigation" />
40
41 <facet facet="branches">
42
43@@ -1115,10 +1120,16 @@
44 layer="canonical.launchpad.layers.CodeLayer"/>
45 <browser:page
46 for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"
47- class="lp.code.browser.sourcepackagerecipe.SourcePackageRecipeBuildView"
48+ class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildView"
49 name="+index"
50 template="../templates/sourcepackagerecipebuild-index.pt"
51 permission="launchpad.View"/>
52+ <browser:page
53+ for="lp.code.interfaces.sourcepackagerecipebuild.ISourcePackageRecipeBuild"
54+ class="lp.code.browser.sourcepackagerecipebuild.SourcePackageRecipeBuildCancelView"
55+ name="+cancel"
56+ template="../../app/templates/generic-edit.pt"
57+ permission="launchpad.View"/>
58 <browser:menus
59 classes="
60 SourcePackageRecipeNavigationMenu
61
62=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
63--- lib/lp/code/browser/sourcepackagerecipe.py 2010-07-02 01:12:15 +0000
64+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-07-16 13:31:52 +0000
65@@ -7,7 +7,6 @@
66
67 __all__ = [
68 'SourcePackageRecipeAddView',
69- 'SourcePackageRecipeBuildView',
70 'SourcePackageRecipeContextMenu',
71 'SourcePackageRecipeEditView',
72 'SourcePackageRecipeNavigationMenu',
73@@ -29,7 +28,6 @@
74
75 from canonical.database.constants import UTC_NOW
76 from canonical.launchpad.browser.launchpad import Hierarchy
77-from canonical.launchpad.browser.librarian import FileNavigationMixin
78 from canonical.launchpad.interfaces import ILaunchBag
79 from canonical.launchpad.webapp import (
80 action, canonical_url, ContextMenu, custom_widget,
81@@ -39,19 +37,18 @@
82 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
83 from canonical.launchpad.webapp.sorting import sorted_dotted_numbers
84 from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget
85-from lp.buildmaster.interfaces.buildbase import BuildStatus
86-from lp.code.errors import BuildAlreadyPending, ForbiddenInstruction
87+from lp.code.errors import ForbiddenInstruction
88+from lp.code.errors import BuildAlreadyPending
89 from lp.code.interfaces.branch import NoSuchBranch
90 from lp.code.interfaces.sourcepackagerecipe import (
91 ISourcePackageRecipe, ISourcePackageRecipeSource, MINIMAL_RECIPE_TEXT)
92 from lp.code.interfaces.sourcepackagerecipebuild import (
93- ISourcePackageRecipeBuild, ISourcePackageRecipeBuildSource)
94+ ISourcePackageRecipeBuildSource)
95 from lp.soyuz.browser.archive import make_archive_vocabulary
96 from lp.soyuz.interfaces.archive import (
97 IArchiveSet)
98 from lp.registry.interfaces.distroseries import IDistroSeriesSet
99 from lp.registry.interfaces.pocket import PackagePublishingPocket
100-from lp.services.job.interfaces.job import JobStatus
101
102 RECIPE_BETA_MESSAGE = structured(
103 'We\'re still working on source package recipes. '
104@@ -193,6 +190,7 @@
105 terms.reverse()
106 return SimpleVocabulary(terms)
107
108+
109 def target_ppas_vocabulary(context):
110 """Return a vocabulary of ppas that the current user can target."""
111 ppas = getUtility(IArchiveSet).getPPAsForUser(getUtility(ILaunchBag).user)
112@@ -264,69 +262,6 @@
113
114
115
116-class SourcePackageRecipeBuildNavigation(Navigation, FileNavigationMixin):
117-
118- usedfor = ISourcePackageRecipeBuild
119-
120-
121-class SourcePackageRecipeBuildView(LaunchpadView):
122- """Default view of a SourcePackageRecipeBuild."""
123-
124- @property
125- def status(self):
126- """A human-friendly status string."""
127- if (self.context.buildstate == BuildStatus.NEEDSBUILD
128- and self.eta is None):
129- return 'No suitable builders'
130- return {
131- BuildStatus.NEEDSBUILD: 'Pending build',
132- BuildStatus.FULLYBUILT: 'Successful build',
133- BuildStatus.MANUALDEPWAIT: (
134- 'Could not build because of missing dependencies'),
135- BuildStatus.CHROOTWAIT: (
136- 'Could not build because of chroot problem'),
137- BuildStatus.SUPERSEDED: (
138- 'Could not build because source package was superseded'),
139- BuildStatus.FAILEDTOUPLOAD: 'Could not be uploaded correctly',
140- }.get(self.context.buildstate, self.context.buildstate.title)
141-
142- @property
143- def eta(self):
144- """The datetime when the build job is estimated to complete.
145-
146- This is the BuildQueue.estimated_duration plus the
147- Job.date_started or BuildQueue.getEstimatedJobStartTime.
148- """
149- if self.context.buildqueue_record is None:
150- return None
151- queue_record = self.context.buildqueue_record
152- if queue_record.job.status == JobStatus.WAITING:
153- start_time = queue_record.getEstimatedJobStartTime()
154- if start_time is None:
155- return None
156- else:
157- start_time = queue_record.job.date_started
158- duration = queue_record.estimated_duration
159- return start_time + duration
160-
161- @property
162- def date(self):
163- """The date when the build completed or is estimated to complete."""
164- if self.estimate:
165- return self.eta
166- return self.context.datebuilt
167-
168- @property
169- def estimate(self):
170- """If true, the date value is an estimate."""
171- if self.context.datebuilt is not None:
172- return False
173- return self.eta is not None
174-
175- def binary_builds(self):
176- return list(self.context.binary_builds)
177-
178-
179 class ISourcePackageAddEditSchema(Interface):
180 """Schema for adding or editing a recipe."""
181
182
183=== added file 'lib/lp/code/browser/sourcepackagerecipebuild.py'
184--- lib/lp/code/browser/sourcepackagerecipebuild.py 1970-01-01 00:00:00 +0000
185+++ lib/lp/code/browser/sourcepackagerecipebuild.py 2010-07-16 13:31:52 +0000
186@@ -0,0 +1,121 @@
187+# Copyright 2010 Canonical Ltd. This software is licensed under the
188+# GNU Affero General Public License version 3 (see the file LICENSE).
189+
190+"""SourcePackageRecipeBuild views."""
191+
192+__metaclass__ = type
193+
194+__all__ = [
195+ 'SourcePackageRecipeBuildContextMenu',
196+ 'SourcePackageRecipeBuildNavigation',
197+ 'SourcePackageRecipeBuildView',
198+ 'SourcePackageRecipeBuildCancelView',
199+ ]
200+
201+from zope.interface import Interface
202+
203+from canonical.launchpad.browser.librarian import FileNavigationMixin
204+from canonical.launchpad.webapp import (
205+ action, canonical_url, ContextMenu, enabled_with_permission,
206+ LaunchpadView, LaunchpadFormView, Link, Navigation)
207+
208+from lp.buildmaster.interfaces.buildbase import BuildStatus
209+from lp.code.interfaces.sourcepackagerecipebuild import (
210+ ISourcePackageRecipeBuild)
211+from lp.services.job.interfaces.job import JobStatus
212+
213+
214+class SourcePackageRecipeBuildNavigation(Navigation, FileNavigationMixin):
215+
216+ usedfor = ISourcePackageRecipeBuild
217+
218+
219+class SourcePackageRecipeBuildContextMenu(ContextMenu):
220+ """Navigation menu for sourcepackagerecipe build."""
221+
222+ usedfor = ISourcePackageRecipeBuild
223+
224+ facet = 'branches'
225+
226+ links = ('cancel',)
227+
228+ @enabled_with_permission('launchpad.Edit')
229+ def cancel(self):
230+ return Link('+cancel', 'Cancel build', icon='remove')
231+
232+
233+class SourcePackageRecipeBuildView(LaunchpadView):
234+ """Default view of a SourcePackageRecipeBuild."""
235+
236+ @property
237+ def status(self):
238+ """A human-friendly status string."""
239+ if (self.context.buildstate == BuildStatus.NEEDSBUILD
240+ and self.eta is None):
241+ return 'No suitable builders'
242+ return {
243+ BuildStatus.NEEDSBUILD: 'Pending build',
244+ BuildStatus.FULLYBUILT: 'Successful build',
245+ BuildStatus.MANUALDEPWAIT: (
246+ 'Could not build because of missing dependencies'),
247+ BuildStatus.CHROOTWAIT: (
248+ 'Could not build because of chroot problem'),
249+ BuildStatus.SUPERSEDED: (
250+ 'Could not build because source package was superseded'),
251+ BuildStatus.FAILEDTOUPLOAD: 'Could not be uploaded correctly',
252+ }.get(self.context.buildstate, self.context.buildstate.title)
253+
254+ @property
255+ def eta(self):
256+ """The datetime when the build job is estimated to complete.
257+
258+ This is the BuildQueue.estimated_duration plus the
259+ Job.date_started or BuildQueue.getEstimatedJobStartTime.
260+ """
261+ if self.context.buildqueue_record is None:
262+ return None
263+ queue_record = self.context.buildqueue_record
264+ if queue_record.job.status == JobStatus.WAITING:
265+ start_time = queue_record.getEstimatedJobStartTime()
266+ if start_time is None:
267+ return None
268+ else:
269+ start_time = queue_record.job.date_started
270+ duration = queue_record.estimated_duration
271+ return start_time + duration
272+
273+ @property
274+ def date(self):
275+ """The date when the build completed or is estimated to complete."""
276+ if self.estimate:
277+ return self.eta
278+ return self.context.datebuilt
279+
280+ @property
281+ def estimate(self):
282+ """If true, the date value is an estimate."""
283+ if self.context.datebuilt is not None:
284+ return False
285+ return self.eta is not None
286+
287+ def binary_builds(self):
288+ return list(self.context.binary_builds)
289+
290+
291+class SourcePackageRecipeBuildCancelView(LaunchpadFormView):
292+ """View for cancelling a build."""
293+
294+ class schema(Interface):
295+ """Schema for cancelling a build."""
296+
297+ page_title = label = "Cancel build"
298+
299+ @property
300+ def cancel_url(self):
301+ return canonical_url(self.context)
302+ next_url = cancel_url
303+
304+ @action('Cancel build', name='cancel')
305+ def request_action(self, action, data):
306+ """Cancel the build."""
307+ self.context.cancelBuild()
308
309=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
310--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-07-02 11:11:32 +0000
311+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-07-16 13:31:52 +0000
312@@ -21,9 +21,9 @@
313 DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
314 from lp.buildmaster.interfaces.buildbase import BuildStatus
315 from lp.code.browser.sourcepackagerecipe import (
316- SourcePackageRecipeView, SourcePackageRecipeRequestBuildsView,
317- SourcePackageRecipeBuildView
318-)
319+ SourcePackageRecipeView, SourcePackageRecipeRequestBuildsView)
320+from lp.code.browser.sourcepackagerecipebuild import (
321+ SourcePackageRecipeBuildView)
322 from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
323 from lp.registry.interfaces.pocket import PackagePublishingPocket
324 from lp.soyuz.model.processor import ProcessorFamily
325
326=== added file 'lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py'
327--- lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 1970-01-01 00:00:00 +0000
328+++ lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 2010-07-16 13:31:52 +0000
329@@ -0,0 +1,91 @@
330+# Copyright 2010 Canonical Ltd. This software is licensed under the
331+# GNU Affero General Public License version 3 (see the file LICENSE).
332+# pylint: disable-msg=F0401,E1002
333+
334+"""Tests for the source package recipe view classes and templates."""
335+
336+__metaclass__ = type
337+
338+from mechanize import LinkNotFoundError
339+import transaction
340+from zope.component import getUtility
341+
342+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
343+from canonical.launchpad.webapp import canonical_url
344+from canonical.testing import DatabaseFunctionalLayer
345+from lp.buildmaster.interfaces.buildbase import BuildStatus
346+from lp.soyuz.model.processor import ProcessorFamily
347+from lp.testing import ANONYMOUS, BrowserTestCase, login, logout
348+
349+
350+class TestSourcePackageRecipeBuild(BrowserTestCase):
351+ """Create some sample data for recipe tests."""
352+
353+ layer = DatabaseFunctionalLayer
354+
355+ def setUp(self):
356+ """Provide useful defaults."""
357+ super(TestSourcePackageRecipeBuild, self).setUp()
358+ self.chef = self.factory.makePerson(
359+ displayname='Master Chef', name='chef', password='test')
360+ self.user = self.chef
361+ self.ppa = self.factory.makeArchive(
362+ displayname='Secret PPA', owner=self.chef, name='ppa')
363+ self.squirrel = self.factory.makeDistroSeries(
364+ displayname='Secret Squirrel', name='secret', version='100.04',
365+ distribution=self.ppa.distribution)
366+ self.squirrel.nominatedarchindep = self.squirrel.newArch(
367+ 'i386', ProcessorFamily.get(1), False, self.chef,
368+ supports_virtualized=True)
369+
370+ def makeRecipeBuild(self):
371+ """Create and return a specific recipe."""
372+ chocolate = self.factory.makeProduct(name='chocolate')
373+ cake_branch = self.factory.makeProductBranch(
374+ owner=self.chef, name='cake', product=chocolate)
375+ recipe = self.factory.makeSourcePackageRecipe(
376+ owner=self.chef, distroseries=self.squirrel, name=u'cake_recipe',
377+ description=u'This recipe builds a foo for disto bar, with my'
378+ ' Secret Squirrel changes.', branches=[cake_branch],
379+ daily_build_archive=self.ppa)
380+ build = self.factory.makeSourcePackageRecipeBuild(
381+ recipe=recipe)
382+ return build
383+
384+ def test_cancel_build(self):
385+ """An admin can cancel a build."""
386+ experts = getUtility(ILaunchpadCelebrities).bazaar_experts.teamowner
387+ build = self.makeRecipeBuild()
388+ transaction.commit()
389+ build_url = canonical_url(build)
390+ logout()
391+
392+ browser = self.getUserBrowser(build_url, user=experts)
393+ browser.getLink('Cancel build').click()
394+
395+ self.assertEqual(
396+ browser.getLink('Cancel').url,
397+ build_url)
398+
399+ browser.getControl('Cancel build').click()
400+
401+ self.assertEqual(
402+ browser.url,
403+ build_url)
404+
405+ login(ANONYMOUS)
406+ self.assertEqual(
407+ BuildStatus.SUPERSEDED,
408+ build.status)
409+
410+ def test_cancel_build_not_admin(self):
411+ """No one but admins can cancel a build."""
412+ build = self.makeRecipeBuild()
413+ transaction.commit()
414+ build_url = canonical_url(build)
415+ logout()
416+
417+ browser = self.getUserBrowser(build_url, user=self.chef)
418+ self.assertRaises(
419+ LinkNotFoundError,
420+ browser.getLink, 'Cancel build')
421
422=== modified file 'lib/lp/code/interfaces/sourcepackagerecipebuild.py'
423--- lib/lp/code/interfaces/sourcepackagerecipebuild.py 2010-06-30 02:27:10 +0000
424+++ lib/lp/code/interfaces/sourcepackagerecipebuild.py 2010-07-16 13:31:52 +0000
425@@ -76,6 +76,9 @@
426 def getFileByName(filename):
427 """Return the file under +files with specified name."""
428
429+ def cancelBuild():
430+ """Cancel the build."""
431+
432 def destroySelf():
433 """Delete the build itself."""
434
435
436=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
437--- lib/lp/code/model/sourcepackagerecipebuild.py 2010-06-30 02:27:10 +0000
438+++ lib/lp/code/model/sourcepackagerecipebuild.py 2010-07-16 13:31:52 +0000
439@@ -240,7 +240,8 @@
440 recipe.is_stale = False
441 return builds
442
443- def destroySelf(self):
444+ def _unqueueBuild(self):
445+ """Remove the build's queue and job."""
446 store = Store.of(self)
447 if self.buildqueue_record is not None:
448 job = self.buildqueue_record.job
449@@ -249,6 +250,15 @@
450 SourcePackageRecipeBuildJob,
451 SourcePackageRecipeBuildJob.build == self.id).remove()
452 store.remove(job)
453+
454+ def cancelBuild(self):
455+ """See `ISourcePackageRecipeBuild.`"""
456+ self._unqueueBuild()
457+ self.status = BuildStatus.SUPERSEDED
458+
459+ def destroySelf(self):
460+ self._unqueueBuild()
461+ store = Store.of(self)
462 store.remove(self)
463
464 @classmethod
465@@ -309,6 +319,7 @@
466 if build.status == BuildStatus.FULLYBUILT:
467 build.notify()
468
469+
470 class SourcePackageRecipeBuildJob(BuildFarmJobOldDerived, Storm):
471 classProvides(ISourcePackageRecipeBuildJobSource)
472 implements(ISourcePackageRecipeBuildJob)
473
474=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
475--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-06-30 02:27:10 +0000
476+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py 2010-07-16 13:31:52 +0000
477@@ -294,6 +294,16 @@
478 build = self.factory.makeSourcePackageRecipeBuild()
479 build.destroySelf()
480
481+ def test_cancelBuild(self):
482+ # ISourcePackageRecipeBuild should make sure to remove jobs and build
483+ # queue entries and then invalidate itself.
484+ build = self.factory.makeSourcePackageRecipeBuild()
485+ build.cancelBuild()
486+
487+ self.assertEqual(
488+ BuildStatus.SUPERSEDED,
489+ build.status)
490+
491
492 class TestAsBuildmaster(TestCaseWithFactory):
493
494
495=== modified file 'lib/lp/code/templates/sourcepackagerecipebuild-index.pt'
496--- lib/lp/code/templates/sourcepackagerecipebuild-index.pt 2010-05-05 19:16:24 +0000
497+++ lib/lp/code/templates/sourcepackagerecipebuild-index.pt 2010-07-16 13:31:52 +0000
498@@ -158,6 +158,16 @@
499 (<span tal:replace="file/content/filesize/fmt:bytes" />)
500 </li>
501 </ul>
502+
503+ <div
504+ style="margin-top: 1.5em"
505+ tal:define="context_menu view/context/menu:context;
506+ link context_menu/cancel"
507+ tal:condition="link/enabled"
508+ >
509+ <a tal:replace="structure link/fmt:link" />
510+ </div>
511+
512 </metal:macro>
513
514 <metal:macro define-macro="buildlog">