~launchpad-pqm/launchpad/devel

14625.1.1 by Steve Kowalik
Hide SPRBs that the user can't see in the SPRecipe views.
1
# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
10498.3.1 by Paul Hummer
Added empty browser file for ISourcePackageRecipe
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""SourcePackageRecipe views."""
5
6
__metaclass__ = type
7
7675.618.39 by Paul Hummer
Added a recipe edit view.
8
__all__ = [
7675.618.40 by Paul Hummer
Fixed __all__ in lp.code.browser.sourcepackagerecipe
9
    'SourcePackageRecipeAddView',
10
    'SourcePackageRecipeContextMenu',
7675.618.45 by Paul Hummer
Fixed some lint
11
    'SourcePackageRecipeEditView',
7675.618.39 by Paul Hummer
Added a recipe edit view.
12
    'SourcePackageRecipeNavigationMenu',
7675.618.40 by Paul Hummer
Fixed __all__ in lp.code.browser.sourcepackagerecipe
13
    'SourcePackageRecipeRequestBuildsView',
14
    'SourcePackageRecipeView',
7675.618.39 by Paul Hummer
Added a recipe edit view.
15
    ]
10498.3.6 by Aaron Bentley
Initial cut of index page.
16
12373.2.1 by Tim Penhey
First hack.
17
import itertools
12013.4.10 by Ian Booth
Lint issues
18
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
19
from bzrlib.plugins.builder.recipe import (
11476.1.2 by Aaron Bentley
Use permitted_instructions when parsing.
20
    ForbiddenInstructionError,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
21
    RecipeParseError,
22
    RecipeParser,
23
    )
7675.622.3 by Paul Hummer
Added update notification code.
24
from lazr.lifecycle.event import ObjectModifiedEvent
25
from lazr.lifecycle.snapshot import Snapshot
13314.13.2 by Ian Booth
Lint
26
from lazr.restful.interface import (
27
    copy_field,
28
    use_template,
29
    )
12547.1.5 by Ian Booth
Fix imports
30
from lazr.restful.interfaces import (
31
    IFieldHTMLRenderer,
32
    IWebServiceClientRequest,
33
    )
12442.2.9 by j.c.sackett
Ran import reformatter per review.
34
import simplejson
11043.2.4 by Paul Hummer
No commits in view code
35
from storm.locals import Store
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
36
from z3c.ptcompat import ViewPageTemplateFile
12547.1.5 by Ian Booth
Fix imports
37
from zope import component
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
38
from zope.app.form.browser.widget import Widget
39
from zope.app.form.interfaces import IView
10498.5.1 by Aaron Bentley
Get distroseries listing displaying.
40
from zope.component import getUtility
7675.622.3 by Paul Hummer
Added update notification code.
41
from zope.event import notify
11814.1.2 by Paul Hummer
Fixed teh bug
42
from zope.formlib import form
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
43
from zope.interface import (
12547.1.5 by Ian Booth
Fix imports
44
    implementer,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
45
    implements,
46
    Interface,
47
    providedBy,
48
    )
49
from zope.schema import (
12442.2.9 by j.c.sackett
Ran import reformatter per review.
50
    Choice,
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
51
    Field,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
52
    List,
53
    Text,
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
54
    TextLine,
55
    )
12547.1.5 by Ian Booth
Fix imports
56
from zope.schema.interfaces import ICollection
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
57
from zope.schema.vocabulary import (
58
    SimpleTerm,
59
    SimpleVocabulary,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
60
    )
12599.4.2 by Leonard Richardson
Merge from trunk.
61
from zope.security.proxy import isinstance as zope_isinstance
10498.5.1 by Aaron Bentley
Get distroseries listing displaying.
62
14600.1.12 by Curtis Hovey
Move i18n to lp.
63
from lp import _
13130.1.12 by Curtis Hovey
Sorted imports.
64
from lp.app.browser.launchpad import Hierarchy
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
65
from lp.app.browser.launchpadform import (
66
    action,
67
    custom_widget,
11929.11.6 by Tim Penhey
Allow forms to specify that some fields have structured widget help.
68
    has_structured_doc,
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
69
    LaunchpadEditFormView,
70
    LaunchpadFormView,
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
71
    render_radio_widget_part,
11929.9.1 by Tim Penhey
Move launchpadform into lp.app.browser.
72
    )
12261.2.2 by Tim Penhey
Merge the refactoring branch, and fix the new declarations.
73
from lp.app.browser.lazrjs import (
12344.2.1 by Tim Penhey
Add boolean choice widget.
74
    BooleanChoiceWidget,
12261.2.2 by Tim Penhey
Merge the refactoring branch, and fix the new declarations.
75
    InlineEditPickerWidget,
13429.1.2 by Ian Booth
Add meta attribute support to picker widgets
76
    InlinePersonEditPickerWidget,
12261.2.2 by Tim Penhey
Merge the refactoring branch, and fix the new declarations.
77
    TextAreaEditorWidget,
12421.3.1 by Tim Penhey
Add inline editing of the recipe name.
78
    TextLineEditorWidget,
12261.2.2 by Tim Penhey
Merge the refactoring branch, and fix the new declarations.
79
    )
12547.1.7 by Ian Booth
Refactor spr interface and improve rendering
80
from lp.app.browser.tales import format_link
12442.2.9 by j.c.sackett
Ran import reformatter per review.
81
from lp.app.validators.name import name_validator
12293.1.10 by Curtis Hovey
Formatted imports.
82
from lp.app.widgets.itemswidgets import (
83
    LabeledMultiCheckBoxWidget,
84
    LaunchpadRadioWidget,
85
    )
86
from lp.app.widgets.suggestion import RecipeOwnerWidget
11236.1.2 by Aaron Bentley
Handle PrivateBranchRecipe as a user error in the web UI.
87
from lp.code.errors import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
88
    BuildAlreadyPending,
89
    NoSuchBranch,
90
    PrivateBranchRecipe,
13824.2.1 by Brad Crittenden
Catch TooManyBuild exception rather than OOPS
91
    TooManyBuilds,
11262.3.2 by Curtis Hovey
Merged devel.
92
    TooNewRecipeFormat,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
93
    )
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
94
from lp.code.interfaces.branchtarget import IBranchTarget
10744.5.3 by Paul Hummer
Got a working create recipe flow
95
from lp.code.interfaces.sourcepackagerecipe import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
96
    ISourcePackageRecipe,
97
    ISourcePackageRecipeSource,
98
    MINIMAL_RECIPE_TEXT,
12373.1.3 by Tim Penhey
Lint cleanup.
99
    )
12599.4.2 by Leonard Richardson
Merge from trunk.
100
from lp.code.model.branchtarget import PersonBranchTarget
12373.2.8 by Tim Penhey
Set initial distroseries, and make description required again (needs db patch to remove not null constraint).
101
from lp.code.model.sourcepackagerecipe import get_buildable_distroseries_set
102
from lp.registry.interfaces.series import SeriesStatus
13378.1.1 by j.c.sackett
Undid the rollback by wgrant to get the functionality we need from wallyworld's branch.
103
from lp.services.fields import PersonChoice
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
104
from lp.services.propertycache import cachedproperty
14612.2.1 by William Grant
format-imports on lib/. So many imports.
105
from lp.services.webapp import (
106
    canonical_url,
107
    ContextMenu,
108
    enabled_with_permission,
109
    LaunchpadView,
110
    Link,
111
    NavigationMenu,
112
    structured,
113
    )
114
from lp.services.webapp.authorization import check_permission
115
from lp.services.webapp.breadcrumb import Breadcrumb
13912.4.2 by Aaron Bentley
Handle ArchiveDisabled as user error.
116
from lp.soyuz.interfaces.archive import ArchiveDisabled
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
117
from lp.soyuz.model.archive import Archive
10498.3.22 by Aaron Bentley
Restrict the number of old builds shown.
118
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
119
7675.618.59 by Paul Hummer
Responded to Tim's review
120
class IRecipesForPerson(Interface):
7675.618.56 by Paul Hummer
Got the breadcrumbs working.
121
    """A marker interface for source package recipe sets."""
122
123
7675.618.59 by Paul Hummer
Responded to Tim's review
124
class RecipesForPersonBreadcrumb(Breadcrumb):
10788.2.2 by Aaron Bentley
Fix lint errors.
125
    """A Breadcrumb to handle the "Recipes" link for recipe breadcrumbs."""
7675.618.56 by Paul Hummer
Got the breadcrumbs working.
126
127
    rootsite = 'code'
128
    text = 'Recipes'
129
7675.618.59 by Paul Hummer
Responded to Tim's review
130
    implements(IRecipesForPerson)
131
7675.618.56 by Paul Hummer
Got the breadcrumbs working.
132
    @property
133
    def url(self):
11318.8.9 by Tim Penhey
Root site of code needed on recipe views.
134
        return canonical_url(
135
            self.context, view_name="+recipes", rootsite='code')
7675.618.56 by Paul Hummer
Got the breadcrumbs working.
136
137
7675.618.55 by Paul Hummer
Added breadcrumb code for the source package recipes
138
class SourcePackageRecipeHierarchy(Hierarchy):
13824.2.1 by Brad Crittenden
Catch TooManyBuild exception rather than OOPS
139
    """Hierarchy for Source Package Recipe."""
7675.618.55 by Paul Hummer
Added breadcrumb code for the source package recipes
140
141
    vhost_breadcrumb = False
142
143
    @property
144
    def objects(self):
145
        """See `Hierarchy`."""
146
        traversed = list(self.request.traversed_objects)
147
148
        # Pop the root object
149
        yield traversed.pop(0)
150
151
        recipe = traversed.pop(0)
152
        while not ISourcePackageRecipe.providedBy(recipe):
153
            yield recipe
154
            recipe = traversed.pop(0)
7675.618.56 by Paul Hummer
Got the breadcrumbs working.
155
156
        # Pop in the "Recipes" link to recipe listings.
7675.618.59 by Paul Hummer
Responded to Tim's review
157
        yield RecipesForPersonBreadcrumb(recipe.owner)
7675.618.55 by Paul Hummer
Added breadcrumb code for the source package recipes
158
        yield recipe
159
160
        for item in traversed:
161
            yield item
162
163
7675.618.39 by Paul Hummer
Added a recipe edit view.
164
class SourcePackageRecipeNavigationMenu(NavigationMenu):
165
    """Navigation menu for sourcepackage recipes."""
166
167
    usedfor = ISourcePackageRecipe
168
169
    facet = 'branches'
170
7675.618.47 by Paul Hummer
Added delete view
171
    links = ('edit', 'delete')
7675.618.39 by Paul Hummer
Added a recipe edit view.
172
173
    @enabled_with_permission('launchpad.Edit')
174
    def edit(self):
175
        return Link('+edit', 'Edit recipe', icon='edit')
176
7675.618.47 by Paul Hummer
Added delete view
177
    @enabled_with_permission('launchpad.Edit')
178
    def delete(self):
179
        return Link('+delete', 'Delete recipe', icon='trash-icon')
180
7675.618.39 by Paul Hummer
Added a recipe edit view.
181
10498.6.1 by Aaron Bentley
Use table for build listing, generate link properly.
182
class SourcePackageRecipeContextMenu(ContextMenu):
183
    """Context menu for sourcepackage recipes."""
184
185
    usedfor = ISourcePackageRecipe
186
187
    facet = 'branches'
188
12378.2.3 by Ian Booth
Initial implementation
189
    links = ('request_builds', 'request_daily_build',)
10498.6.1 by Aaron Bentley
Use table for build listing, generate link properly.
190
191
    def request_builds(self):
10498.5.16 by Aaron Bentley
Update docs
192
        """Provide a link for requesting builds of a recipe."""
10498.6.1 by Aaron Bentley
Use table for build listing, generate link properly.
193
        return Link('+request-builds', 'Request build(s)', icon='add')
194
12378.2.3 by Ian Booth
Initial implementation
195
    def request_daily_build(self):
196
        """Provide a link for requesting a daily build of a recipe."""
197
        recipe = self.context
198
        ppa = recipe.daily_build_archive
13912.4.1 by Aaron Bentley
Disabling archive disables daily build link.
199
        if (ppa is None or not ppa.enabled or not recipe.build_daily or not
200
            recipe.is_stale or not recipe.distroseries):
12378.2.3 by Ian Booth
Initial implementation
201
            show_request_build = False
202
        else:
203
            has_upload = ppa.checkArchivePermission(recipe.owner)
204
            show_request_build = has_upload
205
13429.1.3 by Ian Booth
Lint
206
        show_request_build = (show_request_build and
12941.2.1 by Ian Booth
Do not show recipie build now if user does not have edit permission on recipe
207
            check_permission('launchpad.Edit', recipe))
12378.2.3 by Ian Booth
Initial implementation
208
        return Link(
209
                '+request-daily-build', 'Build now',
210
                enabled=show_request_build)
211
10498.6.1 by Aaron Bentley
Use table for build listing, generate link properly.
212
10498.3.6 by Aaron Bentley
Initial cut of index page.
213
class SourcePackageRecipeView(LaunchpadView):
214
    """Default view of a SourcePackageRecipe."""
215
11049.5.2 by Paul Hummer
Added Warning notification
216
    def initialize(self):
217
        super(SourcePackageRecipeView, self).initialize()
12177.4.1 by Tim Penhey
Show a message on the main recipe page if the recipe owner can't upload into the daily ppa.
218
        recipe = self.context
12221.9.19 by Tim Penhey
Handle missing PPAs in the view.
219
        if recipe.build_daily and recipe.daily_build_archive is None:
220
            self.request.response.addWarningNotification(
221
                structured(
222
                    "Daily builds for this recipe will <strong>not</strong> "
223
                    "occur.<br/><br/>There is no PPA."))
224
        elif self.dailyBuildWithoutUploadPermission():
12177.4.1 by Tim Penhey
Show a message on the main recipe page if the recipe owner can't upload into the daily ppa.
225
            self.request.response.addWarningNotification(
226
                structured(
227
                    "Daily builds for this recipe will <strong>not</strong> "
228
                    "occur.<br/><br/>The owner of the recipe (%s) does not "
229
                    "have permission to upload packages into the daily "
230
                    "build PPA (%s)" % (
231
                        format_link(recipe.owner),
232
                        format_link(recipe.daily_build_archive))))
11049.5.2 by Paul Hummer
Added Warning notification
233
10498.3.8 by Aaron Bentley
Get index page looking close to intended display.
234
    @property
7675.618.55 by Paul Hummer
Added breadcrumb code for the source package recipes
235
    def page_title(self):
7675.618.52 by Paul Hummer
Fixed the title
236
        return "%(name)s\'s %(recipe_name)s recipe" % {
237
            'name': self.context.owner.displayname,
238
            'recipe_name': self.context.name}
10498.3.8 by Aaron Bentley
Get index page looking close to intended display.
239
7675.618.55 by Paul Hummer
Added breadcrumb code for the source package recipes
240
    label = page_title
10498.3.6 by Aaron Bentley
Initial cut of index page.
241
10498.3.22 by Aaron Bentley
Restrict the number of old builds shown.
242
    @property
243
    def builds(self):
12013.4.6 by Ian Booth
Hook up validation errors
244
        return builds_for_recipe(self.context)
10498.3.22 by Aaron Bentley
Restrict the number of old builds shown.
245
12177.4.1 by Tim Penhey
Show a message on the main recipe page if the recipe owner can't upload into the daily ppa.
246
    def dailyBuildWithoutUploadPermission(self):
247
        """Returns true if there are upload permissions to the daily archive.
