Merge lp:~wallyworld/launchpad/recipe-find-related-branches into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 12111
Proposed branch: lp:~wallyworld/launchpad/recipe-find-related-branches
Merge into: lp:launchpad
Diff against target: 610 lines (+458/-4)
4 files modified
lib/lp/code/browser/sourcepackagerecipe.py (+107/-2)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+282/-2)
lib/lp/code/templates/sourcepackagerecipe-new.pt (+3/-0)
lib/lp/code/templates/sourcepackagerecipe-related-branches.pt (+66/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/recipe-find-related-branches
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Steve Kowalik (community) code* Approve
Review via email: mp+42097@code.launchpad.net

Commit message

[r=stevenk,thumper][ui=none][bug=670452] Add display of related branches to recipe add and edit pages

Description of the change

= Summary =

Bug 670452 - add a facility to SourcePackageRecipe add/edit pages to show branches related to the base branch of the recipe being added/edited.

= Implementation =

A custom widget was created: RelatedBranchesWidget. A new 'related-branches' form field added to the recipe Add and Edit views is specified to use the widget for rendering. The widget uses the ViewPageTemplateFile class to render some tales markup in ../templates/sourcepackagerecipe-related-branches.pt

What is rendered is a collapsible section called "Related Branches". This has 2 branches listings - related package branches and related series branches.

= Screenshot =

http://people.canonical.com/~ianb/recipe-related-branches.png

= Tests =

bin/test -vvt test_sourcepackagerecipe

New tests were added:
  test_new_recipe_with_package_branches
  test_new_recipe_with_series_branches
  test_new_recipe_view_related_branches
  test_new_recipe_with_related_branches
  test_edit_recipe_with_package_branches
  test_edit_recipe_with_series_branches
  test_edit_recipe_view_related_branches
  test_edit_recipe_with_related_branches

The tests checked that if there were no related branches, the new widget was no rendered, and also checked the contents of the page when related branches were present.

A small drive by improvement was made to the test_create_recipe_no_distroseries test.

= Lint =

Linting changed files:
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/browser/tests/test_sourcepackagerecipe.py
  lib/lp/code/templates/sourcepackagerecipe-related-branches.pt

./lib/lp/code/browser/tests/test_sourcepackagerecipe.py
     511: E501 line too long (80 characters)
    1023: E501 line too long (85 characters)
    1037: E501 line too long (89 characters)
    1049: E501 line too long (85 characters)
    1064: E501 line too long (89 characters)
    1080: E501 line too long (85 characters)
     511: Line exceeds 78 characters.
    1023: Line exceeds 78 characters.
    1037: Line exceeds 78 characters.
    1049: Line exceeds 78 characters.
    1064: Line exceeds 78 characters.
    1067: Line exceeds 78 characters.
    1080: Line exceeds 78 characters.
./lib/lp/code/templates/sourcepackagerecipe-related-branches.pt
       1: unbound prefix

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) wrote :

This looks great! My only concerns are the lint errors, and the (it seems to me) excessive use of removeSecurityProxy.

review: Approve (code*)
Revision history for this message
Ian Booth (wallyworld) wrote :

lint errors (almost all line length) were there already and since this
code base is relatively new, I figured I'd leave well enough alone.

I'll see if I can tweak the zcml instead of using removeSecurityProxy

Thanks.

On 03/12/10 15:01, Steve Kowalik wrote:
> Review: Approve code*
> This looks great! My only concerns are the lint errors, and the (it seems to me) excessive use of removeSecurityProxy.

Revision history for this message
Ian Booth (wallyworld) wrote :

I had a look at the 3 usages of removeSecurityProxy in the new code:

The ones in createRelatedBranches are required:

naked_product is required for ICanHasLinkedBranch(naked_product)
sourcepackage is required for ICanHasLinkedBranch(naked_sourcepackage)

In checkRelatedBranches:

naked_branch is not required so has been removed

On 03/12/10 15:07, Ian Booth wrote:
> lint errors (almost all line length) were there already and since this
> code base is relatively new, I figured I'd leave well enough alone.
>
> I'll see if I can tweak the zcml instead of using removeSecurityProxy
>
> Thanks.
>
> On 03/12/10 15:01, Steve Kowalik wrote:
>> Review: Approve code*
>> This looks great! My only concerns are the lint errors, and the (it seems to me) excessive use of removeSecurityProxy.

