Merge lp:~wallyworld/launchpad/recipe-find-related-branches into lp:launchpad
- recipe-find-related-branches
- Merge into devel
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 |
Related bugs: |
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,
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: RelatedBranches
What is rendered is a collapsible section called "Related Branches". This has 2 branches listings - related package branches and related series branches.
= Screenshot =
http://
= Tests =
bin/test -vvt test_sourcepack
New tests were added:
test_
test_
test_
test_
test_
test_
test_
test_
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_
= Lint =
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
./lib/lp/
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/
1: unbound prefix
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 removeSecurityP
Ian Booth (wallyworld) wrote : | # |
I had a look at the 3 usages of removeSecurityProxy in the new code:
The ones in createRelatedBr
naked_product is required for ICanHasLinkedBr
sourcepackage is required for ICanHasLinkedBr
In checkRelatedBra
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 removeSecurityP
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.
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-
The first implementation this branch involved replacing the above with a
macro:
<div metal:use-
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-
<tal:product-name define="widget nocall:
<metal:block use-macro=
</tal:
<tal:product-name define="widget nocall:
<metal:block use-macro=
</tal:
<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 sourcepackagere
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.
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-
>
> The first implementation this branch involved replacing the above with a
> macro:
>
> <div metal:use-
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.
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.
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-
>>
>> The first implementation this branch involved replacing the above with a
>> macro:
>>
>> <div metal:use-
>
> 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.
Tim Penhey (thumper) wrote : | # |
Can you move the functionality in the render method into the setupWidgets?
Apart from that, you are good to go.
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
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> |
This looks great! My only concerns are the lint errors, and the (it seems to me) excessive use of removeSecurityP roxy.