248
249
        If the recipe isn't built daily, we don't consider this a problem.
250
        """
251
        recipe = self.context
252
        ppa = recipe.daily_build_archive
253
        if recipe.build_daily:
254
            has_upload = ppa.checkArchivePermission(recipe.owner)
255
            return not has_upload
256
        return False
257
12221.10.2 by Tim Penhey
Move code to use lazrjs.InlineEditPickerWidget.
258
    @property
259
    def person_picker(self):
13314.13.1 by Ian Booth
Fix implementation - use custom vocab
260
        field = copy_field(
14174.1.1 by Ian Booth
Remove picker feature flags
261
            ISourcePackageRecipe['owner'],
262
            vocabularyName='UserTeamsParticipationPlusSelfSimpleDisplay')
13429.1.2 by Ian Booth
Add meta attribute support to picker widgets
263
        return InlinePersonEditPickerWidget(
13314.13.1 by Ian Booth
Fix implementation - use custom vocab
264
            self.context, field,
12221.10.2 by Tim Penhey
Move code to use lazrjs.InlineEditPickerWidget.
265
            format_link(self.context.owner),
266
            header='Change owner',
267
            step_title='Select a new owner')
268
12221.9.21 by Tim Penhey
Move the archive to use the InlineEditPickerWidget.
269
    @property
270
    def archive_picker(self):
12268.3.28 by Tim Penhey
Don't allow the removal of a recipe PPA.
271
        field = ISourcePackageEditSchema['daily_build_archive']
12221.9.21 by Tim Penhey
Move the archive to use the InlineEditPickerWidget.
272
        return InlineEditPickerWidget(
12397.1.5 by Ian Booth
Add support for specifying default values for ObjectFormatterAPI when context is None
273
            self.context, field,
274
            format_link(self.context.daily_build_archive),
12221.9.21 by Tim Penhey
Move the archive to use the InlineEditPickerWidget.
275
            header='Change daily build archive',
276
            step_title='Select a PPA')
277
12261.2.1 by Tim Penhey
initial hack.
278
    @property
12261.2.9 by Tim Penhey
Style and widget fixes.
279
    def recipe_text_widget(self):
12261.2.1 by Tim Penhey
initial hack.
280
        """The recipe text as widget HTML."""
12261.2.2 by Tim Penhey
Merge the refactoring branch, and fix the new declarations.
281
        recipe_text = ISourcePackageRecipe['recipe_text']
12261.2.9 by Tim Penhey
Style and widget fixes.
282
        return TextAreaEditorWidget(self.context, recipe_text, title="")
12261.2.1 by Tim Penhey
initial hack.
283
12261.2.7 by Tim Penhey
Merge daily-ajax.
284
    @property
12344.2.1 by Tim Penhey
Add boolean choice widget.
285
    def daily_build_widget(self):
286
        return BooleanChoiceWidget(
287
            self.context, ISourcePackageRecipe['build_daily'],
12344.2.3 by Tim Penhey
Update the lazr-js-widgets documentation to include the BooleanChoiceWidget.
288
            tag='span',
12344.2.13 by Tim Penhey
Use 'built' rather than 'build' when talking about the build schedule.
289
            false_text='Built on request',
290
            true_text='Built daily',
12344.2.2 by Tim Penhey
Get it all working.
291
            header='Change build schedule')
12261.2.1 by Tim Penhey
initial hack.
292
12261.3.1 by Tim Penhey
Interim work on editing the description.
293
    @property
294
    def description_widget(self):
295
        """The description as a widget."""
296
        description = ISourcePackageRecipe['description']
297
        return TextAreaEditorWidget(
298
            self.context, description, title="")
299
12421.3.1 by Tim Penhey
Add inline editing of the recipe name.
300
    @property
301
    def name_widget(self):
302
        name = ISourcePackageRecipe['name']
303
        title = "Edit the recipe name"
304
        return TextLineEditorWidget(self.context, name, title, 'h1')
305
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
306
    @property
307
    def distroseries_widget(self):
308
        from lp.app.browser.lazrjs import InlineMultiCheckboxWidget
309
        field = ISourcePackageEditSchema['distroseries']
310
        return InlineMultiCheckboxWidget(
311
            self.context,
312
            field,
313
            attribute_type="reference",
12547.1.7 by Ian Booth
Refactor spr interface and improve rendering
314
            vocabulary='BuildableDistroSeries',
12547.1.34 by Ian Booth
Text fixes
315
            label="Distribution series:",
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
316
            label_tag="dt",
317
            header="Change default distribution series:",
318
            empty_display_value="None",
12547.1.14 by Ian Booth
Fix display order and add windmill test
319
            selected_items=sorted(
320
                self.context.distroseries, key=lambda ds: ds.displayname),
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
321
            items_tag="dd",
322
            )
323
324
12547.1.21 by Ian Booth
Lint
325
@component.adapter(ISourcePackageRecipe, ICollection,
326
                   IWebServiceClientRequest)
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
327
@implementer(IFieldHTMLRenderer)
328
def distroseries_renderer(context, field, request):
12547.1.5 by Ian Booth
Fix imports
329
    """Render a distroseries collection as a set of links."""
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
330
331
    def render(value):
12547.1.14 by Ian Booth
Fix display order and add windmill test
332
        distroseries = sorted(
333
            context.distroseries, key=lambda ds: ds.displayname)
12547.1.9 by Ian Booth
Fix distroseries rendering for empty values
334
        if not distroseries:
335
            return 'None'
12547.1.6 by Ian Booth
Complete renderer
336
        html = "<ul>"
337
        html += ''.join(
12547.1.21 by Ian Booth
Lint
338
            ["<li>%s</li>" % format_link(series) for series in distroseries])
12547.1.6 by Ian Booth
Complete renderer
339
        html += "</ul>"
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
340
        return html
341
    return render
10498.3.8 by Aaron Bentley
Get index page looking close to intended display.
342
12547.1.21 by Ian Booth
Lint
343
12013.4.6 by Ian Booth
Hook up validation errors
344
def builds_for_recipe(recipe):
345
        """A list of interesting builds.