Revision history for this message
Tim Penhey (thumper) wrote :

I have two issues with this branch.

1) What do we show when we can't find any related branches?

2) Creating a widget is overkill for what is needed, which is some text on the page.

review: Needs Fixing
Revision history for this message
Ian Booth (wallyworld) wrote :

On 06/12/10 14:22, Tim Penhey wrote:
> Review: Needs Fixing
> I have two issues with this branch.
>
> 1) What do we show when we can't find any related branches?
>

Nothing. The page appears as it does now before the branch. Do you want
some text to say something like "No related branches"? I didn't think
that was needed but can easily add it.

> 2) Creating a widget is overkill for what is needed, which is some text on the page.

The page template uses this construct to render the form:

<div metal:use-macro="context/@@launchpad_form/form" />

The first implementation this branch involved replacing the above with a
macro:

<div metal:use-macro="context/@@+sourcepackagerecipe-macros/recipe-form"/>

which:

- expanded the default metal form definition to explicitly list the
required form fields to display:
- adds the related branches markup after all the form fields are specified

eg

<div metal:use-macro="context/@@launchpad_form/form">
  <tal:product-name define="widget nocall:widgets/name">
    <metal:block use-macro="context/@@launchpad_form/widget_row" />
  </tal:product-name>
  <tal:product-name define="widget nocall:widgets/description">
    <metal:block use-macro="context/@@launchpad_form/widget_row" />
  </tal:product-name>
  <another form field...>
  <another form field...>

  <!-- markup for related branches goes here -->
  <table>
  xxxx
  </table>

</div>

The macro was used for the add and edit page templates. Because the edit
form used the generic edit template, a new sourcepackagerecipe-edit page
template was created.

I thought that expanding the default form element and explicitly listing
the form fields in the page template was perhaps not the best approach
since a change to the form schema would require editing several
different files. The current implementation programatically adds the
custom field used to display the related branches to the form schema and
is more robust to changes in the underlying form.

I can revert to this approach is that's considered better.

Revision history for this message
Tim Penhey (thumper) wrote :

On Tue, 07 Dec 2010 03:11:15 you wrote:
> On 06/12/10 14:22, Tim Penhey wrote:
> > Review: Needs Fixing
> > I have two issues with this branch.
> >
> > 1) What do we show when we can't find any related branches?
>
> Nothing. The page appears as it does now before the branch. Do you want
> some text to say something like "No related branches"? I didn't think
> that was needed but can easily add it.

No, I think that nothing is better. I take it that the fieldset isn't rendered
at all when there are no branches?

> > 2) Creating a widget is overkill for what is needed, which is some text
> > on the page.
>
> The page template uses this construct to render the form:
>
> <div metal:use-macro="context/@@launchpad_form/form" />
>
> The first implementation this branch involved replacing the above with a
> macro:
>
> <div metal:use-macro="context/@@+sourcepackagerecipe-macros/recipe-form"/>

There is a simpler way, which I happen to have done with my "ppa creation
during new recipe" branch, which does expand all the rendered widgets.

The form macro defines a number of slots which can be replaced. However with
my new change (which I know you didn't know about), it is easier to just add a
simple rendering, and have the methods to get the branches on the Add view
class. Perhaps you might want to merge my branch and make yours dependent on
it?

This is one of the problems of having multiple people hacking on the same
area.

Revision history for this message
Ian Booth (wallyworld) wrote :

>>>
>>> 1) What do we show when we can't find any related branches?
>>
>> Nothing. The page appears as it does now before the branch. Do you want
>> some text to say something like "No related branches"? I didn't think
>> that was needed but can easily add it.
>
> No, I think that nothing is better. I take it that the fieldset isn't rendered
> at all when there are no branches?
>

Correct. There's a tal condition wrapping the fieldset.

