Merge lp:~cjwatson/launchpad/snap-basic-browser into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17685
Proposed branch: lp:~cjwatson/launchpad/snap-basic-browser
Merge into: lp:launchpad
Diff against target: 1103 lines (+989/-3)
9 files modified
lib/lp/app/browser/configure.zcml (+6/-0)
lib/lp/snappy/browser/configure.zcml (+43/-0)
lib/lp/snappy/browser/snap.py (+59/-0)
lib/lp/snappy/browser/snapbuild.py (+152/-2)
lib/lp/snappy/browser/tests/test_snap.py (+180/-0)
lib/lp/snappy/browser/tests/test_snapbuild.py (+246/-0)
lib/lp/snappy/model/snap.py (+2/-1)
lib/lp/snappy/templates/snap-index.pt (+96/-0)
lib/lp/snappy/templates/snapbuild-index.pt (+205/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-basic-browser
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+267324@code.launchpad.net

Commit message

Add basic browser views for Snap and SnapBuild.

Description of the change

Add basic browser views for Snap and SnapBuild.

This is mostly read-only right now: there are no admin, edit, or delete views, and the only way to add things is using the webservice. But with this branch we have enough to be able to see builds in action: there's a temporary demo at https://dogfood.paddev.net/~cjwatson/+snap/wget.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2015-07-23 14:32:50 +0000
+++ lib/lp/app/browser/configure.zcml 2015-08-07 10:52:53 +0000
@@ -570,6 +570,12 @@
570 factory="lp.app.browser.tales.BuildImageDisplayAPI"570 factory="lp.app.browser.tales.BuildImageDisplayAPI"
571 name="image"571 name="image"
572 />572 />
573 <adapter
574 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
575 provides="zope.traversing.interfaces.IPathAdapter"
576 factory="lp.app.browser.tales.BuildImageDisplayAPI"
577 name="image"
578 />
573579
574 <adapter580 <adapter
575 for="lp.soyuz.interfaces.archive.IArchive"581 for="lp.soyuz.interfaces.archive.IArchive"
576582
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 2015-07-23 16:41:12 +0000
+++ lib/lp/snappy/browser/configure.zcml 2015-08-07 10:52:53 +0000
@@ -13,9 +13,23 @@
13 for="lp.snappy.interfaces.snap.ISnap"13 for="lp.snappy.interfaces.snap.ISnap"
14 path_expression="string:+snap/${name}"14 path_expression="string:+snap/${name}"
15 attribute_to_parent="owner" />15 attribute_to_parent="owner" />
16 <browser:defaultView
17 for="lp.snappy.interfaces.snap.ISnap"
18 name="+index" />
19 <browser:page
20 for="lp.snappy.interfaces.snap.ISnap"
21 class="lp.snappy.browser.snap.SnapView"
22 permission="launchpad.View"
23 name="+index"
24 template="../templates/snap-index.pt" />
16 <browser:navigation25 <browser:navigation
17 module="lp.snappy.browser.snap"26 module="lp.snappy.browser.snap"
18 classes="SnapNavigation" />27 classes="SnapNavigation" />
28 <adapter
29 provides="lp.services.webapp.interfaces.IBreadcrumb"
30 for="lp.snappy.interfaces.snap.ISnap"
31 factory="lp.snappy.browser.snap.SnapBreadcrumb"
32 permission="zope.Public" />
19 <browser:url33 <browser:url
20 for="lp.snappy.interfaces.snap.ISnapSet"34 for="lp.snappy.interfaces.snap.ISnapSet"
21 path_expression="string:+snaps"35 path_expression="string:+snaps"
@@ -24,8 +38,37 @@
24 for="lp.snappy.interfaces.snapbuild.ISnapBuild"38 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
25 path_expression="string:+build/${id}"39 path_expression="string:+build/${id}"
26 attribute_to_parent="snap" />40 attribute_to_parent="snap" />
41 <browser:menus
42 module="lp.snappy.browser.snapbuild"
43 classes="SnapBuildContextMenu" />
27 <browser:navigation44 <browser:navigation
28 module="lp.snappy.browser.snapbuild"45 module="lp.snappy.browser.snapbuild"
29 classes="SnapBuildNavigation" />46 classes="SnapBuildNavigation" />
47 <browser:defaultView
48 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
49 name="+index" />
50 <browser:page
51 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
52 class="lp.snappy.browser.snapbuild.SnapBuildView"
53 permission="launchpad.View"
54 name="+index"
55 template="../templates/snapbuild-index.pt" />
56 <browser:page
57 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
58 class="lp.snappy.browser.snapbuild.SnapBuildCancelView"
59 permission="launchpad.Edit"
60 name="+cancel"
61 template="../../app/templates/generic-edit.pt" />
62 <browser:page
63 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
64 class="lp.snappy.browser.snapbuild.SnapBuildRescoreView"
65 permission="launchpad.Admin"
66 name="+rescore"
67 template="../../app/templates/generic-edit.pt" />
68 <adapter
69 provides="lp.services.webapp.interfaces.IBreadcrumb"
70 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
71 factory="lp.services.webapp.breadcrumb.TitleBreadcrumb"
72 permission="zope.Public" />
30 </facet>73 </facet>
31</configure>74</configure>
3275
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2015-07-23 16:02:58 +0000
+++ lib/lp/snappy/browser/snap.py 2015-08-07 10:52:53 +0000
@@ -6,12 +6,20 @@
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'SnapNavigation',8 'SnapNavigation',
9 'SnapView',
9 ]10 ]
1011
11from lp.services.webapp import (12from lp.services.webapp import (
13 canonical_url,
14 LaunchpadView,
12 Navigation,15 Navigation,
13 stepthrough,16 stepthrough,
14 )17 )
18from lp.services.webapp.authorization import check_permission
19from lp.services.webapp.breadcrumb import (
20 Breadcrumb,
21 NameBreadcrumb,
22 )
15from lp.snappy.interfaces.snap import ISnap23from lp.snappy.interfaces.snap import ISnap
16from lp.snappy.interfaces.snapbuild import ISnapBuildSet24from lp.snappy.interfaces.snapbuild import ISnapBuildSet
17from lp.soyuz.browser.build import get_build_by_id_str25from lp.soyuz.browser.build import get_build_by_id_str
@@ -26,3 +34,54 @@
26 if build is None or build.snap != self.context:34 if build is None or build.snap != self.context:
27 return None35 return None
28 return build36 return build
37
38
39class SnapBreadcrumb(NameBreadcrumb):
40
41 @property
42 def inside(self):
43 return Breadcrumb(
44 self.context.owner,
45 url=canonical_url(self.context.owner, view_name="+snap"),
46 text="Snap packages", inside=self.context.owner)
47
48
49class SnapView(LaunchpadView):
50 """Default view of a Snap."""
51
52 @property
53 def page_title(self):
54 return "%(name)s's %(snap_name)s snap package" % {
55 'name': self.context.owner.displayname,
56 'snap_name': self.context.name,
57 }
58
59 label = page_title
60
61 @property
62 def builds(self):
63 return builds_for_snap(self.context)
64
65
66def builds_for_snap(snap):
67 """A list of interesting builds.
68
69 All pending builds are shown, as well as 1-10 recent builds. Recent
70 builds are ordered by date finished (if completed) or date_started (if
71 date finished is not set due to an error building or other circumstance
72 which resulted in the build not being completed). This allows started
73 but unfinished builds to show up in the view but be discarded as more
74 recent builds become available.
75
76 Builds that the user does not have permission to see are excluded.
77 """
78 builds = [
79 build for build in snap.pending_builds
80 if check_permission('launchpad.View', build)]
81 for build in snap.completed_builds:
82 if not check_permission('launchpad.View', build):
83 continue
84 builds.append(build)
85 if len(builds) >= 10:
86 break
87 return builds
2988
=== modified file 'lib/lp/snappy/browser/snapbuild.py'
--- lib/lp/snappy/browser/snapbuild.py 2015-07-23 16:02:58 +0000
+++ lib/lp/snappy/browser/snapbuild.py 2015-08-07 10:52:53 +0000
@@ -5,13 +5,163 @@
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'SnapBuildContextMenu',
8 'SnapBuildNavigation',9 'SnapBuildNavigation',
10 'SnapBuildView',
9 ]11 ]
1012
11from lp.services.librarian.browser import FileNavigationMixin13from zope.interface import Interface
12from lp.services.webapp import Navigation14
15from lp.app.browser.launchpadform import (
16 action,
17 LaunchpadFormView,
18 )
19from lp.buildmaster.enums import BuildQueueStatus
20from lp.services.librarian.browser import (
21 FileNavigationMixin,
22 ProxiedLibraryFileAlias,
23 )
24from lp.services.propertycache import cachedproperty
25from lp.services.webapp import (
26 canonical_url,
27 ContextMenu,
28 enabled_with_permission,
29 LaunchpadView,
30 Link,
31 Navigation,
32 )
13from lp.snappy.interfaces.snapbuild import ISnapBuild33from lp.snappy.interfaces.snapbuild import ISnapBuild
34from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm
1435
1536
16class SnapBuildNavigation(Navigation, FileNavigationMixin):37class SnapBuildNavigation(Navigation, FileNavigationMixin):
17 usedfor = ISnapBuild38 usedfor = ISnapBuild
39
40
41class SnapBuildContextMenu(ContextMenu):
42 """Context menu for snap package builds."""
43
44 usedfor = ISnapBuild
45
46 facet = 'overview'
47
48 links = ('cancel', 'rescore')
49
50 @enabled_with_permission('launchpad.Edit')
51 def cancel(self):
52 return Link(
53 '+cancel', 'Cancel build', icon='remove',
54 enabled=self.context.can_be_cancelled)
55
56 @enabled_with_permission('launchpad.Admin')
57 def rescore(self):
58 return Link(
59 '+rescore', 'Rescore build', icon='edit',
60 enabled=self.context.can_be_rescored)
61
62
63class SnapBuildView(LaunchpadView):
64 """Default view of a SnapBuild."""
65
66 @property
67 def label(self):
68 return self.context.title
69
70 page_title = label
71
72 @cachedproperty
73 def eta(self):
74 """The datetime when the build job is estimated to complete.
75
76 This is the BuildQueue.estimated_duration plus the
77 Job.date_started or BuildQueue.getEstimatedJobStartTime.
78 """
79 if self.context.buildqueue_record is None:
80 return None
81 queue_record = self.context.buildqueue_record
82 if queue_record.status == BuildQueueStatus.WAITING:
83 start_time = queue_record.getEstimatedJobStartTime()
84 else:
85 start_time = queue_record.date_started
86 if start_time is None:
87 return None
88 duration = queue_record.estimated_duration
89 return start_time + duration
90
91 @cachedproperty
92 def estimate(self):
93 """If true, the date value is an estimate."""
94 if self.context.date_finished is not None:
95 return False
96 return self.eta is not None
97
98 @cachedproperty
99 def date(self):
100 """The date when the build completed or is estimated to complete."""
101 if self.estimate:
102 return self.eta
103 return self.context.date_finished
104
105 @cachedproperty
106 def files(self):
107 """Return `LibraryFileAlias`es for files produced by this build."""
108 if not self.context.was_built:
109 return None
110
111 return [
112 ProxiedLibraryFileAlias(alias, self.context)
113 for _, alias, _ in self.context.getFiles() if not alias.deleted]
114
115 @cachedproperty
116 def has_files(self):
117 return bool(self.files)
118
119
120class SnapBuildCancelView(LaunchpadFormView):
121 """View for cancelling a snap package build."""
122
123 class schema(Interface):
124 """Schema for cancelling a build."""
125
126 page_title = label = 'Cancel build'
127
128 @property
129 def cancel_url(self):
130 return canonical_url(self.context)
131 next_url = cancel_url
132
133 @action('Cancel build', name='cancel')
134 def request_action(self, action, data):
135 """Cancel the build."""
136 self.context.cancel()
137
138
139class SnapBuildRescoreView(LaunchpadFormView):
140 """View for rescoring a snap package build."""
141
142 schema = IBuildRescoreForm
143
144 page_title = label = 'Rescore build'
145
146 def __call__(self):
147 if self.context.can_be_rescored:
148 return super(SnapBuildRescoreView, self).__call__()
149 self.request.response.addWarningNotification(
150 "Cannot rescore this build because it is not queued.")
151 self.request.response.redirect(canonical_url(self.context))
152
153 @property
154 def cancel_url(self):
155 return canonical_url(self.context)
156 next_url = cancel_url
157
158 @action('Rescore build', name='rescore')
159 def request_action(self, action, data):
160 """Rescore the build."""
161 score = data.get('priority')
162 self.context.rescore(score)
163 self.request.response.addNotification('Build rescored to %s.' % score)
164
165 @property
166 def initial_values(self):
167 return {'score': str(self.context.buildqueue_record.lastscore)}
18168
=== added directory 'lib/lp/snappy/browser/tests'
=== added file 'lib/lp/snappy/browser/tests/__init__.py'
=== added file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2015-08-07 10:52:53 +0000
@@ -0,0 +1,180 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test snap package views."""
5
6__metaclass__ = type
7
8from datetime import (
9 datetime,
10 timedelta,
11 )
12
13import pytz
14from zope.component import getUtility
15
16from lp.app.interfaces.launchpad import ILaunchpadCelebrities
17from lp.buildmaster.enums import BuildStatus
18from lp.buildmaster.interfaces.processor import IProcessorSet
19from lp.services.features.testing import FeatureFixture
20from lp.services.webapp import canonical_url
21from lp.snappy.browser.snap import SnapView
22from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
23from lp.testing import (
24 BrowserTestCase,
25 person_logged_in,
26 TestCaseWithFactory,
27 time_counter,
28 )
29from lp.testing.layers import (
30 DatabaseFunctionalLayer,
31 LaunchpadFunctionalLayer,
32 )
33from lp.testing.publication import test_traverse
34
35
36class TestSnapNavigation(TestCaseWithFactory):
37
38 layer = DatabaseFunctionalLayer
39
40 def setUp(self):
41 super(TestSnapNavigation, self).setUp()
42 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
43
44 def test_canonical_url(self):
45 owner = self.factory.makePerson(name="person")
46 snap = self.factory.makeSnap(
47 registrant=owner, owner=owner, name=u"snap")
48 self.assertEqual(
49 "http://launchpad.dev/~person/+snap/snap", canonical_url(snap))
50
51 def test_snap(self):
52 snap = self.factory.makeSnap()
53 obj, _, _ = test_traverse(
54 "http://launchpad.dev/~%s/+snap/%s" % (snap.owner.name, snap.name))
55 self.assertEqual(snap, obj)
56
57
58class TestSnapView(BrowserTestCase):
59
60 layer = LaunchpadFunctionalLayer
61
62 def setUp(self):
63 super(TestSnapView, self).setUp()
64 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
65 self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
66 self.distroseries = self.factory.makeDistroSeries(
67 distribution=self.ubuntu, name="shiny", displayname="Shiny")
68 processor = getUtility(IProcessorSet).getByName("386")
69 self.distroarchseries = self.factory.makeDistroArchSeries(
70 distroseries=self.distroseries, architecturetag="i386",
71 processor=processor)
72 self.person = self.factory.makePerson(
73 name="test-person", displayname="Test Person")
74 self.factory.makeBuilder(virtualized=True)
75
76 def makeSnap(self, branch=None, git_ref=None):
77 kwargs = {}
78 if branch is None and git_ref is None:
79 branch = self.factory.makeAnyBranch()
80 if branch is not None:
81 kwargs["branch"] = branch
82 else:
83 kwargs["git_repository"] = git_ref.repository
84 kwargs["git_path"] = git_ref.path
85 return self.factory.makeSnap(
86 registrant=self.person, owner=self.person,
87 distroseries=self.distroseries, name=u"snap-name", **kwargs)
88
89 def makeBuild(self, snap=None, archive=None, date_created=None, **kwargs):
90 if snap is None:
91 snap = self.makeSnap()
92 if archive is None:
93 archive = self.ubuntu.main_archive
94 if date_created is None:
95 date_created = datetime.now(pytz.UTC) - timedelta(hours=1)
96 return self.factory.makeSnapBuild(
97 requester=self.person, snap=snap, archive=archive,
98 distroarchseries=self.distroarchseries, date_created=date_created,
99 **kwargs)
100
101 def test_index(self):
102 build = self.makeBuild(
103 status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30))
104 self.assertTextMatchesExpressionIgnoreWhitespace("""\
105 Snap packages snap-name
106 .*
107 Snap package information
108 Owner: Test Person
109 Distribution series: Ubuntu Shiny
110 Latest builds
111 Status When complete Architecture Archive
112 Successfully built 30 minutes ago i386
113 Primary Archive for Ubuntu Linux
114 """, self.getMainText(build.snap))
115
116 def test_index_success_with_buildlog(self):
117 # The build log is shown if it is there.
118 build = self.makeBuild(
119 status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30))
120 build.setLog(self.factory.makeLibraryFileAlias())
121 self.assertTextMatchesExpressionIgnoreWhitespace("""\
122 Latest builds
123 Status When complete Architecture Archive
124 Successfully built 30 minutes ago buildlog \(.*\) i386
125 Primary Archive for Ubuntu Linux
126 """, self.getMainText(build.snap))
127
128 def test_index_hides_builds_into_private_archive(self):
129 # The index page hides builds into archives the user can't view.
130 archive = self.factory.makeArchive(private=True)
131 with person_logged_in(archive.owner):
132 snap = self.makeBuild(archive=archive).snap
133 self.assertIn(
134 "This snap package has not been built yet.",
135 self.getMainText(snap))
136
137 def test_index_no_builds(self):
138 # A message is shown when there are no builds.
139 snap = self.factory.makeSnap()
140 self.assertIn(
141 "This snap package has not been built yet.",
142 self.getMainText(snap))
143
144 def test_index_pending(self):
145 # A pending build is listed as such.
146 build = self.makeBuild()
147 build.queueBuild()
148 self.assertTextMatchesExpressionIgnoreWhitespace("""\
149 Latest builds
150 Status When complete Architecture Archive
151 Needs building in .* \(estimated\) i386
152 Primary Archive for Ubuntu Linux
153 """, self.getMainText(build.snap))
154
155 def setStatus(self, build, status):
156 build.updateStatus(
157 BuildStatus.BUILDING, date_started=build.date_created)
158 build.updateStatus(
159 status, date_finished=build.date_started + timedelta(minutes=30))
160
161 def test_builds(self):
162 # SnapView.builds produces reasonable results.
163 snap = self.makeSnap()
164 # Create oldest builds first so that they sort properly by id.
165 date_gen = time_counter(
166 datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1))
167 builds = [
168 self.makeBuild(snap=snap, date_created=next(date_gen))
169 for i in range(11)]
170 view = SnapView(snap, None)
171 self.assertEqual(list(reversed(builds)), view.builds)
172 self.setStatus(builds[10], BuildStatus.FULLYBUILT)
173 self.setStatus(builds[9], BuildStatus.FAILEDTOBUILD)
174 # When there are >= 9 pending builds, only the most recent of any
175 # completed builds is returned.
176 self.assertEqual(
177 list(reversed(builds[:9])) + [builds[10]], view.builds)
178 for build in builds[:9]:
179 self.setStatus(build, BuildStatus.FULLYBUILT)
180 self.assertEqual(list(reversed(builds[1:])), view.builds)
0181
=== added file 'lib/lp/snappy/browser/tests/test_snapbuild.py'
--- lib/lp/snappy/browser/tests/test_snapbuild.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/tests/test_snapbuild.py 2015-08-07 10:52:53 +0000
@@ -0,0 +1,246 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test snap package build views."""
5
6__metaclass__ = type
7
8from fixtures import FakeLogger
9from mechanize import LinkNotFoundError
10from storm.locals import Store
11from testtools.matchers import StartsWith
12import transaction
13from zope.component import getUtility
14from zope.security.interfaces import Unauthorized
15from zope.security.proxy import removeSecurityProxy
16
17from lp.app.interfaces.launchpad import ILaunchpadCelebrities
18from lp.buildmaster.enums import BuildStatus
19from lp.services.features.testing import FeatureFixture
20from lp.services.webapp import canonical_url
21from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
22from lp.testing import (
23 admin_logged_in,
24 ANONYMOUS,
25 BrowserTestCase,
26 login,
27 logout,
28 person_logged_in,
29 TestCaseWithFactory,
30 )
31from lp.testing.layers import (
32 DatabaseFunctionalLayer,
33 LaunchpadFunctionalLayer,
34 )
35from lp.testing.pages import (
36 extract_text,
37 find_main_content,
38 find_tags_by_class,
39 )
40from lp.testing.views import create_initialized_view
41
42
43class TestCanonicalUrlForSnapBuild(TestCaseWithFactory):
44
45 layer = DatabaseFunctionalLayer
46
47 def setUp(self):
48 super(TestCanonicalUrlForSnapBuild, self).setUp()
49 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
50
51 def test_canonical_url(self):
52 owner = self.factory.makePerson(name="person")
53 snap = self.factory.makeSnap(
54 registrant=owner, owner=owner, name=u"snap")
55 build = self.factory.makeSnapBuild(requester=owner, snap=snap)
56 self.assertThat(
57 canonical_url(build),
58 StartsWith("http://launchpad.dev/~person/+snap/snap/+build/"))
59
60
61class TestSnapBuildView(TestCaseWithFactory):
62
63 layer = LaunchpadFunctionalLayer
64
65 def setUp(self):
66 super(TestSnapBuildView, self).setUp()
67 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
68
69 def test_files(self):
70 # SnapBuildView.files returns all the associated files.
71 build = self.factory.makeSnapBuild(status=BuildStatus.FULLYBUILT)
72 snapfile = self.factory.makeSnapFile(snapbuild=build)
73 build_view = create_initialized_view(build, "+index")
74 self.assertEqual(
75 [snapfile.libraryfile.filename],
76 [lfa.filename for lfa in build_view.files])
77 # Deleted files won't be included.
78 self.assertFalse(snapfile.libraryfile.deleted)
79 removeSecurityProxy(snapfile.libraryfile).content = None
80 self.assertTrue(snapfile.libraryfile.deleted)
81 build_view = create_initialized_view(build, "+index")
82 self.assertEqual([], build_view.files)
83
84 def test_eta(self):
85 # SnapBuildView.eta returns a non-None value when it should, or None
86 # when there's no start time.
87 build = self.factory.makeSnapBuild()
88 build.queueBuild()
89 self.assertIsNone(create_initialized_view(build, "+index").eta)
90 self.factory.makeBuilder(processors=[build.processor])
91 self.assertIsNotNone(create_initialized_view(build, "+index").eta)
92
93 def test_estimate(self):
94 # SnapBuildView.estimate returns True until the job is completed.
95 build = self.factory.makeSnapBuild()
96 build.queueBuild()
97 self.factory.makeBuilder(processors=[build.processor])
98 build.updateStatus(BuildStatus.BUILDING)
99 self.assertTrue(create_initialized_view(build, "+index").estimate)
100 build.updateStatus(BuildStatus.FULLYBUILT)
101 self.assertFalse(create_initialized_view(build, "+index").estimate)
102
103
104class TestSnapBuildOperations(BrowserTestCase):
105
106 layer = DatabaseFunctionalLayer
107
108 def setUp(self):
109 super(TestSnapBuildOperations, self).setUp()
110 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
111 self.useFixture(FakeLogger())
112 self.build = self.factory.makeSnapBuild()
113 self.build_url = canonical_url(self.build)
114 self.requester = self.build.requester
115 self.buildd_admin = self.factory.makePerson(
116 member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
117
118 def test_cancel_build(self):
119 # The requester of a build can cancel it.
120 self.build.queueBuild()
121 transaction.commit()
122 browser = self.getViewBrowser(self.build, user=self.requester)
123 browser.getLink("Cancel build").click()
124 self.assertEqual(self.build_url, browser.getLink("Cancel").url)
125 browser.getControl("Cancel build").click()
126 self.assertEqual(self.build_url, browser.url)
127 login(ANONYMOUS)
128 self.assertEqual(BuildStatus.CANCELLED, self.build.status)
129
130 def test_cancel_build_random_user(self):
131 # An unrelated non-admin user cannot cancel a build.
132 self.build.queueBuild()
133 transaction.commit()
134 user = self.factory.makePerson()
135 browser = self.getViewBrowser(self.build, user=user)
136 self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
137 self.assertRaises(
138 Unauthorized, self.getUserBrowser, self.build_url + "/+cancel",
139 user=user)
140
141 def test_cancel_build_wrong_state(self):
142 # If the build isn't queued, you can't cancel it.
143 browser = self.getViewBrowser(self.build, user=self.requester)
144 self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
145
146 def test_rescore_build(self):
147 # A buildd admin can rescore a build.
148 self.build.queueBuild()
149 transaction.commit()
150 browser = self.getViewBrowser(self.build, user=self.buildd_admin)
151 browser.getLink("Rescore build").click()
152 self.assertEqual(self.build_url, browser.getLink("Cancel").url)
153 browser.getControl("Priority").value = "1024"
154 browser.getControl("Rescore build").click()
155 self.assertEqual(self.build_url, browser.url)
156 login(ANONYMOUS)
157 self.assertEqual(1024, self.build.buildqueue_record.lastscore)
158
159 def test_rescore_build_invalid_score(self):
160 # Build scores can only take numbers.
161 self.build.queueBuild()
162 transaction.commit()
163 browser = self.getViewBrowser(self.build, user=self.buildd_admin)
164 browser.getLink("Rescore build").click()
165 self.assertEqual(self.build_url, browser.getLink("Cancel").url)
166 browser.getControl("Priority").value = "tentwentyfour"
167 browser.getControl("Rescore build").click()
168 self.assertEqual(
169 "Invalid integer data",
170 extract_text(find_tags_by_class(browser.contents, "message")[1]))
171
172 def test_rescore_build_not_admin(self):
173 # A non-admin user cannot cancel a build.
174 self.build.queueBuild()
175 transaction.commit()
176 user = self.factory.makePerson()
177 browser = self.getViewBrowser(self.build, user=user)
178 self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
179 self.assertRaises(
180 Unauthorized, self.getUserBrowser, self.build_url + "/+rescore",
181 user=user)
182
183 def test_rescore_build_wrong_state(self):
184 # If the build isn't NEEDSBUILD, you can't rescore it.
185 self.build.queueBuild()
186 with person_logged_in(self.requester):
187 self.build.cancel()
188 browser = self.getViewBrowser(self.build, user=self.buildd_admin)
189 self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
190
191 def test_rescore_build_wrong_state_stale_link(self):
192 # An attempt to rescore a non-queued build from a stale link shows a
193 # sensible error message.
194 self.build.queueBuild()
195 with person_logged_in(self.requester):
196 self.build.cancel()
197 browser = self.getViewBrowser(
198 self.build, "+rescore", user=self.buildd_admin)
199 self.assertEqual(self.build_url, browser.url)
200 self.assertIn(
201 "Cannot rescore this build because it is not queued.",
202 browser.contents)
203
204 def test_builder_history(self):
205 Store.of(self.build).flush()
206 self.build.updateStatus(
207 BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder())
208 title = self.build.title
209 browser = self.getViewBrowser(self.build.builder, "+history")
210 self.assertTextMatchesExpressionIgnoreWhitespace(
211 "Build history.*%s" % title,
212 extract_text(find_main_content(browser.contents)))
213 self.assertEqual(self.build_url, browser.getLink(title).url)
214
215 def makeBuildingSnap(self, archive=None):
216 builder = self.factory.makeBuilder()
217 build = self.factory.makeSnapBuild(archive=archive)
218 build.updateStatus(BuildStatus.BUILDING, builder=builder)
219 build.queueBuild()
220 build.buildqueue_record.builder = builder
221 build.buildqueue_record.logtail = "tail of the log"
222 return build
223
224 def test_builder_index_public(self):
225 build = self.makeBuildingSnap()
226 builder_url = canonical_url(build.builder)
227 logout()
228 browser = self.getNonRedirectingBrowser(
229 url=builder_url, user=ANONYMOUS)
230 self.assertIn("tail of the log", browser.contents)
231
232 def test_builder_index_private(self):
233 archive = self.factory.makeArchive(private=True)
234 with admin_logged_in():
235 build = self.makeBuildingSnap(archive=archive)
236 builder_url = canonical_url(build.builder)
237 logout()
238
239 # An unrelated user can't see the logtail of a private build.
240 browser = self.getNonRedirectingBrowser(url=builder_url)
241 self.assertNotIn("tail of the log", browser.contents)
242
243 # But someone who can see the archive can.
244 browser = self.getNonRedirectingBrowser(
245 url=builder_url, user=archive.owner)
246 self.assertIn("tail of the log", browser.contents)
0247
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2015-08-05 10:52:13 +0000
+++ lib/lp/snappy/model/snap.py 2015-08-07 10:52:53 +0000
@@ -21,6 +21,7 @@
21 )21 )
22from zope.component import getUtility22from zope.component import getUtility
23from zope.interface import implementer23from zope.interface import implementer
24from zope.security.proxy import removeSecurityProxy
2425
25from lp.buildmaster.enums import BuildStatus26from lp.buildmaster.enums import BuildStatus
26from lp.buildmaster.interfaces.processor import IProcessorSet27from lp.buildmaster.interfaces.processor import IProcessorSet
@@ -69,7 +70,7 @@
69 This method is registered as a subscriber to `IObjectModifiedEvent`70 This method is registered as a subscriber to `IObjectModifiedEvent`
70 events on snap packages.71 events on snap packages.
71 """72 """
72 snap.date_last_modified = UTC_NOW73 removeSecurityProxy(snap).date_last_modified = UTC_NOW
7374
7475
75@implementer(ISnap, IHasOwner)76@implementer(ISnap, IHasOwner)
7677
=== added directory 'lib/lp/snappy/templates'
=== added file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snap-index.pt 2015-08-07 10:52:53 +0000
@@ -0,0 +1,96 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_side"
7 i18n:domain="launchpad"
8>
9
10<body>
11 <metal:registering fill-slot="registering">
12 Created by
13 <tal:registrant replace="structure context/registrant/fmt:link"/>
14 on
15 <tal:created-on replace="structure context/date_created/fmt:date"/>
16 and last modified on
17 <tal:last-modified replace="structure context/date_last_modified/fmt:date"/>
18 </metal:registering>
19
20 <metal:side fill-slot="side">
21 <div tal:replace="structure context/@@+global-actions"/>
22 </metal:side>
23
24 <metal:heading fill-slot="heading">
25 <h1 tal:content="context/name"/>
26 </metal:heading>
27
28 <div metal:fill-slot="main">
29 <h2>Snap package information</h2>
30 <div class="two-column-list">
31 <dl id="owner">
32 <dt>Owner:</dt>
33 <dd tal:content="structure context/owner/fmt:link"/>
34 </dl>
35 <dl id="distro_series">
36 <dt>Distribution series:</dt>
37 <dd tal:define="distro_series context/distro_series">
38 <a tal:attributes="href distro_series/fmt:url"
39 tal:content="distro_series/fullseriesname"/>
40 </dd>
41 </dl>
42 </div>
43
44 <h2>Latest builds</h2>
45 <table id="latest-builds-listing" class="listing"
46 style="margin-bottom: 1em;">
47 <thead>
48 <tr>
49 <th>Status</th>
50 <th>When complete</th>
51 <th>Architecture</th>
52 <th>Archive</th>
53 </tr>
54 </thead>
55 <tbody>
56 <tal:snap-builds repeat="build view/builds">
57 <tal:build-view define="buildview nocall:build/@@+index">
58 <tr tal:attributes="id string:build-${build/id}">
59 <td>
60 <span tal:replace="structure build/image:icon"/>
61 <a tal:content="build/status/title"
62 tal:attributes="href build/fmt:url"/>
63 </td>
64 <td>
65 <tal:date replace="buildview/date/fmt:displaydate"/>
66 <tal:estimate condition="buildview/estimate">
67 (estimated)
68 </tal:estimate>
69
70 <tal:build-log define="file build/log" tal:condition="file">
71 <a class="sprite download"
72 tal:attributes="href build/log_url">buildlog</a>
73 (<span tal:replace="file/content/filesize/fmt:bytes"/>)
74 </tal:build-log>
75 </td>
76 <td>
77 <a class="sprite distribution"
78 tal:define="archseries build/distro_arch_series"
79 tal:attributes="href archseries/fmt:url"
80 tal:content="archseries/architecturetag"/>
81 </td>
82 <td>
83 <tal:archive replace="structure build/archive/fmt:link"/>
84 </td>
85 </tr>
86 </tal:build-view>
87 </tal:snap-builds>
88 </tbody>
89 </table>
90 <p tal:condition="not: view/builds">
91 This snap package has not been built yet.
92 </p>
93 </div>
94
95</body>
96</html>
097
=== added file 'lib/lp/snappy/templates/snapbuild-index.pt'
--- lib/lp/snappy/templates/snapbuild-index.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snapbuild-index.pt 2015-08-07 10:52:53 +0000
@@ -0,0 +1,205 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10 <body>
11
12 <tal:registering metal:fill-slot="registering">
13 created
14 <span tal:content="context/date_created/fmt:displaydate"
15 tal:attributes="title context/date_created/fmt:datetime"/>
16 </tal:registering>
17
18 <div metal:fill-slot="main">
19
20 <div class="yui-g">
21
22 <div id="status" class="yui-u first">
23 <div class="portlet">
24 <div metal:use-macro="template/macros/status"/>
25 </div>
26 </div>
27
28 <div id="details" class="yui-u">
29 <div class="portlet">
30 <div metal:use-macro="template/macros/details"/>
31 </div>
32 </div>
33
34 </div> <!-- yui-g -->
35
36 <div id="files" class="portlet" tal:condition="view/has_files">
37 <div metal:use-macro="template/macros/files"/>
38 </div>
39
40 <div id="buildlog" class="portlet"
41 tal:condition="context/status/enumvalue:BUILDING">
42 <div metal:use-macro="template/macros/buildlog"/>
43 </div>
44
45 </div> <!-- main -->
46
47
48<metal:macros fill-slot="bogus">
49
50 <metal:macro define-macro="details">
51 <tal:comment replace="nothing">
52 Details section.
53 </tal:comment>
54 <h2>Build details</h2>
55 <div class="two-column-list">
56 <dl>
57 <dt>Snap:</dt>
58 <dd>
59 <tal:snap replace="structure context/snap/fmt:link"/>
60 </dd>
61 </dl>
62 <dl>
63 <dt>Archive:</dt>
64 <dd>
65 <span tal:replace="structure context/archive/fmt:link"/>
66 </dd>
67 </dl>
68 <dl>
69 <dt>Series:</dt>
70 <dd><a class="sprite distribution"
71 tal:define="series context/distro_series"
72 tal:attributes="href series/fmt:url"
73 tal:content="series/displayname"/>
74 </dd>
75 </dl>
76 <dl>
77 <dt>Architecture:</dt>
78 <dd><a class="sprite distribution"
79 tal:define="archseries context/distro_arch_series"
80 tal:attributes="href archseries/fmt:url"
81 tal:content="archseries/architecturetag"/>
82 </dd>
83 </dl>
84 <dl>
85 <dt>Pocket:</dt>
86 <dd><span tal:replace="context/pocket/title"/></dd>
87 </dl>
88 </div>
89 </metal:macro>
90
91 <metal:macro define-macro="status">
92 <tal:comment replace="nothing">
93 Status section.
94 </tal:comment>
95 <h2>Build status</h2>
96 <p>
97 <span tal:replace="structure context/image:icon" />
98 <span tal:attributes="
99 class string:buildstatus${context/status/name};"
100 tal:content="context/status/title"/>
101 <tal:building condition="context/status/enumvalue:BUILDING">
102 on <a tal:content="context/buildqueue_record/builder/title"
103 tal:attributes="href context/buildqueue_record/builder/fmt:url"/>
104 </tal:building>
105 <tal:built condition="context/builder">
106 on <a tal:content="context/builder/title"
107 tal:attributes="href context/builder/fmt:url"/>
108 </tal:built>
109 <tal:cancel define="link context/menu:context/cancel"
110 condition="link/enabled"
111 replace="structure link/fmt:link" />
112 </p>
113
114 <ul>
115 <li tal:condition="context/dependencies">
116 Missing build dependencies: <em tal:content="context/dependencies"/>
117 </li>
118 <tal:reallypending condition="context/buildqueue_record">
119 <tal:pending condition="context/buildqueue_record/status/enumvalue:WAITING">
120 <li tal:define="eta context/buildqueue_record/getEstimatedJobStartTime">
121 Start <tal:eta replace="eta/fmt:approximatedate"/>
122 (<span tal:replace="context/buildqueue_record/lastscore"/>)
123 <a href="https://help.launchpad.net/Packaging/BuildScores"
124 target="_blank">What's this?</a>
125 </li>
126 </tal:pending>
127 </tal:reallypending>
128 <tal:started condition="context/date_started">
129 <li tal:condition="context/date_started">
130 Started <span
131 tal:define="start context/date_started"
132 tal:attributes="title start/fmt:datetime"
133 tal:content="start/fmt:displaydate"/>
134 </li>
135 </tal:started>
136 <tal:finish condition="not: context/date_finished">
137 <li tal:define="eta view/eta" tal:condition="view/eta">
138 Estimated finish <tal:eta replace="eta/fmt:approximatedate"/>
139 </li>
140 </tal:finish>
141
142 <li tal:condition="context/date_finished">
143 Finished <span
144 tal:attributes="title context/date_finished/fmt:datetime"
145 tal:content="context/date_finished/fmt:displaydate"/>
146 <tal:duration condition="context/duration">
147 (took <span tal:replace="context/duration/fmt:exactduration"/>)
148 </tal:duration>
149 </li>
150 <li tal:define="file context/log"
151 tal:condition="file">
152 <a class="sprite download"
153 tal:attributes="href context/log_url">buildlog</a>
154 (<span tal:replace="file/content/filesize/fmt:bytes" />)
155 </li>
156 <li tal:define="file context/upload_log"
157 tal:condition="file">
158 <a class="sprite download"
159 tal:attributes="href context/upload_log_url">uploadlog</a>
160 (<span tal:replace="file/content/filesize/fmt:bytes" />)
161 </li>
162 </ul>
163
164 <div
165 style="margin-top: 1.5em"
166 tal:define="link context/menu:context/rescore"
167 tal:condition="link/enabled"
168 >
169 <a tal:replace="structure link/fmt:link"/>
170 </div>
171 </metal:macro>
172
173 <metal:macro define-macro="files">
174 <tal:comment replace="nothing">
175 Files section.
176 </tal:comment>
177 <h2>Built files</h2>
178 <p>Files resulting from this build:</p>
179 <ul>
180 <li tal:repeat="file view/files">
181 <a class="sprite download"
182 tal:content="file/filename"
183 tal:attributes="href file/http_url"/>
184 (<span tal:replace="file/content/filesize/fmt:bytes"/>)
185 </li>
186 </ul>
187 </metal:macro>
188
189 <metal:macro define-macro="buildlog">
190 <tal:comment replace="nothing">
191 Buildlog section.
192 </tal:comment>
193 <h2>Buildlog</h2>
194 <div id="buildlog-tail" class="logtail"
195 tal:define="logtail context/buildqueue_record/logtail"
196 tal:content="structure logtail/fmt:text-to-html"/>
197 <p class="lesser" tal:condition="view/user">
198 Updated on <span tal:replace="structure view/user/fmt:local-time"/>
199 </p>
200 </metal:macro>
201
202</metal:macros>
203
204 </body>
205</html>