346
347
        All pending builds are shown, as well as 1-5 recent builds.
348
        Recent builds are ordered by date finished (if completed) or
349
        date_started (if date finished is not set due to an error building or
350
        other circumstance which resulted in the build not being completed).
351
        This allows started but unfinished builds to show up in the view but
352
        be discarded as more recent builds become available.
14625.1.1 by Steve Kowalik
Hide SPRBs that the user can't see in the SPRecipe views.
353
354
        Builds that the user does not have permission to see are excluded.
12013.4.6 by Ian Booth
Hook up validation errors
355
        """
14625.1.1 by Steve Kowalik
Hide SPRBs that the user can't see in the SPRecipe views.
356
        builds = [build for build in recipe.pending_builds
357
            if check_permission('launchpad.View', build)]
12397.2.8 by Ian Booth
Change from using getter methods to properties for exported recipe and build accessors
358
        for build in recipe.completed_builds:
14625.1.1 by Steve Kowalik
Hide SPRBs that the user can't see in the SPRecipe views.
359
            if not check_permission('launchpad.View', build):
360
                continue
12013.4.6 by Ian Booth
Hook up validation errors
361
            builds.append(build)
362
            if len(builds) >= 5:
363
                break
364
        return builds
365
366
12378.3.13 by Ian Booth
Fix typo in test
367
def new_builds_notification_text(builds, already_pending=None):
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
368
    nr_builds = len(builds)
12378.2.17 by Ian Booth
Add info message for ajax build now requests
369
    if not nr_builds:
370
        builds_text = "All requested recipe builds are already queued."
371
    elif nr_builds == 1:
372
        builds_text = "1 new recipe build has been queued."
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
373
    else:
12378.2.17 by Ian Booth
Add info message for ajax build now requests
374
        builds_text = "%d new recipe builds have been queued." % nr_builds
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
375
    if nr_builds > 0 and already_pending:
376
        builds_text = "<p>%s</p>%s" % (builds_text, already_pending)
377
    return structured(builds_text)
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
378
379
10498.5.1 by Aaron Bentley
Get distroseries listing displaying.
380
class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):
381
    """A view for requesting builds of a SourcePackageRecipe."""
382
383
    @property
384
    def initial_values(self):
10498.5.16 by Aaron Bentley
Update docs
385
        """Set initial values for the widgets.