>
> There is a simpler way, which I happen to have done with my "ppa creation
> during new recipe" branch, which does expand all the rendered widgets.
>
> The form macro defines a number of slots which can be replaced. However with
> my new change (which I know you didn't know about), it is easier to just add a
> simple rendering, and have the methods to get the branches on the Add view
> class. Perhaps you might want to merge my branch and make yours dependent on
> it?
>
> This is one of the problems of having multiple people hacking on the same
> area.

Thanks for the info. I'll use the new code to improve my implementation.

Revision history for this message
Ian Booth (wallyworld) wrote :

>
>>> 2) Creating a widget is overkill for what is needed, which is some text
>>> on the page.
>>
>> The page template uses this construct to render the form:
>>
>> <div metal:use-macro="context/@@launchpad_form/form" />
>>
>> The first implementation this branch involved replacing the above with a
>> macro:
>>
>> <div metal:use-macro="context/@@+sourcepackagerecipe-macros/recipe-form"/>
>
> There is a simpler way, which I happen to have done with my "ppa creation
> during new recipe" branch, which does expand all the rendered widgets.

The custom widget is the way to do it with least code changes. It's not
just a simple field rendering as is the case with the ppa stuff -
there's an entire page template file which renders the tables and links
etc. Using another method eg a macro involves editing zcml and adding an
extra page template for the edit form etc. So I'd like to land it as is
if possible.

Revision history for this message
Tim Penhey (thumper) wrote :

Can you move the functionality in the render method into the setupWidgets?

Apart from that, you are good to go.

review: Approve
Revision history for this message
Ian Booth (wallyworld) wrote :

> Can you move the functionality in the render method into the setupWidgets?
>
> Apart from that, you are good to go.

