1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
# 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',
'SourcePackageRecipeBuildCancelView',
'SourcePackageRecipeBuildRescoreView',
]
from zope.interface import Interface
from zope.schema import Int
from lp.app.browser.launchpadform import (
action,
LaunchpadFormView,
)
from lp.buildmaster.enums import BuildStatus
from lp.code.interfaces.sourcepackagerecipebuild import (
ISourcePackageRecipeBuild,
)
from lp.services.job.interfaces.job import JobStatus
from lp.services.librarian.browser import FileNavigationMixin
from lp.services.propertycache import (
cachedproperty,
)
from lp.services.webapp import (
canonical_url,
ContextMenu,
enabled_with_permission,
LaunchpadView,
Link,
Navigation,
)
UNEDITABLE_BUILD_STATES = (
BuildStatus.FULLYBUILT,
BuildStatus.FAILEDTOBUILD,
BuildStatus.SUPERSEDED,
BuildStatus.FAILEDTOUPLOAD,)
class SourcePackageRecipeBuildNavigation(Navigation, FileNavigationMixin):
usedfor = ISourcePackageRecipeBuild
class SourcePackageRecipeBuildContextMenu(ContextMenu):
"""Navigation menu for sourcepackagerecipe build."""
usedfor = ISourcePackageRecipeBuild
facet = 'branches'
links = ('cancel', 'rescore')
@enabled_with_permission('launchpad.Admin')
def cancel(self):
if self.context.status in UNEDITABLE_BUILD_STATES:
enabled = False
else:
enabled = True
return Link('+cancel', 'Cancel build', icon='remove', enabled=enabled)
@enabled_with_permission('launchpad.Admin')
def rescore(self):
if self.context.status in UNEDITABLE_BUILD_STATES:
enabled = False
else:
enabled = True
return Link('+rescore', 'Rescore build', icon='edit', enabled=enabled)
class SourcePackageRecipeBuildView(LaunchpadView):
"""Default view of a SourcePackageRecipeBuild."""
@property
def status(self):
"""A human-friendly status string."""
if (self.context.status == BuildStatus.NEEDSBUILD
and self.eta is None):
return 'No suitable builders'
return {
BuildStatus.NEEDSBUILD: 'Pending build',
BuildStatus.UPLOADING: 'Build uploading',
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.status, self.context.status.title)
@cachedproperty
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
@cachedproperty
def date(self):
"""The date when the build completed or is estimated to complete."""
if self.estimate:
return self.eta
return self.context.date_finished
@cachedproperty
def estimate(self):
"""If true, the date value is an estimate."""
if self.context.date_finished is not None:
return False
return self.eta is not None
def binary_builds(self):
return list(self.context.binary_builds)
class SourcePackageRecipeBuildCancelView(LaunchpadFormView):
"""View for cancelling a build."""
class schema(Interface):
"""Schema for cancelling a build."""
page_title = label = "Cancel build"
@property
def cancel_url(self):
return canonical_url(self.context)
next_url = cancel_url
@action('Cancel build', name='cancel')
def request_action(self, action, data):
"""Cancel the build."""
self.context.cancelBuild()
class SourcePackageRecipeBuildRescoreView(LaunchpadFormView):
"""View for rescoring a build."""
class schema(Interface):
"""Schema for deleting a build."""
score = Int(
title=u'Score', required=True,
description=u'The score of the recipe.')
page_title = label = "Rescore build"
def __call__(self):
if self.context.buildqueue_record is not None:
return super(SourcePackageRecipeBuildRescoreView, self).__call__()
self.request.response.addWarningNotification(
'Cannot rescore this build because it is not queued.')
self.request.response.redirect(canonical_url(self.context))
@property
def cancel_url(self):
return canonical_url(self.context)
next_url = cancel_url
@action('Rescore build', name='rescore')
def request_action(self, action, data):
"""Rescore the build."""
self.context.buildqueue_record.lastscore = int(data['score'])
@property
def initial_values(self):
return {'score': str(self.context.buildqueue_record.lastscore)}
|