386
387
        The distroseries function as defaults for requesting a build.
388
        """
12547.1.36 by Ian Booth
Test fixes
389
        initial_values = {'distroseries': self.context.distroseries}
12397.2.8 by Ian Booth
Change from using getter methods to properties for exported recipe and build accessors
390
        build = self.context.last_build
14625.1.1 by Steve Kowalik
Hide SPRBs that the user can't see in the SPRecipe views.
391
        if build:
14625.1.2 by Steve Kowalik
Shift the check_permission() call under if build.
392
            # If the build can't be viewed, the archive can't.
393
            if check_permission('launchpad.View', build):
394
                initial_values['archive'] = build.archive
10875.2.1 by Aaron Bentley
Use last build archive as initial value
395
        return initial_values
10498.7.1 by Aaron Bentley
Require each distroseries to have at least one enabled architecture.
396
10498.5.19 by Aaron Bentley
Convert schema to use vocabulary utilities.
397
    class schema(Interface):
398
        """Schema for requesting a build."""
12013.4.2 by Ian Booth
Initial implementation
399
        archive = Choice(vocabulary='TargetPPAs', title=u'Archive')
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
400
        distroseries = List(
10498.5.19 by Aaron Bentley
Convert schema to use vocabulary utilities.
401
            Choice(vocabulary='BuildableDistroSeries'),
402
            title=u'Distribution series')
10498.5.1 by Aaron Bentley
Get distroseries listing displaying.
403
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
404
    custom_widget('distroseries', LabeledMultiCheckBoxWidget)
10498.5.1 by Aaron Bentley
Get distroseries listing displaying.
405
10936.6.2 by Aaron Bentley
Handle over-quota builds
406
    def validate(self, data):
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
407
        distros = data.get('distroseries', [])
12013.4.3 by Ian Booth
Factor out sepatate view for builds table aned update from ajax call
408
        if not len(distros):
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
409
            self.setFieldError('distroseries',
12013.4.6 by Ian Booth
Hook up validation errors
410
                "You need to specify at least one distro series for which "
411
                "to build.")
12013.4.3 by Ian Booth
Factor out sepatate view for builds table aned update from ajax call
412
            return
12013.4.6 by Ian Booth
Hook up validation errors
413
        over_quota_distroseries = []
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
414
        for distroseries in data['distroseries']:
10936.6.2 by Aaron Bentley
Handle over-quota builds
415
            if self.context.isOverQuota(self.user, distroseries):
416
                over_quota_distroseries.append(str(distroseries))
417
        if len(over_quota_distroseries) > 0:
418
            self.setFieldError(
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
419
                'distroseries',
10936.6.2 by Aaron Bentley
Handle over-quota builds
420
                "You have exceeded today's quota for %s." %
421
                ', '.join(over_quota_distroseries))
422
12013.4.6 by Ian Booth
Hook up validation errors
423
    def requestBuild(self, data):
12013.4.15 by Ian Booth
Code review fixes
424
        """User action for requesting a number of builds.
425
426
        We raise exceptions for most errors but if there's already a pending
427
        build for a particular distroseries, we simply record that so that
428
        other builds can ne queued and a message be displayed to the caller.