Done

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
2--- lib/lp/code/browser/sourcepackagerecipe.py 2010-12-17 04:48:01 +0000
3+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-12-20 00:01:04 +0000
4@@ -14,6 +14,7 @@
5 'SourcePackageRecipeView',
6 ]
7
8+from operator import attrgetter
9
10 from bzrlib.plugins.builder.recipe import (
11 ForbiddenInstructionError,
12@@ -24,6 +25,9 @@
13 from lazr.lifecycle.snapshot import Snapshot
14 from lazr.restful.interface import use_template
15 from storm.locals import Store
16+from z3c.ptcompat import ViewPageTemplateFile
17+from zope.app.form.browser.widget import Widget
18+from zope.app.form.interfaces import IView
19 from zope.component import getUtility
20 from zope.event import notify
21 from zope.formlib import form
22@@ -33,6 +37,7 @@
23 providedBy,
24 )
25 from zope.schema import (
26+ Field,
27 Choice,
28 List,
29 Text,
30@@ -75,10 +80,12 @@
31 )
32 from lp.code.errors import (
33 BuildAlreadyPending,
34+ NoLinkedBranch,
35 NoSuchBranch,
36 PrivateBranchRecipe,
37 TooNewRecipeFormat,
38 )
39+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
40 from lp.code.interfaces.sourcepackagerecipe import (
41 ISourcePackageRecipe,
42 ISourcePackageRecipeSource,
43@@ -88,6 +95,7 @@
44 ISourcePackageRecipeBuildSource,
45 )
46 from lp.registry.interfaces.pocket import PackagePublishingPocket
47+from lp.services.propertycache import cachedproperty
48 from lp.soyuz.model.archive import Archive
49
50
51@@ -360,7 +368,95 @@
52 self.setFieldError('recipe_text', str(error))
53
54
55-class SourcePackageRecipeAddView(RecipeTextValidatorMixin, LaunchpadFormView):
56+class RelatedBranchesWidget(Widget):
57+ """A widget to render the related branches for a recipe."""
58+ implements(IView)
59+
60+ __call__ = ViewPageTemplateFile(
61+ '../templates/sourcepackagerecipe-related-branches.pt')
62+
63+ related_package_branches = []
64+ related_series_branches = []
65+
66+ def hasInput(self):
67+ return True
68+
69+ def setRenderedValue(self, value):
70+ self.related_package_branches = value['related_package_branches']
71+ self.related_series_branches = value['related_series_branches']
72+
73+
74+class RecipeRelatedBranchesMixin(LaunchpadFormView):
75+ """A class to find related branches for a recipe's base branch."""
76+
77+ custom_widget('related-branches', RelatedBranchesWidget)
78+
79+ def extendFields(self):
80+ """See `LaunchpadFormView`.
81+
82+ Adds a related branches field to the form.
83+ """
84+ self.form_fields += form.Fields(Field(__name__='related-branches'))
85+ self.form_fields['related-branches'].custom_widget = (
86+ self.custom_widgets['related-branches'])
87+ self.widget_errors['related-branches'] = ''
88+
89+ def setUpWidgets(self, context=None):
90+ # Adds a new related branches widget.
91+ super(RecipeRelatedBranchesMixin, self).setUpWidgets(context)
92+ self.widgets['related-branches'].display_label = False
93+ self.widgets['related-branches'].setRenderedValue(dict(
94+ related_package_branches=self.related_package_branches,
95+ related_series_branches=self.related_series_branches))
96+
97+ @cachedproperty
98+ def related_series_branches(self):
99+ """Find development branches related to the base branch's product."""
100+ result = set()
101+ branch_to_check = self.getBranch()
102+ product = branch_to_check.product
103+
104+ # We include the development focus branch.
105+ dev_focus_branch = ICanHasLinkedBranch(product).branch
106+ if dev_focus_branch is not None:
107+ result.add(dev_focus_branch)
108+
109+ # Now any branches for the product's series.
110+ for series in product.series:
111+ try:
112+ branch = ICanHasLinkedBranch(series).branch
113+ if branch is not None:
114+ result.add(branch)
115+ except NoLinkedBranch:
116+ # If there's no branch for a particular series, we don't care.
117+ pass
118+ # We don't want to include the source branch.
119+ result.discard(branch_to_check)
120+ return sorted(result, key=attrgetter('unique_name'))
121+
122+ @cachedproperty
123+ def related_package_branches(self):
124+ """Find branches for the base branch's product's distro src pkgs."""
125+ result = set()
126+ branch_to_check = self.getBranch()
127+ product = branch_to_check.product
128+
129+ for sourcepackage in product.distrosourcepackages:
130+ try:
131+ branch = ICanHasLinkedBranch(sourcepackage).branch
132+ if branch is not None:
133+ result.add(branch)
134+ except NoLinkedBranch:
135+ # If there's no branch for a particular source package,
136+ # we don't care.
137+ pass
138+ # We don't want to include the source branch.
139+ result.discard(branch_to_check)
140+ return sorted(result, key=attrgetter('unique_name'))
141+
142+
143+class SourcePackageRecipeAddView(RecipeRelatedBranchesMixin,
144+ RecipeTextValidatorMixin, LaunchpadFormView):
145 """View for creating Source Package Recipes."""
146
147 title = label = 'Create a new source package recipe'
148@@ -390,6 +486,10 @@
149 # all confused when we want to create a new PPA.
150 archive_widget._displayItemForMissingValue = False
151
152+ def getBranch(self):
153+ """The branch on which the recipe is built."""
154+ return self.context
155+
156 @property
157 def initial_values(self):
158 return {
159@@ -461,10 +561,15 @@
160 self.setFieldError('ppa_name', error)
161
162
163-class SourcePackageRecipeEditView(RecipeTextValidatorMixin,
164+class SourcePackageRecipeEditView(RecipeRelatedBranchesMixin,
165+ RecipeTextValidatorMixin,
166 LaunchpadEditFormView):
167 """View for editing Source Package Recipes."""
168
169+ def getBranch(self):
170+ """The branch on which the recipe is built."""
171+ return self.context.base_branch
172+
173 @property
174 def title(self):
175 return 'Edit %s source package recipe' % self.context.name
176
177=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
178--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-12-13 04:33:22 +0000
179+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-12-20 00:01:04 +0000
180@@ -7,10 +7,12 @@
181 __metaclass__ = type
182
183
184+from BeautifulSoup import BeautifulSoup
185 from datetime import (
186 datetime,
187 timedelta,
188 )
189+from operator import attrgetter
190 from textwrap import dedent
191
192 from mechanize import LinkNotFoundError
193@@ -28,6 +30,7 @@
194 get_radio_button_text_for_field,
195 )
196 from canonical.launchpad.webapp import canonical_url
197+from canonical.launchpad.webapp.interfaces import ILaunchpadRoot
198 from canonical.testing.layers import (
199 DatabaseFunctionalLayer,
200 LaunchpadFunctionalLayer,
201@@ -40,15 +43,18 @@
202 from lp.code.browser.sourcepackagerecipebuild import (
203 SourcePackageRecipeBuildView,
204 )
205+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
206 from lp.code.interfaces.sourcepackagerecipe import MINIMAL_RECIPE_TEXT
207 from lp.code.tests.helpers import recipe_parser_newest_version
208 from lp.registry.interfaces.person import TeamSubscriptionPolicy
209 from lp.registry.interfaces.pocket import PackagePublishingPocket
210 from lp.services.propertycache import clear_property_cache
211 from lp.soyuz.model.processor import ProcessorFamily
212+from lp.testing.views import create_initialized_view
213 from lp.testing import (
214 ANONYMOUS,
215 BrowserTestCase,
216+ celebrity_logged_in,
217 login,
218 person_logged_in,
219 )
220@@ -86,6 +92,139 @@
221 ' Secret Squirrel changes.', branches=[cake_branch],
222 daily_build_archive=self.ppa)
223
224+ def createRelatedBranches(self, base_branch=None, nr_series_branches=5,
225+ nr_package_branches=5):
226+ """Create a recipe base branch and some others associated with it.
227+ The other branches are:
228+ - series branches: a set of branches associated with product
229+ series of the same product as the recipe base branch.
230+ - package branches: a set of branches associated with packagesource
231+ entities of the same product as the recipe base branch.
232+ """
233+ related_series_branches = set()
234+ related_package_branches = set()
235+ if base_branch is None:
236+ naked_product = removeSecurityProxy(
237+ self.factory.makeProduct(owner=self.chef))
238+ else:
239+ naked_product = removeSecurityProxy(base_branch.product)
240+
241+ if nr_series_branches > 0:
242+ # Add a development branch
243+ naked_product.development_focus.name = 'trunk'
244+ devel_branch = self.factory.makeProductBranch(
245+ product=naked_product, name='trunk', owner=self.chef)
246+ linked_branch = ICanHasLinkedBranch(naked_product)
247+ linked_branch.setBranch(devel_branch)
248+ related_series_branches.add(devel_branch)
249+
250+ # Add some product series
251+ for x in range(nr_series_branches-1):
252+ branch = self.factory.makeBranch(
253+ product=naked_product, owner=self.chef)
254+ self.factory.makeProductSeries(
255+ product=naked_product, branch=branch)
256+ related_series_branches.add(branch)
257+
258+ if nr_package_branches > 0:
259+ distro = self.factory.makeDistribution()
260+ distroseries = self.factory.makeDistroSeries(
261+ distribution=distro)
262+
263+ for x in range(nr_package_branches):
264+ sourcepackagename = self.factory.makeSourcePackageName()
265+
266+ suitesourcepackage = self.factory.makeSuiteSourcePackage(
267+ sourcepackagename=sourcepackagename,
268+ distroseries=distroseries,
269+ pocket=PackagePublishingPocket.RELEASE)
270+ naked_sourcepackage = removeSecurityProxy(
271+ suitesourcepackage)
272+
273+ branch = self.factory.makePackageBranch(
274+ owner=self.chef, sourcepackagename=sourcepackagename,
275+ distroseries=distroseries)
276+ linked_branch = ICanHasLinkedBranch(naked_sourcepackage)
277+ with celebrity_logged_in('admin'):
278+ linked_branch.setBranch(branch, self.chef)
279+
280+ series = self.factory.makeProductSeries(
281+ product=naked_product)
282+ self.factory.makePackagingLink(
283+ distroseries=distroseries, productseries=series,
284+ sourcepackagename=sourcepackagename)
285+ related_package_branches.add(branch)
286+
287+ if base_branch is None:
288+ # Create the 'source' branch ie the base branch of a recipe.
289+ base_branch = self.factory.makeProductBranch(
290+ product=naked_product)
291+ return (
292+ base_branch,
293+ sorted(related_series_branches, key=attrgetter('unique_name')),
294+ sorted(related_package_branches, key=attrgetter('unique_name')))
295+
296+ def checkRelatedBranches(self, related_series_branches,
297+ related_package_branches, browser_contents):
298+ """Check that the browser contents contain the correct branch info."""
299+ login(ANONYMOUS)
300+ soup = BeautifulSoup(browser_contents)
301+
302+ # The related branches collapsible section needs to be there.
303+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
304+ self.assertIsNot(related_branches, None)
305+
306+ # Check the related package branches.
307+ branch_table = soup.find(
308+ 'table', {'id': 'related-package-branches-listing'})
309+ rows = branch_table.tbody.findAll('tr')
310+
311+ package_branches_info = []
312+ root_url = canonical_url(
313+ getUtility(ILaunchpadRoot), rootsite='code')
314+ root_url = root_url.rstrip('/')
315+ for row in rows:
316+ branch_links = row.findAll('a')
317+ self.assertEqual(2, len(branch_links))
318+ package_branches_info.append(
319+ '%s%s' % (root_url, branch_links[0]['href']))
320+ package_branches_info.append(branch_links[0].renderContents())
321+ package_branches_info.append(
322+ '%s%s' % (root_url, branch_links[1]['href']))
323+ package_branches_info.append(branch_links[1].renderContents())
324+ expected_branch_info = []
325+ for branch in related_package_branches:
326+ expected_branch_info.append(
327+ canonical_url(branch, rootsite='code'))
328+ expected_branch_info.append(branch.displayname)
329+ expected_branch_info.append(
330+ canonical_url(branch.sourcepackage, rootsite='code'))
331+ expected_branch_info.append(branch.sourcepackage.name)
332+ self.assertEqual(package_branches_info, expected_branch_info)
333+
334+ # Check the related series branches.
335+ branch_table = soup.find(
336+ 'table', {'id': 'related-series-branches-listing'})
337+ rows = branch_table.tbody.findAll('tr')
338+
339+ series_branches_info = []
340+ for row in rows:
341+ branch_links = row.findAll('a')
342+ self.assertEqual(2, len(branch_links))
343+ series_branches_info.append(
344+ '%s%s' % (root_url, branch_links[0]['href']))
345+ series_branches_info.append(branch_links[0].renderContents())
346+ series_branches_info.append(branch_links[1]['href'])
347+ series_branches_info.append(branch_links[1].renderContents())
348+ expected_branch_info = []
349+ for branch in related_series_branches:
350+ expected_branch_info.append(
351+ canonical_url(branch, rootsite='code'))
352+ expected_branch_info.append(branch.displayname)
353+ expected_branch_info.append(canonical_url(branch.owner))
354+ expected_branch_info.append(branch.owner.displayname)
355+ self.assertEqual(series_branches_info, expected_branch_info)
356+
357
358 def get_message_text(browser, index):
359 """Return the text of a message, specified by index."""
360@@ -336,8 +475,8 @@
361 browser.getControl('Automatically build each day').click()
362 browser.getControl('Create Recipe').click()
363 self.assertEqual(
364- extract_text(find_tags_by_class(browser.contents, 'message')[2]),
365- 'You must specify at least one series for daily builds.')
366+ 'You must specify at least one series for daily builds.',
367+ get_message_text(browser, 2))
368
369 def test_create_recipe_bad_base_branch(self):
370 # If a user tries to create source package recipe with a bad base
371@@ -414,6 +553,72 @@
372 get_message_text(browser, 2),
373 'Recipe may not refer to private branch: %s' % bzr_identity)
374
375+ def test_new_recipe_with_no_related_branches(self):
376+ # The Related Branches section should not appear if there are no
377+ # related branches.
378+ branch = self.makeBranch()
379+ # A new recipe can be created from the branch page.
380+ browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
381+ # There shouldn't be a related-branches section if there are no
382+ # related branches..
383+ soup = BeautifulSoup(browser.contents)
384+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
385+ self.assertIs(related_branches, None)
386+
387+ def test_new_recipe_with_package_branches(self):
388+ # The series branches table should not appear if there are none.
389+ (branch, related_series_branches, related_package_branches) = (
390+ self.createRelatedBranches(nr_series_branches=0))
391+ browser = self.getUserBrowser(
392+ canonical_url(branch, view_name='+new-recipe'), user=self.chef)
393+ soup = BeautifulSoup(browser.contents)
394+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
395+ self.assertIsNot(related_branches, None)
396+ related_branches = soup.find(
397+ 'table', {'id': 'related-package-branches-listing'})
398+ self.assertIsNot(related_branches, None)
399+ related_branches = soup.find(
400+ 'table', {'id': 'related-series-branches-listing'})
401+ self.assertIs(related_branches, None)
402+
403+ def test_new_recipe_with_series_branches(self):
404+ # The package branches table should not appear if there are none.
405+ (branch, related_series_branches, related_package_branches) = (
406+ self.createRelatedBranches(nr_package_branches=0))
407+ browser = self.getUserBrowser(
408+ canonical_url(branch, view_name='+new-recipe'), user=self.chef)
409+ soup = BeautifulSoup(browser.contents)
410+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
411+ self.assertIsNot(related_branches, None)
412+ related_branches = soup.find(
413+ 'table', {'id': 'related-series-branches-listing'})
414+ self.assertIsNot(related_branches, None)
415+ related_branches = soup.find(
416+ 'table', {'id': 'related-package-branches-listing'})
417+ self.assertIs(related_branches, None)
418+
419+ def test_new_recipe_view_related_branches(self):
420+ # The view class should provide the expected related branches for
421+ # rendering.
422+ (branch, related_series_branches,
423+ related_package_branches) = self.createRelatedBranches()
424+ with person_logged_in(self.chef):
425+ view = create_initialized_view(branch, "+new-recipe")
426+ self.assertEqual(
427+ related_series_branches, view.related_series_branches)
428+ self.assertEqual(
429+ related_package_branches, view.related_package_branches)
430+
431+ def test_new_recipe_with_related_branches(self):
432+ # The related branches should be rendered correctly on the page.
433+ (branch, related_series_branches,
434+ related_package_branches) = self.createRelatedBranches()
435+ browser = self.getUserBrowser(
436+ canonical_url(branch, view_name='+new-recipe'), user=self.chef)
437+ self.checkRelatedBranches(
438+ related_series_branches, related_package_branches,
439+ browser.contents)
440+
441 def test_ppa_selector_not_shown_if_user_has_no_ppas(self):
442 # If the user creating a recipe has no existing PPAs, the selector
443 # isn't shown, but the field to enter a new PPA name is.
444@@ -817,6 +1022,81 @@
445 get_message_text(browser, 1),
446 'Recipe may not refer to private branch: %s' % bzr_identity)
447
448+ def test_edit_recipe_with_no_related_branches(self):
449+ # The Related Branches section should not appear if there are no
450+ # related branches.
451+ recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
452+ browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
453+ browser.getLink('Edit recipe').click()
454+ # There shouldn't be a related-branches section if there are no
455+ # related branches.
456+ soup = BeautifulSoup(browser.contents)
457+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
458+ self.assertIs(related_branches, None)
459+
460+ def test_edit_recipe_with_package_branches(self):
461+ # The series branches table should not appear if there are none.
462+ with person_logged_in(self.chef):
463+ recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
464+ self.createRelatedBranches(
465+ base_branch=recipe.base_branch, nr_series_branches=0)
466+ browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
467+ browser.getLink('Edit recipe').click()
468+ soup = BeautifulSoup(browser.contents)
469+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
470+ self.assertIsNot(related_branches, None)
471+ related_branches = soup.find(
472+ 'table', {'id': 'related-package-branches-listing'})
473+ self.assertIsNot(related_branches, None)
474+ related_branches = soup.find(
475+ 'table', {'id': 'related-series-branches-listing'})
476+ self.assertIs(related_branches, None)
477+
478+ def test_edit_recipe_with_series_branches(self):
479+ # The package branches table should not appear if there are none.
480+ with person_logged_in(self.chef):
481+ recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
482+ self.createRelatedBranches(
483+ base_branch=recipe.base_branch, nr_package_branches=0)
484+ browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
485+ browser.getLink('Edit recipe').click()
486+ soup = BeautifulSoup(browser.contents)
487+ related_branches = soup.find('fieldset', {'id': 'related-branches'})
488+ self.assertIsNot(related_branches, None)
489+ related_branches = soup.find(
490+ 'table', {'id': 'related-series-branches-listing'})
491+ self.assertIsNot(related_branches, None)
492+ related_branches = soup.find(
493+ 'table', {'id': 'related-package-branches-listing'})
494+ self.assertIs(related_branches, None)
495+
496+ def test_edit_recipe_view_related_branches(self):
497+ # The view class should provide the expected related branches for
498+ # rendering.
499+ with person_logged_in(self.chef):
500+ recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
501+ (branch, related_series_branches,
502+ related_package_branches) = self.createRelatedBranches(
503+ base_branch=recipe.base_branch)
504+ view = create_initialized_view(recipe, "+edit")
505+ self.assertEqual(
506+ related_series_branches, view.related_series_branches)
507+ self.assertEqual(
508+ related_package_branches, view.related_package_branches)
509+
510+ def test_edit_recipe_with_related_branches(self):
511+ # The related branches should be rendered correctly on the page.
512+ with person_logged_in(self.chef):
513+ recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
514+ (branch, related_series_branches,
515+ related_package_branches) = self.createRelatedBranches(
516+ base_branch=recipe.base_branch)
517+ browser = self.getUserBrowser(
518+ canonical_url(recipe, view_name='+edit'), user=self.chef)
519+ self.checkRelatedBranches(
520+ related_series_branches, related_package_branches,
521+ browser.contents)
522+
523
524 class TestSourcePackageRecipeView(TestCaseForRecipe):
525
526
527=== modified file 'lib/lp/code/templates/sourcepackagerecipe-new.pt'
528--- lib/lp/code/templates/sourcepackagerecipe-new.pt 2010-12-06 01:28:24 +0000
529+++ lib/lp/code/templates/sourcepackagerecipe-new.pt 2010-12-20 00:01:04 +0000
530@@ -104,6 +104,9 @@
531 <tal:widget define="widget nocall:view/widgets/recipe_text">
532 <metal:block use-macro="context/@@launchpad_form/widget_row" />
533 </tal:widget>
534+ <tal:widget define="widget nocall:view/widgets/related-branches">
535+ <metal:block use-macro="context/@@launchpad_form/widget_row" />
536+ </tal:widget>
537
538 </table>
539 </metal:formbody>
540
541=== added file 'lib/lp/code/templates/sourcepackagerecipe-related-branches.pt'
542--- lib/lp/code/templates/sourcepackagerecipe-related-branches.pt 1970-01-01 00:00:00 +0000
543+++ lib/lp/code/templates/sourcepackagerecipe-related-branches.pt 2010-12-20 00:01:04 +0000
544@@ -0,0 +1,66 @@
545+<fieldset id="related-branches"
546+ class="collapsible collapsed"
547+ tal:define="seriesBranches view/related_series_branches;
548+ packageBranches view/related_package_branches"
549+ tal:condition="python: seriesBranches or packageBranches">
550+ <legend>Related Branches</legend>
551+ <table class="extra-options">
552+
553+ <table class="listing" id="related-package-branches-listing"
554+ tal:condition="packageBranches">
555+ <thead>
556+ <tr>
557+ <th>Source Package Branches</th>
558+ <th>Source Package</th>
559+ </tr>
560+ </thead>
561+ <tbody>
562+ <tr tal:repeat="branch packageBranches">
563+ <td width="60%">
564+ <a href="#"
565+ tal:content="branch/displayname"
566+ tal:attributes="href branch/fmt:url">
567+ source package branch
568+ </a>
569+ </td>
570+ <td>
571+ <a href="#"
572+ tal:attributes="href branch/sourcepackage/fmt:url"
573+ tal:content="branch/sourcepackagename/name">
574+ source package
575+ </a>
576+ </td>
577+ </tr>
578+ </tbody>
579+ </table>
580+
581+ <table class="listing" id="related-series-branches-listing"
582+ tal:condition="seriesBranches">
583+ <thead>
584+ <tr>
585+ <th>Product Series Branches</th>
586+ <th>Owner</th>
587+ </tr>
588+ </thead>
589+ <tbody>
590+ <tr tal:repeat="branch seriesBranches">
591+ <td width="60%">
592+ <a href="#"
593+ tal:content="branch/displayname"
594+ tal:attributes="href branch/fmt:url">
595+ product series branch
596+ </a>
597+ </td>
598+ <td>
599+ <a href="#"
600+ tal:content="branch/owner/displayname"
601+ tal:attributes="href branch/owner/fmt:url">
602+ owner
603+ </a>
604+ </td>
605+ </tr>
606+ </tbody>
607+ </table>
608+
609+ </table>
610+</fieldset>