~launchpad-pqm/launchpad/devel

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)}