429
        """
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
430
        informational = {}
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
431
        builds = []
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
432
        for distroseries in data['distroseries']:
7675.729.3 by Aaron Bentley
Handle identical builds in the UI.
433
            try:
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
434
                build = self.context.requestBuild(
12013.4.2 by Ian Booth
Initial implementation
435
                    data['archive'], self.user, distroseries, manual=True)
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
436
                builds.append(build)
7675.729.3 by Aaron Bentley
Handle identical builds in the UI.
437
            except BuildAlreadyPending, e:
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
438
                existing_message = informational.get("already_pending")
439
                if existing_message:
440
                    new_message = existing_message[:-1] + (
12378.3.1 by Ian Booth
Add code to display partial request build successes
441
                                    ", and %s." % e.distroseries)
442
                else:
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
443
                    new_message = ("An identical build is "
12378.3.1 by Ian Booth
Add code to display partial request build successes
444
                                "already pending for %s." % e.distroseries)
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
445
                informational["already_pending"] = new_message
12378.3.7 by Ian Booth
Add windmill test
446
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
447
        return builds, informational
12013.4.6 by Ian Booth
Hook up validation errors
448
12013.4.10 by Ian Booth
Lint issues
449
12013.4.6 by Ian Booth
Hook up validation errors
450
class SourcePackageRecipeRequestBuildsHtmlView(
451
        SourcePackageRecipeRequestBuildsView):
452
    """Supports HTML form recipe build requests."""
453
454
    @property
455
    def title(self):
456
        return 'Request builds for %s' % self.context.name
457
458
    label = title
459
460
    @property
461
    def cancel_url(self):
462
        return canonical_url(self.context)
463
464
    @action('Request builds', name='request')
465
    def request_action(self, action, data):
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
466
        builds, informational = self.requestBuild(data)
12013.4.6 by Ian Booth
Hook up validation errors
467
        self.next_url = self.cancel_url
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
468
        already_pending = informational.get("already_pending")
469
        notification_text = new_builds_notification_text(
470
            builds, already_pending)
471
        self.request.response.addNotification(notification_text)
12013.4.6 by Ian Booth
Hook up validation errors
472
473
474
class SourcePackageRecipeRequestBuildsAjaxView(
475
        SourcePackageRecipeRequestBuildsView):
476
    """Supports AJAX form recipe build requests."""
477
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
478
    def _process_error(self, data=None, builds=None, informational=None,
479
                       errors=None, reason="Validation"):
12013.4.15 by Ian Booth
Code review fixes
480
        """Set up the response and json data to return to the caller."""
13662.7.93 by Henning Eggers
Refactored request build.
481
        self.request.response.setStatus(200, reason)
12013.4.6 by Ian Booth
Hook up validation errors
482
        self.request.response.setHeader('Content-type', 'application/json')
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
483
        return_data = dict(builds=builds, errors=errors)
484
        if informational:
485
            return_data.update(informational)
486
        return simplejson.dumps(return_data)
12013.4.6 by Ian Booth
Hook up validation errors
487
488
    def failure(self, action, data, errors):
12013.4.15 by Ian Booth
Code review fixes
489
        """Called by the form if validate() finds any errors.
490
491
           We simply convert the errors to json and return that data to the
492
           caller for display to the user.
493
        """
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
494
        return self._process_error(data=data, errors=self.widget_errors)
12013.4.6 by Ian Booth
Hook up validation errors
495
496
    @action('Request builds', name='request', failure=failure)
497
    def request_action(self, action, data):
12013.4.15 by Ian Booth
Code review fixes
498
        """User action for requesting a number of builds.
499
500
        The failure handler will handle any validation errors. We still need
501
        to handle errors which may occur when invoking the business logic.
502
        These "expected" errors are ones which result in a predefined message
503
        being displayed to the user. If the business method raises an
504
        unexpected exception, that will be handled using the form's standard
505
        exception processing mechanism (using response code 500).
506
        """
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
507
        builds, informational = self.requestBuild(data)
12013.4.8 by Ian Booth
Add windmill tests
508
        # If there are errors we return a json data snippet containing the
12378.3.5 by Ian Booth
Better informational/error message display plus remove dup code
509
        # errors as well as the form content. These errors are processed
510
        # by the caller's response handler and displayed to the user. The
511
        # form content may be rendered as well if required.
512
        if informational:
513
            builds_html = None
514
            if len(builds):
515
                builds_html = self.render()
516
            return self._process_error(
517
                data=data, builds=builds_html, informational=informational,
518
                reason="Request Build")
12013.4.6 by Ian Booth
Hook up validation errors
519
520
    @property
521
    def builds(self):
522
        return builds_for_recipe(self.context)
7675.729.3 by Aaron Bentley
Handle identical builds in the UI.
523
10795.6.13 by Aaron Bentley
Show binary builds resulting from sourcepackagebuild.
524
12378.2.5 by Ian Booth
Add tests and support for non-ajax forms
525
class SourcePackageRecipeRequestDailyBuildView(LaunchpadFormView):
12378.2.3 by Ian Booth
Initial implementation
526
    """Supports requests to perform a daily build for a recipe.
527
528
    Renders the recipe builds table so that the recipe index page can be
529
    updated with the new build records.
12378.2.5 by Ian Booth
Add tests and support for non-ajax forms
530
531
    This view works for both ajax and html form requests.
12378.2.3 by Ian Booth
Initial implementation
532
    """
533
12378.2.5 by Ian Booth
Add tests and support for non-ajax forms
534
    # Attributes for the html version
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
535
    page_title = "Build now"
12378.2.5 by Ian Booth
Add tests and support for non-ajax forms
536
537
    class schema(Interface):
538
        """Schema for requesting a build."""
539
540
    @action('Build now', name='build')
541
    def build_action(self, action, data):
12378.2.3 by Ian Booth
Initial implementation
542
        recipe = self.context
13824.2.1 by Brad Crittenden
Catch TooManyBuild exception rather than OOPS
543
        try:
544
            builds = recipe.performDailyBuild()
13912.4.2 by Aaron Bentley
Handle ArchiveDisabled as user error.
545
        except (TooManyBuilds, ArchiveDisabled) as e:
13824.2.1 by Brad Crittenden
Catch TooManyBuild exception rather than OOPS
546
            self.request.response.addErrorNotification(str(e))
547
            self.next_url = canonical_url(recipe)
548
            return
549
12378.2.5 by Ian Booth
Add tests and support for non-ajax forms
550
        if self.request.is_ajax:
551
            template = ViewPageTemplateFile(
552
                    "../templates/sourcepackagerecipe-builds.pt")
553
            return template(self)
554
        else:
555
            self.next_url = canonical_url(recipe)
12378.2.11 by Ian Booth
Code review changes plus add new build notification for non-ajax version
556
            self.request.response.addNotification(
557
                    new_builds_notification_text(builds))
12378.2.3 by Ian Booth
Initial implementation
558
559
    @property
560
    def builds(self):
561
        return builds_for_recipe(self.context)
562
563
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
564
class ISourcePackageEditSchema(Interface):
7675.618.46 by Paul Hummer
Fixed indentation
565
    """Schema for adding or editing a recipe."""
7675.618.41 by Paul Hummer
Got a working edit
566
7675.618.46 by Paul Hummer
Fixed indentation
567
    use_template(ISourcePackageRecipe, include=[
568
        'name',
569
        'description',
570
        'owner',
10409.5.105 by Curtis Hovey
Merged devel, resolved conflicts.
571
        'build_daily',
12547.1.7 by Ian Booth
Refactor spr interface and improve rendering
572
        'distroseries',
7675.618.46 by Paul Hummer
Fixed indentation
573
        ])
10899.4.1 by Aaron Bentley
Initial UI for source package recipe builds.
574
    daily_build_archive = Choice(vocabulary='TargetPPAs',
11862.1.4 by Paul Hummer
Fixed some missing descriptions
575
        title=u'Daily build archive',
576
        description=(
11862.1.5 by Paul Hummer
Made deryck's suggested change
577
            u'If built daily, this is the archive where the package '
11862.1.4 by Paul Hummer
Fixed some missing descriptions
578
            u'will be uploaded.'))
11929.11.6 by Tim Penhey
Allow forms to specify that some fields have structured widget help.
579
    recipe_text = has_structured_doc(
580
        Text(
581
            title=u'Recipe text', required=True,
14354.1.1 by Francis J. Lacoste
Revert sourcerecipe help link change.
582
            description=u"""The text of the recipe.
14388.1.5 by William Grant
Use new help directories.
583
                <a href="/+help-code/recipe-syntax.html" target="help"
14354.1.1 by Francis J. Lacoste
Revert sourcerecipe help link change.
584
                  >Syntax help&nbsp;
585
                  <span class="sprite maybe">
586
                    <span class="invisible-link">Help</span>
587
                  </span></a>
11929.11.6 by Tim Penhey
Allow forms to specify that some fields have structured widget help.
588
               """))
7675.618.41 by Paul Hummer
Got a working edit
589
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
590
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
591
EXISTING_PPA = 'existing-ppa'
592
CREATE_NEW = 'create-new'
593
594
595
USE_ARCHIVE_VOCABULARY = SimpleVocabulary((
11929.11.28 by Tim Penhey
Other tweaks suggested by reviewer.
596
    SimpleTerm(EXISTING_PPA, EXISTING_PPA, _("Use an existing PPA")),
597
    SimpleTerm(
598
        CREATE_NEW, CREATE_NEW, _("Create a new PPA for this recipe")),
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
599
    ))
600
601
602
class ISourcePackageAddSchema(ISourcePackageEditSchema):
603
11929.11.26 by Tim Penhey
Default the initial ppa name to 'ppa' if the user has no existing PPAs.
604
    daily_build_archive = Choice(vocabulary='TargetPPAs',
605
        title=u'Daily build archive', required=False,
606
        description=(
607
            u'If built daily, this is the archive where the package '
608
            u'will be uploaded.'))
609
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
610
    use_ppa = Choice(
611
        title=_('Which PPA'),
612
        vocabulary=USE_ARCHIVE_VOCABULARY,
613
        description=_("Which PPA to use..."),
614
        required=True)
615
616
    ppa_name = TextLine(
617
            title=_("New PPA name"), required=False,
618
            constraint=name_validator,
11929.11.24 by Tim Penhey
Tweak the description text.
619
            description=_("A new PPA with this name will be created for "
620
                          "the owner of the recipe ."))
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
621
12335.5.4 by Steve Kowalik
Lint fixes
622
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
623
class ErrorHandled(Exception):
624
    """A field error occured and was handled."""
625
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
626
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
627
class RecipeTextValidatorMixin:
628
    """Class to validate that the Source Package Recipe text is valid."""
629
630
    def validate(self, data):
10899.4.1 by Aaron Bentley
Initial UI for source package recipe builds.
631
        if data['build_daily']:
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
632
            if len(data['distroseries']) == 0:
10899.4.1 by Aaron Bentley
Initial UI for source package recipe builds.
633
                self.setFieldError(
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
634
                    'distroseries',
10899.4.1 by Aaron Bentley
Initial UI for source package recipe builds.
635
                    'You must specify at least one series for daily builds.')
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
636
        try:
637
            parser = RecipeParser(data['recipe_text'])
10788.2.2 by Aaron Bentley
Fix lint errors.
638
            parser.parse()
11151.1.1 by Paul Hummer
Pushing the bzr-builder recipe out to the user.
639
        except RecipeParseError, error:
11970.1.1 by Aaron Bentley
Show usage for intruction parse errors.
640
            self.setFieldError('recipe_text', str(error))
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
641
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
642
    def error_handler(self, callable, *args, **kwargs):
12335.5.1 by Steve Kowalik
* Refactor the error handling for creating and updating a recipe into
643
        try:
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
644
            return callable(*args)
12335.5.1 by Steve Kowalik
* Refactor the error handling for creating and updating a recipe into
645
        except TooNewRecipeFormat:
646
            self.setFieldError(
647
                'recipe_text',
648
                'The recipe format version specified is not available.')
649
        except ForbiddenInstructionError, e:
650
            self.setFieldError(
651
                'recipe_text',
652
                'The bzr-builder instruction "%s" is not permitted '
653
                'here.' % e.instruction_name)
654
        except NoSuchBranch, e:
655
            self.setFieldError(
656
                'recipe_text', '%s is not a branch on Launchpad.' % e.name)
657
        except PrivateBranchRecipe, e:
658
            self.setFieldError('recipe_text', str(e))
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
659
        raise ErrorHandled()
660
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
661
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
662
class RelatedBranchesWidget(Widget):
663
    """A widget to render the related branches for a recipe."""
664
    implements(IView)
665
666
    __call__ = ViewPageTemplateFile(
667
        '../templates/sourcepackagerecipe-related-branches.pt')
668
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
669
    related_package_branch_info = []
11994.2.19 by Ian Booth
Make ui pretty, keep private branches hidden, display series instead of owner, new tests
670
    related_series_branch_info = []
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
671
672
    def hasInput(self):
673
        return True
674
675
    def setRenderedValue(self, value):
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
676
        self.related_package_branch_info = (
677
            value['related_package_branch_info'])
11994.2.19 by Ian Booth
Make ui pretty, keep private branches hidden, display series instead of owner, new tests
678
        self.related_series_branch_info = value['related_series_branch_info']
679
680
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
681
class RecipeRelatedBranchesMixin(LaunchpadFormView):
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
682
    """A class to find related branches for a recipe's base branch."""
683
684
    custom_widget('related-branches', RelatedBranchesWidget)
685
686
    def extendFields(self):
687
        """See `LaunchpadFormView`.
688
689
        Adds a related branches field to the form.
690
        """
691
        self.form_fields += form.Fields(Field(__name__='related-branches'))
692
        self.form_fields['related-branches'].custom_widget = (
693
            self.custom_widgets['related-branches'])
694
        self.widget_errors['related-branches'] = ''
695
696
    def setUpWidgets(self, context=None):
697
        # Adds a new related branches widget.
698
        super(RecipeRelatedBranchesMixin, self).setUpWidgets(context)
699
        self.widgets['related-branches'].display_label = False
700
        self.widgets['related-branches'].setRenderedValue(dict(
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
701
                related_package_branch_info=self.related_package_branch_info,
11994.2.19 by Ian Booth
Make ui pretty, keep private branches hidden, display series instead of owner, new tests
702
                related_series_branch_info=self.related_series_branch_info))
703
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
704
    @cachedproperty
11994.2.19 by Ian Booth
Make ui pretty, keep private branches hidden, display series instead of owner, new tests
705
    def related_series_branch_info(self):
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
706
        branch_to_check = self.getBranch()
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
707
        return IBranchTarget(
708
                branch_to_check.target).getRelatedSeriesBranchInfo(
11994.2.24 by Ian Booth
Add option to limit the related branches results. UI only displays top 5 most recent branches
709
                                            branch_to_check,
710
                                            limit_results=5)
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
711
712
    @cachedproperty
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
713
    def related_package_branch_info(self):
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
714
        branch_to_check = self.getBranch()
11994.2.21 by Ian Booth
Refactor related branches methods to IBranchTarget and show distro series instead of source package for related package branches
715
        return IBranchTarget(
716
                branch_to_check.target).getRelatedPackageBranchInfo(
11994.2.24 by Ian Booth
Add option to limit the related branches results. UI only displays top 5 most recent branches
717
                                            branch_to_check,
718
                                            limit_results=5)
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
719
720
721
class SourcePackageRecipeAddView(RecipeRelatedBranchesMixin,
722
                                 RecipeTextValidatorMixin, LaunchpadFormView):
7675.618.41 by Paul Hummer
Got a working edit
723
    """View for creating Source Package Recipes."""
724
725
    title = label = 'Create a new source package recipe'
726
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
727
    schema = ISourcePackageAddSchema
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
728
    custom_widget('distroseries', LabeledMultiCheckBoxWidget)
11869.12.1 by Aaron Bentley
Recipe owner proof-of-concept.
729
    custom_widget('owner', RecipeOwnerWidget)
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
730
    custom_widget('use_ppa', LaunchpadRadioWidget)
10744.5.3 by Paul Hummer
Got a working create recipe flow
731
11049.5.2 by Paul Hummer
Added Warning notification
732
    def initialize(self):
733
        super(SourcePackageRecipeAddView, self).initialize()
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
734
        widget = self.widgets['use_ppa']
735
        current_value = widget._getFormValue()
736
        self.use_ppa_existing = render_radio_widget_part(
737
            widget, EXISTING_PPA, current_value)
738
        self.use_ppa_new = render_radio_widget_part(
739
            widget, CREATE_NEW, current_value)
11929.11.26 by Tim Penhey
Default the initial ppa name to 'ppa' if the user has no existing PPAs.
740
        archive_widget = self.widgets['daily_build_archive']
741
        self.show_ppa_chooser = len(archive_widget.vocabulary) > 0
742
        if not self.show_ppa_chooser:
743
            self.widgets['ppa_name'].setRenderedValue('ppa')
744
        # Force there to be no '(no value)' item in the select.  We do this as
745
        # the input isn't listed as 'required' otherwise the validator gets
746
        # all confused when we want to create a new PPA.
747
        archive_widget._displayItemForMissingValue = False
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
748
12547.1.36 by Ian Booth
Test fixes
749
    def setUpFields(self):
750
        super(SourcePackageRecipeAddView, self).setUpFields()
751
        # Ensure distro series widget allows input
752
        self.form_fields['distroseries'].for_input = True
753
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
754
    def getBranch(self):
755
        """The branch on which the recipe is built."""
756
        return self.context
757
12373.2.1 by Tim Penhey
First hack.
758
    def _recipe_names(self):
759
        """A generator of recipe names."""
12599.4.2 by Leonard Richardson
Merge from trunk.
760
        # +junk-daily doesn't make a very good recipe name, so use the
761
        # branch name in that case.
762
        if zope_isinstance(self.context.target, PersonBranchTarget):
763
            branch_target_name = self.context.name
764
        else:
765
            branch_target_name = self.context.target.name.split('/')[-1]
12373.2.6 by Tim Penhey
Fix the typo, and update the description for the name.
766
        yield "%s-daily" % branch_target_name
12373.2.1 by Tim Penhey
First hack.
767
        counter = itertools.count(1)
768
        while True:
769
            yield "%s-daily-%s" % (branch_target_name, counter.next())
770
771
    def _find_unused_name(self, owner):
772
        # Grab the last path element of the branch target path.
773
        source = getUtility(ISourcePackageRecipeSource)
774
        for recipe_name in self._recipe_names():
12378.3.2 by Ian Booth
Lint
775
            if not source.exists(owner, recipe_name):
776
                return recipe_name
12373.2.1 by Tim Penhey
First hack.
777
10744.5.3 by Paul Hummer
Got a working create recipe flow
778
    @property
779
    def initial_values(self):
12373.2.16 by Tim Penhey
Rename to distroseries.
780
        distroseries = get_buildable_distroseries_set(self.user)
781
        series = [series for series in distroseries if series.status in (
12373.2.8 by Tim Penhey
Set initial distroseries, and make description required again (needs db patch to remove not null constraint).
782
                SeriesStatus.CURRENT, SeriesStatus.DEVELOPMENT)]
7675.618.43 by Paul Hummer
Added owner to the add/edit form
783
        return {
12378.3.2 by Ian Booth
Lint
784
            'name': self._find_unused_name(self.user),
7675.618.43 by Paul Hummer
Added owner to the add/edit form
785
            'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity,
10899.4.1 by Aaron Bentley
Initial UI for source package recipe builds.
786
            'owner': self.user,
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
787
            'distroseries': series,
12373.2.8 by Tim Penhey
Set initial distroseries, and make description required again (needs db patch to remove not null constraint).
788
            'build_daily': True,
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
789
            'use_ppa': EXISTING_PPA,
790
            }
10744.5.3 by Paul Hummer
Got a working create recipe flow
791
792
    @property
793
    def cancel_url(self):
794
        return canonical_url(self.context)
795
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
796
    @action('Create Recipe', name='create')
10744.5.3 by Paul Hummer
Got a working create recipe flow
797
    def request_action(self, action, data):
12335.5.1 by Steve Kowalik
* Refactor the error handling for creating and updating a recipe into
798
        owner = data['owner']
799
        if data['use_ppa'] == CREATE_NEW:
800
            ppa_name = data.get('ppa_name', None)
801
            ppa = owner.createPPA(ppa_name)
802
        else:
803
            ppa = data['daily_build_archive']
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
804
        try:
12335.5.5 by Steve Kowalik
super() isn't needed, switch to self.
805
            source_package_recipe = self.error_handler(
806
                getUtility(ISourcePackageRecipeSource).new,
807
                self.user, owner, data['name'],
12547.1.21 by Ian Booth
Lint
808
                data['recipe_text'], data['description'],
809
                data['distroseries'], ppa, data['build_daily'])
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
810
            Store.of(source_package_recipe).flush()
811
        except ErrorHandled:
12335.5.1 by Steve Kowalik
* Refactor the error handling for creating and updating a recipe into
812
            return
10979.1.3 by Paul Hummer
Fixed the failing test
813
10744.5.3 by Paul Hummer
Got a working create recipe flow
814
        self.next_url = canonical_url(source_package_recipe)
7675.618.39 by Paul Hummer
Added a recipe edit view.
815
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
816
    def validate(self, data):
817
        super(SourcePackageRecipeAddView, self).validate(data)
818
        name = data.get('name', None)
7675.711.12 by Paul Hummer
Fixed a potential bug
819
        owner = data.get('owner', None)
820
        if name and owner:
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
821
            SourcePackageRecipeSource = getUtility(ISourcePackageRecipeSource)
7675.711.12 by Paul Hummer
Fixed a potential bug
822
            if SourcePackageRecipeSource.exists(owner, name):
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
823
                self.setFieldError(
824
                    'name',
825
                    'There is already a recipe owned by %s with this name.' %
7675.711.12 by Paul Hummer
Fixed a potential bug
826
                        owner.displayname)
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
827
        if data['use_ppa'] == CREATE_NEW:
828
            ppa_name = data.get('ppa_name', None)
11929.11.26 by Tim Penhey
Default the initial ppa name to 'ppa' if the user has no existing PPAs.
829
            if ppa_name is None:
830
                self.setFieldError(
831
                    'ppa_name', 'You need to specify a name for the PPA.')
832
            else:
833
                error = Archive.validatePPA(owner, ppa_name)
834
                if error is not None:
835
                    self.setFieldError('ppa_name', error)
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
836
7675.618.39 by Paul Hummer
Added a recipe edit view.
837
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
838
class SourcePackageRecipeEditView(RecipeRelatedBranchesMixin,
839
                                  RecipeTextValidatorMixin,
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
840
                                  LaunchpadEditFormView):
841
    """View for editing Source Package Recipes."""
7675.618.39 by Paul Hummer
Added a recipe edit view.
842
11994.2.16 by Ian Booth
Add new tests and code to allow for branchesrelated to source packages as well as products
843
    def getBranch(self):
844
        """The branch on which the recipe is built."""
845
        return self.context.base_branch
846
7675.618.39 by Paul Hummer
Added a recipe edit view.
847
    @property
848
    def title(self):
849
        return 'Edit %s source package recipe' % self.context.name
850
    label = title
851
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
852
    schema = ISourcePackageEditSchema
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
853
    custom_widget('distroseries', LabeledMultiCheckBoxWidget)
7675.618.41 by Paul Hummer
Got a working edit
854
11814.1.2 by Paul Hummer
Fixed teh bug
855
    def setUpFields(self):
856
        super(SourcePackageRecipeEditView, self).setUpFields()
857
12547.1.36 by Ian Booth
Test fixes
858
        # Ensure distro series widget allows input
859
        self.form_fields['distroseries'].for_input = True
860
11814.1.2 by Paul Hummer
Fixed teh bug
861
        if check_permission('launchpad.Admin', self.context):
862
            # Exclude the PPA archive dropdown.
863
            self.form_fields = self.form_fields.omit('daily_build_archive')
864
865
            owner_field = self.schema['owner']
13378.1.1 by j.c.sackett
Undid the rollback by wgrant to get the functionality we need from wallyworld's branch.
866
            any_owner_choice = PersonChoice(
11814.1.2 by Paul Hummer
Fixed teh bug
867
                __name__='owner', title=owner_field.title,
868
                description=(u"As an administrator you are able to reassign"
869
                             u" this branch to any person or team."),
870
                required=True, vocabulary='ValidPersonOrTeam')
871
            any_owner_field = form.Fields(
872
                any_owner_choice, render_context=self.render_context)
873
            # Replace the normal owner field with a more permissive vocab.
874
            self.form_fields = self.form_fields.omit('owner')
875
            self.form_fields = any_owner_field + self.form_fields
876
7675.618.41 by Paul Hummer
Got a working edit
877
    @property
878
    def initial_values(self):
879
        return {
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
880
            'distroseries': self.context.distroseries,
12178.1.1 by Tim Penhey
Use the un-validated recipe text for viewing, and initialising the edit view.
881
            'recipe_text': self.context.recipe_text,
10409.5.105 by Curtis Hovey
Merged devel, resolved conflicts.
882
            }
7675.618.41 by Paul Hummer
Got a working edit
883
884
    @property
885
    def cancel_url(self):
886
        return canonical_url(self.context)
887
7675.622.1 by Paul Hummer
Fixing MOST of the concerns in Tim's review (still need to make LaunchpadEditForm work
888
    @action('Update Recipe', name='update')
7675.618.41 by Paul Hummer
Got a working edit
889
    def request_action(self, action, data):
7675.622.3 by Paul Hummer
Added update notification code.
890
        changed = False
891
        recipe_before_modification = Snapshot(
892
            self.context, providing=providedBy(self.context))
893
894
        recipe_text = data.pop('recipe_text')
895
        parser = RecipeParser(recipe_text)
7675.622.4 by Paul Hummer
Fixed a bollocks'd up parser.parse
896
        recipe = parser.parse()
897
        if self.context.builder_recipe != recipe:
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
898
            try:
12335.5.5 by Steve Kowalik
super() isn't needed, switch to self.
899
                self.error_handler(self.context.setRecipeText, recipe_text)
12335.5.3 by Steve Kowalik
And one bit of code cleanup I missed.
900
                changed = True
12335.5.2 by Steve Kowalik
Use a new exception to show that an error was raised, and update the two
901
            except ErrorHandled:
12335.5.1 by Steve Kowalik
* Refactor the error handling for creating and updating a recipe into
902
                return
10979.1.5 by Paul Hummer
Fixed the failing edit test
903
12547.1.2 by Ian Booth
Add inline distro series editing - first cut
904
        distros = data.pop('distroseries')
7675.622.3 by Paul Hummer
Added update notification code.
905
        if distros != self.context.distroseries:
906
            self.context.distroseries.clear()
907
            for distroseries_item in distros:
908
                self.context.distroseries.add(distroseries_item)
909
            changed = True
910
911
        if self.updateContextFromData(data, notify_modified=False):
912
            changed = True
913
914
        if changed:
915
            field_names = [
916
                form_field.__name__ for form_field in self.form_fields]
917
            notify(ObjectModifiedEvent(
918
                self.context, recipe_before_modification, field_names))
7675.618.41 by Paul Hummer
Got a working edit
919
920
        self.next_url = canonical_url(self.context)
7675.618.47 by Paul Hummer
Added delete view
921
7675.622.2 by Paul Hummer
Fixed the recipe edit view.
922
    @property
923
    def adapters(self):
924
        """See `LaunchpadEditFormView`"""
11929.11.17 by Tim Penhey
Rework the new page to allow the creation of a ppa on the fly.
925
        return {ISourcePackageEditSchema: self.context}
7675.618.49 by Paul Hummer
Merge from recipe-create-update
926
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
927
    def validate(self, data):
928
        super(SourcePackageRecipeEditView, self).validate(data)
929
        name = data.get('name', None)
7675.711.12 by Paul Hummer
Fixed a potential bug
930
        owner = data.get('owner', None)
931
        if name and owner:
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
932
            SourcePackageRecipeSource = getUtility(ISourcePackageRecipeSource)
7675.711.12 by Paul Hummer
Fixed a potential bug
933
            if SourcePackageRecipeSource.exists(owner, name):
934
                recipe = owner.getRecipe(name)
935
                if recipe != self.context:
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
936
                    self.setFieldError(
937
                        'name',
938
                        'There is already a recipe owned by %s with this '
7675.711.12 by Paul Hummer
Fixed a potential bug
939
                        'name.' % owner.displayname)
7675.711.9 by Paul Hummer
Fixed the test (and the bug)
940
7675.618.47 by Paul Hummer
Added delete view
941
942
class SourcePackageRecipeDeleteView(LaunchpadFormView):
943
944
    @property
945
    def title(self):
946
        return 'Delete %s source package recipe' % self.context.name
947
    label = title
948
949
    class schema(Interface):
950
        """Schema for deleting a branch."""
951
952
    @property
953
    def cancel_url(self):
954
        return canonical_url(self.context)
955
956
    @property
957
    def next_url(self):
958
        return canonical_url(self.context.owner)
959
960
    @action('Delete recipe', name='delete')
961
    def request_action(self, action, data):
962
        self.context.destroySelf()