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: 12313
Proposed branch: lp:~wallyworld/launchpad/recipe-find-related-branches
Merge into: lp:launchpad
Diff against target: 969 lines (+732/-6)
8 files modified
lib/lp/code/browser/sourcepackagerecipe.py (+77/-3)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+246/-2)
lib/lp/code/interfaces/branchtarget.py (+30/-1)
lib/lp/code/model/branchtarget.py (+88/-0)
lib/lp/code/model/tests/test_branchtarget.py (+60/-0)
lib/lp/code/templates/sourcepackagerecipe-new.pt (+3/-0)
lib/lp/code/templates/sourcepackagerecipe-related-branches.pt (+75/-0)
lib/lp/testing/factory.py (+153/-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
Curtis Hovey (community) ui Approve
Guilherme Salgado (community) ui* Needs Information
Review via email: mp+47367@code.launchpad.net

Commit message

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

Description of the change

This branch was merged before but failed QA and had to be rolled back.
See the original merge proposal for some background to the work done:
https://code.launchpad.net/~wallyworld/launchpad/recipe-find-related-branches/+merge/42097

Corrections have been made to the originally submitted code. The main problem was that the recipe [add|edit] view assumed the context branch was associated with a product. If the branch target was a sourcepackage instead, the view(s) would fail to load. Hence, for example, creating a recipe for a sourcepackage branch failed.

== Implementation ==

The related_series_branches() and related_package_branches() methods of the RecipeRelatedBranchesMixin class have been modified to fix the core issue. If a branch's product attribute is None, then no series branches are returned.

Talking to stevenk, it was decided that in the case of a branch with a target sourcepackage, the related package branches would be determined by using the linked_branches property of the target sourcepackage.

== Screenshot ==

*** ui review required ***

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

The ui was modified to make the tables used to display the related branches a fixed width, similar to the text area widget. Vertical space was also added between the 2 tables. These changes make the ui look better, but perhaps more work is required?

== Tests ==

Tests were added to cater for the additional recipe creation/edit scenarios, plus some refactoring of what became common code was done.

New tests:

TestSourcePackageRecipeAddView
  test_new_product_branch_with_no_related_branches_recipe
  test_new_package_branch_with_no_linked_branches_recipe
  test_new_sourcepackage_branch_recipe_view_related_branches
  test_new_sourcepackage_branch_recipe_with_related_branches

TestSourcePackageRecipeEditView
  test_edit_product_branch_with_no_related_branches_recipe
  test_edit_sourcepackage_branch_with_no_related_branches_recipe
  test_edit_product_branch_recipe_with_related_branches
  test_edit_sourcepackage_recipe_with_related_branches

== Lint ==

Checking for conflicts and issues in changed files.

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

./lib/lp/code/browser/tests/test_sourcepackagerecipe.py
     552: E501 line too long (80 characters)
     672: E201 whitespace after '('
    1265: E501 line too long (85 characters)
    1295: E501 line too long (85 characters)
    1330: E501 line too long (85 characters)
     552: Line exceeds 78 characters.
    1265: Line exceeds 78 characters.
    1295: Line exceeds 78 characters.
    1317: Line exceeds 78 characters.
    1330: 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
Guilherme Salgado (salgado) wrote :

Hi Ian,

This looks like I nice addition, but I wonder if it's necessary to list the branch URLs or if it'd be enough to have just the branch's name (linking to the branch itself) and the series/package name (also linking to the relevant thing). Something like

v Related branches[1]

 ------------------------------------------------------------------------
 | From source packages | Owner |
 ------------------------------------------------------------------------
 | _firefox-4.0_ from the _We're back in the game_ series | TheOwner |

 -----------------------------------------------------------------
 | From product series | Owner |
 -----------------------------------------------------------------
 | _bzr-2.2_ from the _Bazaar 2.2_ series | TheOwner |

That makes (what seems to me to be) the important information more prominent, avoids the repetition, provides a link to the series/packages themselves and allows us to use similar table headings for both tables (having one table list the owner and the other just the source package seemed a bit weird to me, but maybe there's a reason for doing so?).

Also, do you really need the fixed width on the table? Shouldn't the surrounding boxes of the launchpad-form limit the width of the table? Or is this because you want them to have the exact same width as the recipe text area?

[1] notice the different capitalization, as recommended by https://dev.launchpad.net/UserInterfaceWording

review: Needs Information (ui*)
Revision history for this message
Curtis Hovey (sinzui) wrote :

The column width issue is the same one that the milestone and project packages pages had to solve. User really do need the columns to be the same size and aligned to scan the page. But as Salgado notes, the columns do not have the same content. This is a bit like the related packages problem (see https://launchpad.net/~wgrant/+related-software for packages and ppa). This data issue might not be solvable now, but it would be nice to have the same column headings.

I think Salgado has an interesting point about showing the URL
I suspect such a change will frustrate users who copy the branch URL from the listing. But I wonder. Why is the owner missing from the product URLs?

So I think Salgado is asking the right question. I would rephrase it as "Why isn't the data symmetric?"

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Tue, 2011-01-25 at 20:07 +0000, Curtis Hovey wrote:
> The column width issue is the same one that the milestone and project
> packages pages had to solve. User really do need the columns to be the
> same size and aligned to scan the page. But as Salgado notes, the
> columns do not have the same content. This is a bit like the related
> packages problem (see https://launchpad.net/~wgrant/+related-software
> for packages and ppa). This data issue might not be solvable now, but
> it would be nice to have the same column headings.
>
> I think Salgado has an interesting point about showing the URL
> I suspect such a change will frustrate users who copy the branch URL
> from the listing. But I wonder. Why is the owner missing from the
> product URLs?

I can see why people would copy the branch URL if they wanted to use
that in the recipe but I was under the impression the main use case here
was just to show stuff that the user might like to know rather than to
aid them in crafting the recipe. That was my understanding after
reading https://bugs.launchpad.net/launchpad/+bug/670452/comments/2 but
maybe I misunderstood or the main use case is to aid people writing
recipes?

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

> Hi Ian,
>
> This looks like I nice addition, but I wonder if it's necessary to list the
> branch URLs or if it'd be enough to have just the branch's name (linking to
> the branch itself) and the series/package name (also linking to the relevant
> thing). Something like
>
> v Related branches[1]
>
> ------------------------------------------------------------------------
> | From source packages | Owner |
> ------------------------------------------------------------------------
> | _firefox-4.0_ from the _We're back in the game_ series | TheOwner |
>
> -----------------------------------------------------------------
> | From product series | Owner |
> -----------------------------------------------------------------
> | _bzr-2.2_ from the _Bazaar 2.2_ series | TheOwner |
>
> That makes (what seems to me to be) the important information more prominent,
> avoids the repetition, provides a link to the series/packages themselves and
> allows us to use similar table headings for both tables (having one table list
> the owner and the other just the source package seemed a bit weird to me, but
> maybe there's a reason for doing so?).
>
> Also, do you really need the fixed width on the table? Shouldn't the
> surrounding boxes of the launchpad-form limit the width of the table? Or is
> this because you want them to have the exact same width as the recipe text
> area?
>
> [1] notice the different capitalization, as recommended by
> https://dev.launchpad.net/UserInterfaceWording

Thanks for such a detailed comment - really appreciated. The reason for showing the branch urls is to allow the user to explicitly see what the urls are and so to allow simple copy and paste into the recipe - I believe this to be one of the key drivers for this work; when creating a recipe, it's the most error prone bit and the info is not easily intuited unless it is displayed. The branch name is clear from reading the url hence I didn't see the need to show it.

The reason for showing the owner of the product series branches is that it is not obvious from the url who the owner is. The source package column in the source package branches table is to allow easy navigation to the source package home page. These choice were based on implementation discussions when the work was commenced, however I am more than happy to show whatever information is deemed necessary. If we want to continue showing the urls for the reason mentioned above, it's tricky showing too many columns given the urls are so long and they look terrible when wrapped.

Before I added the fixed table width, the tables expanded out to the right hand edge of the browser window and looked terrible - I'm not sure why the form didn't limit the width. Having the tables a similar width to the other main recipe form widgets (the description text area in particular) seems to look good to me but that's only an opinion.

I'll fix the capitalization.

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

> The column width issue is the same one that the milestone and project packages
> pages had to solve. User really do need the columns to be the same size and
> aligned to scan the page. But as Salgado notes, the columns do not have the
> same content. This is a bit like the related packages problem (see
> https://launchpad.net/~wgrant/+related-software for packages and ppa). This
> data issue might not be solvable now, but it would be nice to have the same
> column headings.
>

Do we want the same styling to be used here as on the related software page listed above? That would have been my preferred way of doing it - bold table heading text etc. I just used the CCS class "listing" for the table style because I thought that was the correct thing to do. It would be good to change the look of my page to match the one referenced.

> I think Salgado has an interesting point about showing the URL
> I suspect such a change will frustrate users who copy the branch URL from the
> listing. But I wonder. Why is the owner missing from the product URLs?
>
> So I think Salgado is asking the right question. I would rephrase it as "Why
> isn't the data symmetric?"

I agree it would be better to have the same column headings etc. The main reason the two tables have different headings is that the branch urls are so long and I didn't want them to wrap and so was limited in how much space could be devoted to adding other columns. Initial implementation discussion resulting in the key info being chosen for display for each branch type and although I could have displayed owner for source package branches (for example), decided not to in order to limit the required table width.

When I didn't limit the table width, the tables came out very wide - see the screenshot from the original implementation: http://people.canonical.com/~ianb/recipe-related-branches.png
The screenshot lacks the vertical space between the tables, and the columns are each very wide and so the tables could easily accommodate more columns with extra info being added, but I think it the ui looks terrible given the tables are so much wider than the other data entry widgets on the recipe form??

If we are happy to have the tables wider, I could look to make the data in each symmetric....

Revision history for this message
Guilherme Salgado (salgado) wrote :

I think it's good to have the links to the SourcePackage, but we should also have them for the ProductSeries. Maybe that could replace the owner column? Or is the owner really important for some reason?

I thought the text area widgets were constrained by the width of the surrounding elements, but I was wrong; zope's TextAreaWidget uses, by default, a 60-cols width. I'd also prefer if the tables had the same width as the other form elements, but I'm not sure using a fixed width on the table is a good idea because the widget's width might change but most importantly because I don't think the fixed-width table will behave the same way as the textare widget when the window is resized to a smaller size than what's needed to fit the textarea.

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

> I think it's good to have the links to the SourcePackage, but we should also
> have them for the ProductSeries. Maybe that could replace the owner column?
> Or is the owner really important for some reason?
>

I've added links to the product series instead of owner. I've also made the ui look better by including <h2> headings for each table. These headings and the table headers etc are all styled with the "code" domain css. Hence the headings appear in gold etc. New screenshot: http://people.canonical.com/~ianb/recipe-related-branches2.png

The ordering for the product series branches is based on the sorted_active_series_list property provided by the SortSeriesMixin. This provides only active series with the most recent first and the development focus at the top of the list.

I realised when doing these changes that I was exposing private branches and others the users had no right to see. So I fixed the implementation and added a couple of extra tests.

> I thought the text area widgets were constrained by the width of the
> surrounding elements, but I was wrong; zope's TextAreaWidget uses, by default,
> a 60-cols width. I'd also prefer if the tables had the same width as the
> other form elements, but I'm not sure using a fixed width on the table is a
> good idea because the widget's width might change but most importantly because
> I don't think the fixed-width table will behave the same way as the textare
> widget when the window is resized to a smaller size than what's needed to fit
> the textarea.

Before doing the fixed width tables, I did check the behaviour of the widgets' layouts when the browser window was resized and it all behaved as expected - the text area widget and tables retained their width and a simple horizontal scroll bar was added to the browser window.

What I've done now is to make the <fieldset> element which contains everything for the related branches widget a fixed size and remove the width styling from the tables. The resize behaviour is the same as described above but I think it's a cleaner implementation.

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Thu, 2011-01-27 at 12:57 +0000, Ian Booth wrote:
[...]
> > I thought the text area widgets were constrained by the width of the
> > surrounding elements, but I was wrong; zope's TextAreaWidget uses, by default,
> > a 60-cols width. I'd also prefer if the tables had the same width as the
> > other form elements, but I'm not sure using a fixed width on the table is a
> > good idea because the widget's width might change but most importantly because
> > I don't think the fixed-width table will behave the same way as the textare
> > widget when the window is resized to a smaller size than what's needed to fit
> > the textarea.
>
> Before doing the fixed width tables, I did check the behaviour of the
> widgets' layouts when the browser window was resized and it all
> behaved as expected - the text area widget and tables retained their
> width and a simple horizontal scroll bar was added to the browser
> window.

I thought the number of columns on a textarea wouldn't prevent the
browser from resizing it when the window is resized. That's the
behaviour I see with the comment box (which specifies cols=60) on a
merge-proposal page, but I tested here on a html file I crafted and the
browser didn't resize my textarea. Maybe Curtis will know why we're
seeing a different behaviour on the merge-proposal page.

>
> What I've done now is to make the <fieldset> element which contains
> everything for the related branches widget a fixed size and remove the
> width styling from the tables. The resize behaviour is the same as
> described above but I think it's a cleaner implementation.

Do you really think we should go with the fixed width, even with all its
drawbacks?

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

>
>>
>> What I've done now is to make the <fieldset> element which contains
>> everything for the related branches widget a fixed size and remove the
>> width styling from the tables. The resize behaviour is the same as
>> described above but I think it's a cleaner implementation.
>
> Do you really think we should go with the fixed width, even with all its
> drawbacks?
>

Without the fixed width, the tables expand right out to the edge of the
browser window, and on a screen with the browser maximised for example,
it looks really bad (to my eye) when all the form elements are around
the same width (approx size of text area widget) and just the related
branches tables are really wide. In my view, it's better visually to try
and maintain a similar width for all the form widgets and a consistent
resize behaviour ie if the text area widget is fixed, the the tables
(via the width styling on the fieldset element) should be too. When the
tables are really wide without the fixed width, the first column expands
way out and there's a lot of empty space and your eye is forced to scan
all the way to the right to see the 2nd table column. Do you agree with
this view? I'm not an css expert so I'd love to hear about any issues
with using the current fixed width approach that outweigh the positive
benefits outlined above.

Revision history for this message
Guilherme Salgado (salgado) wrote :

On Thu, 2011-01-27 at 14:28 +0000, Ian Booth wrote:
> >
> >>
> >> What I've done now is to make the <fieldset> element which contains
> >> everything for the related branches widget a fixed size and remove the
> >> width styling from the tables. The resize behaviour is the same as
> >> described above but I think it's a cleaner implementation.
> >
> > Do you really think we should go with the fixed width, even with all its
> > drawbacks?
> >
>
> Without the fixed width, the tables expand right out to the edge of the
> browser window, and on a screen with the browser maximised for example,
> it looks really bad (to my eye) when all the form elements are around
> the same width (approx size of text area widget) and just the related
> branches tables are really wide. In my view, it's better visually to try
> and maintain a similar width for all the form widgets and a consistent
> resize behaviour ie if the text area widget is fixed, the the tables
> (via the width styling on the fieldset element) should be too. When the
> tables are really wide without the fixed width, the first column expands
> way out and there's a lot of empty space and your eye is forced to scan
> all the way to the right to see the 2nd table column. Do you agree with
> this view? I'm not an css expert so I'd love to hear about any issues
> with using the current fixed width approach that outweigh the positive
> benefits outlined above.

Well, the main issue is that there's no way to make sure the table's
width is kept in sync with the widget's sync. You have a good point
that it will look really bad on wide windows, so I think you're right
that it's better to use the fixed width for now.

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

The implementation of the methods need to be put into the BranchTarget implementation classes.

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

Code changes made to move core model functionality to the IBranchTarget implementation. The View calls through to this class to get the required data. Separate tests written for IBranchTarget. The ordering of related branches is such the those for the most recent product series or distro series appear first. The sorting was done using sorted_version_numbers()

The related package branch listing now shows the distro series instead of the source package. I would appreciate a careful review of the algorithm used to load the related branches (especially the related package branches) to ensure everything is correct.

One question - I don't like it how the related branches tables are inside the form but I think it looks weird if they are moved outside the form and hence appear below the Update Recipe button. I'll do whatever is deemed best.

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

I added functionality to limit the number of related branches displayed to 5 in each category. See screenshot for example of how it looks:

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

The branches shown are the most recent according to the sorted_version_numbers() function:
- for related series, branches, the ordering is applied to the product series, and the devel focus is always shown
- for package branches, the ordering is applied to the distro series

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

On Mon, 31 Jan 2011 15:18:53 you wrote:
> I added functionality to limit the number of related branches displayed to
> 5 in each category. See screenshot for example of how it looks:
>
> http://people.canonical.com/~ianb/recipe-related-branches3.png
>
> The branches shown are the most recent according to the
> sorted_version_numbers() function: - for related series, branches, the
> ordering is applied to the product series, and the devel focus is always
> shown - for package branches, the ordering is applied to the distro series

I don't think it is beneficial to have the text: "Top 5 most recent
branches...".

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

I think that if we limit the results, the user should be informed, lest
they get confused as to why a branch they may have been expecting to be
there is not. But that's just my opinion as a general ui priciple. If
you feel it's better to remove the text, that's no problem. Or would a
rewording of the text be better?

On 31/01/11 12:28, Tim Penhey wrote:
> On Mon, 31 Jan 2011 15:18:53 you wrote:
>> I added functionality to limit the number of related branches displayed to
>> 5 in each category. See screenshot for example of how it looks:
>>
>> http://people.canonical.com/~ianb/recipe-related-branches3.png
>>
>> The branches shown are the most recent according to the
>> sorted_version_numbers() function: - for related series, branches, the
>> ordering is applied to the product series, and the devel focus is always
>> shown - for package branches, the ordering is applied to the distro series
>
> I don't think it is beneficial to have the text: "Top 5 most recent
> branches...".
>

Revision history for this message
Curtis Hovey (sinzui) wrote :

This is good to land.

review: Approve (ui)
Revision history for this message
Steve Kowalik (stevenk) wrote :

This looks okay to me, aside from a few blank lines that look to have crept -- however, if make lint disagrees with me, then I'll let that slide.

Why are the imports for lp.testing.factory right under the docstring rather than being ordered with the rest of them?

I'd suggest running utilites/format-imports over the files you changed.

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

On 02/02/11 10:43, Steve Kowalik wrote:
> Review: Approve code*
> This looks okay to me, aside from a few blank lines that look to have crept -- however, if make lint disagrees with me, then I'll let that slide.
>

Lint is happy - just complains about lines too long which were already
there, related to string literals in some tests.

> Why are the imports for lp.testing.factory right under the docstring rather than being ordered with the rest of them?
>

Cause I need my eyesight checked. Fixed.

> I'd suggest running utilites/format-imports over the files you changed.

Revision history for this message
Tim Penhey (thumper) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2011-01-28 01:15:05 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-02-02 01:11:03 +0000
@@ -14,7 +14,6 @@
14 'SourcePackageRecipeView',14 'SourcePackageRecipeView',
15 ]15 ]
1616
17
18from bzrlib.plugins.builder.recipe import (17from bzrlib.plugins.builder.recipe import (
19 ForbiddenInstructionError,18 ForbiddenInstructionError,
20 RecipeParseError,19 RecipeParseError,
@@ -24,6 +23,9 @@
24from lazr.lifecycle.snapshot import Snapshot23from lazr.lifecycle.snapshot import Snapshot
25from lazr.restful.interface import use_template24from lazr.restful.interface import use_template
26from storm.locals import Store25from storm.locals import Store
26from z3c.ptcompat import ViewPageTemplateFile
27from zope.app.form.browser.widget import Widget
28from zope.app.form.interfaces import IView
27from zope.component import getUtility29from zope.component import getUtility
28from zope.event import notify30from zope.event import notify
29from zope.formlib import form31from zope.formlib import form
@@ -33,6 +35,7 @@
33 providedBy,35 providedBy,
34 )36 )
35from zope.schema import (37from zope.schema import (
38 Field,
36 Choice,39 Choice,
37 List,40 List,
38 Text,41 Text,
@@ -79,12 +82,14 @@
79 PrivateBranchRecipe,82 PrivateBranchRecipe,
80 TooNewRecipeFormat,83 TooNewRecipeFormat,
81 )84 )
85from lp.code.interfaces.branchtarget import IBranchTarget
82from lp.code.interfaces.sourcepackagerecipe import (86from lp.code.interfaces.sourcepackagerecipe import (
83 ISourcePackageRecipe,87 ISourcePackageRecipe,
84 ISourcePackageRecipeSource,88 ISourcePackageRecipeSource,
85 MINIMAL_RECIPE_TEXT,89 MINIMAL_RECIPE_TEXT,
86 )90 )
87from lp.registry.interfaces.pocket import PackagePublishingPocket91from lp.registry.interfaces.pocket import PackagePublishingPocket
92from lp.services.propertycache import cachedproperty
88from lp.soyuz.model.archive import Archive93from lp.soyuz.model.archive import Archive
8994
9095
@@ -394,7 +399,67 @@
394 self.setFieldError('recipe_text', str(error))399 self.setFieldError('recipe_text', str(error))
395400
396401
397class SourcePackageRecipeAddView(RecipeTextValidatorMixin, LaunchpadFormView):402class RelatedBranchesWidget(Widget):
403 """A widget to render the related branches for a recipe."""
404 implements(IView)
405
406 __call__ = ViewPageTemplateFile(
407 '../templates/sourcepackagerecipe-related-branches.pt')
408
409 related_package_branch_info = []
410 related_series_branch_info = []
411
412 def hasInput(self):
413 return True
414
415 def setRenderedValue(self, value):
416 self.related_package_branch_info = (
417 value['related_package_branch_info'])
418 self.related_series_branch_info = value['related_series_branch_info']
419
420
421class RecipeRelatedBranchesMixin(LaunchpadFormView):
422 """A class to find related branches for a recipe's base branch."""
423
424 custom_widget('related-branches', RelatedBranchesWidget)
425
426 def extendFields(self):
427 """See `LaunchpadFormView`.
428
429 Adds a related branches field to the form.
430 """
431 self.form_fields += form.Fields(Field(__name__='related-branches'))
432 self.form_fields['related-branches'].custom_widget = (
433 self.custom_widgets['related-branches'])
434 self.widget_errors['related-branches'] = ''
435
436 def setUpWidgets(self, context=None):
437 # Adds a new related branches widget.
438 super(RecipeRelatedBranchesMixin, self).setUpWidgets(context)
439 self.widgets['related-branches'].display_label = False
440 self.widgets['related-branches'].setRenderedValue(dict(
441 related_package_branch_info=self.related_package_branch_info,
442 related_series_branch_info=self.related_series_branch_info))
443
444 @cachedproperty
445 def related_series_branch_info(self):
446 branch_to_check = self.getBranch()
447 return IBranchTarget(
448 branch_to_check.target).getRelatedSeriesBranchInfo(
449 branch_to_check,
450 limit_results=5)
451
452 @cachedproperty
453 def related_package_branch_info(self):
454 branch_to_check = self.getBranch()
455 return IBranchTarget(
456 branch_to_check.target).getRelatedPackageBranchInfo(
457 branch_to_check,
458 limit_results=5)
459
460
461class SourcePackageRecipeAddView(RecipeRelatedBranchesMixin,
462 RecipeTextValidatorMixin, LaunchpadFormView):
398 """View for creating Source Package Recipes."""463 """View for creating Source Package Recipes."""
399464
400 title = label = 'Create a new source package recipe'465 title = label = 'Create a new source package recipe'
@@ -424,6 +489,10 @@
424 # all confused when we want to create a new PPA.489 # all confused when we want to create a new PPA.
425 archive_widget._displayItemForMissingValue = False490 archive_widget._displayItemForMissingValue = False
426491
492 def getBranch(self):
493 """The branch on which the recipe is built."""
494 return self.context
495
427 @property496 @property
428 def initial_values(self):497 def initial_values(self):
429 return {498 return {
@@ -495,10 +564,15 @@
495 self.setFieldError('ppa_name', error)564 self.setFieldError('ppa_name', error)
496565
497566
498class SourcePackageRecipeEditView(RecipeTextValidatorMixin,567class SourcePackageRecipeEditView(RecipeRelatedBranchesMixin,
568 RecipeTextValidatorMixin,
499 LaunchpadEditFormView):569 LaunchpadEditFormView):
500 """View for editing Source Package Recipes."""570 """View for editing Source Package Recipes."""
501571
572 def getBranch(self):
573 """The branch on which the recipe is built."""
574 return self.context.base_branch
575
502 @property576 @property
503 def title(self):577 def title(self):
504 return 'Edit %s source package recipe' % self.context.name578 return 'Edit %s source package recipe' % self.context.name
505579
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-01-21 16:52:57 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2011-02-02 01:11:03 +0000
@@ -7,6 +7,7 @@
7__metaclass__ = type7__metaclass__ = type
88
99
10from BeautifulSoup import BeautifulSoup
10from datetime import (11from datetime import (
11 datetime,12 datetime,
12 timedelta,13 timedelta,
@@ -29,6 +30,7 @@
29 get_radio_button_text_for_field,30 get_radio_button_text_for_field,
30 )31 )
31from canonical.launchpad.webapp import canonical_url32from canonical.launchpad.webapp import canonical_url
33from canonical.launchpad.webapp.interfaces import ILaunchpadRoot
32from canonical.testing.layers import (34from canonical.testing.layers import (
33 DatabaseFunctionalLayer,35 DatabaseFunctionalLayer,
34 LaunchpadFunctionalLayer,36 LaunchpadFunctionalLayer,
@@ -103,6 +105,81 @@
103 ' Secret Squirrel changes.', branches=[cake_branch],105 ' Secret Squirrel changes.', branches=[cake_branch],
104 daily_build_archive=self.ppa)106 daily_build_archive=self.ppa)
105107
108 def checkRelatedBranches(self, related_series_branch_info,
109 related_package_branch_info, browser_contents):
110 """Check that the browser contents contain the correct branch info."""
111 login(ANONYMOUS)
112 soup = BeautifulSoup(browser_contents)
113
114 # The related branches collapsible section needs to be there.
115 related_branches = soup.find('fieldset', {'id': 'related-branches'})
116 self.assertIsNot(related_branches, None)
117
118 # Check the related package branches.
119 root_url = canonical_url(
120 getUtility(ILaunchpadRoot), rootsite='code')
121 root_url = root_url.rstrip('/')
122 branch_table = soup.find(
123 'table', {'id': 'related-package-branches-listing'})
124 if not related_package_branch_info:
125 self.assertIs(branch_table, None)
126 else:
127 rows = branch_table.tbody.findAll('tr')
128
129 package_branches_info = []
130 for row in rows:
131 branch_links = row.findAll('a')
132 self.assertEqual(2, len(branch_links))
133 package_branches_info.append(
134 '%s%s' % (root_url, branch_links[0]['href']))
135 package_branches_info.append(branch_links[0].renderContents())
136 package_branches_info.append(
137 '%s%s' % (root_url, branch_links[1]['href']))
138 package_branches_info.append(branch_links[1].renderContents())
139 expected_branch_info = []
140 for branch_info in related_package_branch_info:
141 branch = branch_info[0]
142 distro_series = branch_info[1]
143 expected_branch_info.append(
144 canonical_url(branch, rootsite='code'))
145 expected_branch_info.append(branch.displayname)
146 expected_branch_info.append(
147 canonical_url(distro_series, rootsite='code'))
148 expected_branch_info.append(distro_series.name)
149 self.assertEqual(package_branches_info, expected_branch_info)
150
151 # Check the related series branches.
152 branch_table = soup.find(
153 'table', {'id': 'related-series-branches-listing'})
154 if not related_series_branch_info:
155 self.assertIs(branch_table, None)
156 else:
157 rows = branch_table.tbody.findAll('tr')
158
159 series_branches_info = []
160 for row in rows:
161 branch_links = row.findAll('a')
162 self.assertEqual(2, len(branch_links))
163 series_branches_info.append(
164 '%s%s' % (root_url, branch_links[0]['href']))
165 series_branches_info.append(branch_links[0].renderContents())
166 series_branches_info.append(branch_links[1]['href'])
167 series_branches_info.append(branch_links[1].renderContents())
168 expected_branch_info = []
169 for branch_info in related_series_branch_info:
170 branch = branch_info[0]
171 product_series = branch_info[1]
172 expected_branch_info.append(
173 canonical_url(branch,
174 rootsite='code',
175 path_only_if_possible=True))
176 expected_branch_info.append(branch.displayname)
177 expected_branch_info.append(
178 canonical_url(product_series,
179 path_only_if_possible=True))
180 expected_branch_info.append(product_series.name)
181 self.assertEqual(expected_branch_info, series_branches_info)
182
106183
107def get_message_text(browser, index):184def get_message_text(browser, index):
108 """Return the text of a message, specified by index."""185 """Return the text of a message, specified by index."""
@@ -353,8 +430,8 @@
353 browser.getControl('Automatically build each day').click()430 browser.getControl('Automatically build each day').click()
354 browser.getControl('Create Recipe').click()431 browser.getControl('Create Recipe').click()
355 self.assertEqual(432 self.assertEqual(
356 extract_text(find_tags_by_class(browser.contents, 'message')[2]),433 'You must specify at least one series for daily builds.',
357 'You must specify at least one series for daily builds.')434 get_message_text(browser, 2))
358435
359 def test_create_recipe_bad_base_branch(self):436 def test_create_recipe_bad_base_branch(self):
360 # If a user tries to create source package recipe with a bad base437 # If a user tries to create source package recipe with a bad base
@@ -431,6 +508,81 @@
431 get_message_text(browser, 2),508 get_message_text(browser, 2),
432 'Recipe may not refer to private branch: %s' % bzr_identity)509 'Recipe may not refer to private branch: %s' % bzr_identity)
433510
511 def _test_new_recipe_with_no_related_branches(self, branch):
512 # The Related Branches section should not appear if there are no
513 # related branches.
514 # A new recipe can be created from the branch page.
515 browser = self.getUserBrowser(
516 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
517 # There shouldn't be a related-branches section if there are no
518 # related branches..
519 soup = BeautifulSoup(browser.contents)
520 related_branches = soup.find('fieldset', {'id': 'related-branches'})
521 self.assertIs(related_branches, None)
522
523 def test_new_product_branch_with_no_related_branches_recipe(self):
524 # We can create a new recipe off a product branch.
525 branch = self.factory.makeBranch()
526 self._test_new_recipe_with_no_related_branches(branch)
527
528 def test_new_package_branch_with_no_linked_branches_recipe(self):
529 # We can create a new recipe off a sourcepackage branch where the
530 # sourcepackage has no linked branches.
531 branch = self.factory.makePackageBranch()
532 self._test_new_recipe_with_no_related_branches(branch)
533
534 def test_new_recipe_with_package_branches(self):
535 # The series branches table should not appear if there are none.
536 (branch, related_series_branch_info, related_package_branches) = (
537 self.factory.makeRelatedBranches(with_series_branches=False))
538 browser = self.getUserBrowser(
539 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
540 soup = BeautifulSoup(browser.contents)
541 related_branches = soup.find('fieldset', {'id': 'related-branches'})
542 self.assertIsNot(related_branches, None)
543 related_branches = soup.find(
544 'div', {'id': 'related-package-branches'})
545 self.assertIsNot(related_branches, None)
546 related_branches = soup.find(
547 'div', {'id': 'related-series-branches'})
548 self.assertIs(related_branches, None)
549
550 def test_new_recipe_with_series_branches(self):
551 # The package branches table should not appear if there are none.
552 (branch, related_series_branch_info, related_package_branches) = (
553 self.factory.makeRelatedBranches(with_package_branches=False))
554 browser = self.getUserBrowser(
555 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
556 soup = BeautifulSoup(browser.contents)
557 related_branches = soup.find('fieldset', {'id': 'related-branches'})
558 self.assertIsNot(related_branches, None)
559 related_branches = soup.find(
560 'div', {'id': 'related-series-branches'})
561 self.assertIsNot(related_branches, None)
562 related_branches = soup.find(
563 'div', {'id': 'related-package-branches'})
564 self.assertIs(related_branches, None)
565
566 def test_new_product_branch_recipe_with_related_branches(self):
567 # The related branches should be rendered correctly on the page.
568 (branch, related_series_branch_info,
569 related_package_branch_info) = self.factory.makeRelatedBranches()
570 browser = self.getUserBrowser(
571 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
572 self.checkRelatedBranches(
573 related_series_branch_info, related_package_branch_info,
574 browser.contents)
575
576 def test_new_sourcepackage_branch_recipe_with_related_branches(self):
577 # The related branches should be rendered correctly on the page.
578 reference_branch= self.factory.makePackageBranch()
579 (branch, ignore, related_package_branch_info) = (
580 self.factory.makeRelatedBranches(reference_branch))
581 browser = self.getUserBrowser(
582 canonical_url(branch, view_name='+new-recipe'), user=self.chef)
583 self.checkRelatedBranches(
584 set(), related_package_branch_info, browser.contents)
585
434 def test_ppa_selector_not_shown_if_user_has_no_ppas(self):586 def test_ppa_selector_not_shown_if_user_has_no_ppas(self):
435 # If the user creating a recipe has no existing PPAs, the selector587 # If the user creating a recipe has no existing PPAs, the selector
436 # isn't shown, but the field to enter a new PPA name is.588 # isn't shown, but the field to enter a new PPA name is.
@@ -834,6 +986,98 @@
834 get_message_text(browser, 1),986 get_message_text(browser, 1),
835 'Recipe may not refer to private branch: %s' % bzr_identity)987 'Recipe may not refer to private branch: %s' % bzr_identity)
836988
989 def _test_edit_recipe_with_no_related_branches(self, recipe):
990 # The Related Branches section should not appear if there are no
991 # related branches.
992 browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
993 browser.getLink('Edit recipe').click()
994 # There shouldn't be a related-branches section if there are no
995 # related branches.
996 soup = BeautifulSoup(browser.contents)
997 related_branches = soup.find('fieldset', {'id': 'related-branches'})
998 self.assertIs(related_branches, None)
999
1000 def test_edit_product_branch_with_no_related_branches_recipe(self):
1001 # The Related Branches section should not appear if there are no
1002 # related branches.
1003 base_branch = self.factory.makeBranch()
1004 recipe = self.factory.makeSourcePackageRecipe(
1005 owner=self.chef, branches=[base_branch])
1006 self._test_edit_recipe_with_no_related_branches(recipe)
1007
1008 def test_edit_sourcepackage_branch_with_no_related_branches_recipe(self):
1009 # The Related Branches section should not appear if there are no
1010 # related branches.
1011 base_branch = self.factory.makePackageBranch()
1012 recipe = self.factory.makeSourcePackageRecipe(
1013 owner=self.chef, branches=[base_branch])
1014 self._test_edit_recipe_with_no_related_branches(recipe)
1015
1016 def test_edit_recipe_with_package_branches(self):
1017 # The series branches table should not appear if there are none.
1018 with person_logged_in(self.chef):
1019 recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
1020 self.factory.makeRelatedBranches(
1021 reference_branch=recipe.base_branch,
1022 with_series_branches=False)
1023 browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
1024 browser.getLink('Edit recipe').click()
1025 soup = BeautifulSoup(browser.contents)
1026 related_branches = soup.find('fieldset', {'id': 'related-branches'})
1027 self.assertIsNot(related_branches, None)
1028 related_branches = soup.find(
1029 'div', {'id': 'related-package-branches'})
1030 self.assertIsNot(related_branches, None)
1031 related_branches = soup.find(
1032 'div', {'id': 'related-series-branches'})
1033 self.assertIs(related_branches, None)
1034
1035 def test_edit_recipe_with_series_branches(self):
1036 # The package branches table should not appear if there are none.
1037 with person_logged_in(self.chef):
1038 recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
1039 self.factory.makeRelatedBranches(
1040 reference_branch=recipe.base_branch,
1041 with_package_branches=False)
1042 browser = self.getUserBrowser(canonical_url(recipe), user=self.chef)
1043 browser.getLink('Edit recipe').click()
1044 soup = BeautifulSoup(browser.contents)
1045 related_branches = soup.find('fieldset', {'id': 'related-branches'})
1046 self.assertIsNot(related_branches, None)
1047 related_branches = soup.find(
1048 'div', {'id': 'related-series-branches'})
1049 self.assertIsNot(related_branches, None)
1050 related_branches = soup.find(
1051 'div', {'id': 'related-package-branches'})
1052 self.assertIs(related_branches, None)
1053
1054 def test_edit_product_branch_recipe_with_related_branches(self):
1055 # The related branches should be rendered correctly on the page.
1056 with person_logged_in(self.chef):
1057 recipe = self.factory.makeSourcePackageRecipe(owner=self.chef)
1058 (branch, related_series_branch_info,
1059 related_package_branch_info) = (
1060 self.factory.makeRelatedBranches(
1061 reference_branch=recipe.base_branch))
1062 browser = self.getUserBrowser(
1063 canonical_url(recipe, view_name='+edit'), user=self.chef)
1064 self.checkRelatedBranches(
1065 related_series_branch_info, related_package_branch_info,
1066 browser.contents)
1067
1068 def test_edit_sourcepackage_branch_recipe_with_related_branches(self):
1069 # The related branches should be rendered correctly on the page.
1070 with person_logged_in(self.chef):
1071 reference_branch= self.factory.makePackageBranch()
1072 recipe = self.factory.makeSourcePackageRecipe(
1073 owner=self.chef, branches=[reference_branch])
1074 (branch, ignore, related_package_branch_info) = (
1075 self.factory.makeRelatedBranches(reference_branch))
1076 browser = self.getUserBrowser(
1077 canonical_url(recipe, view_name='+edit'), user=self.chef)
1078 self.checkRelatedBranches(
1079 set(), related_package_branch_info, browser.contents)
1080
8371081
838class TestSourcePackageRecipeView(TestCaseForRecipe):1082class TestSourcePackageRecipeView(TestCaseForRecipe):
8391083
8401084
=== modified file 'lib/lp/code/interfaces/branchtarget.py'
--- lib/lp/code/interfaces/branchtarget.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/branchtarget.py 2011-02-02 01:11:03 +0000
@@ -26,7 +26,6 @@
26from zope.security.interfaces import Unauthorized26from zope.security.interfaces import Unauthorized
2727
28from canonical.launchpad import _28from canonical.launchpad import _
29from canonical.launchpad.webapp.interfaces import IPrimaryContext
30from lp.code.enums import BranchType29from lp.code.enums import BranchType
3130
3231
@@ -143,3 +142,33 @@
143 :returns: an `ICodeImport`.142 :returns: an `ICodeImport`.
144 :raises AssertionError: if supports_code_imports is False.143 :raises AssertionError: if supports_code_imports is False.
145 """144 """
145
146 def getRelatedSeriesBranchInfo(parent_branch, limit_results=None):
147 """Find development branch info related to this parent branch.
148
149 The result is a list of tuples:
150 (branch, product_series)
151 where:
152 branch: the related branch.
153 product_series: the product series associated with the branch.
154
155 The development focus is first in the list.
156
157 :param parent_branch: `IBranch` we are finding related branches for.
158 :param limit_results: if not None, limit the number of results to the
159 specified value.
160 """
161
162 def getRelatedPackageBranchInfo(parent_branch, limit_results=None):
163 """Find package branch info related to this parent branch.
164
165 The result is a list of tuples:
166 (branch, distro_series)
167 where:
168 branch: the related branch.
169 distro_series: the distro series associated with the branch.
170
171 :param parent_branch: `IBranch` we are finding related branches for.
172 :param limit_results: if not None, limit the number of results to the
173 specified value.
174 """
146175
=== modified file 'lib/lp/code/model/branchtarget.py'
--- lib/lp/code/model/branchtarget.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/branchtarget.py 2011-02-02 01:11:03 +0000
@@ -11,18 +11,25 @@
11 'ProductBranchTarget',11 'ProductBranchTarget',
12 ]12 ]
1313
14from operator import attrgetter
15
14from zope.component import getUtility16from zope.component import getUtility
15from zope.interface import implements17from zope.interface import implements
16from zope.security.proxy import isinstance as zope_isinstance18from zope.security.proxy import isinstance as zope_isinstance
1719
20from canonical.launchpad.webapp.authorization import check_permission
18from canonical.launchpad.webapp.interfaces import ICanonicalUrlData21from canonical.launchpad.webapp.interfaces import ICanonicalUrlData
22from canonical.launchpad.webapp.sorting import sorted_version_numbers
23from lp.code.errors import NoLinkedBranch
19from lp.code.interfaces.branchcollection import IAllBranches24from lp.code.interfaces.branchcollection import IAllBranches
20from lp.code.interfaces.branchtarget import (25from lp.code.interfaces.branchtarget import (
21 check_default_stacked_on,26 check_default_stacked_on,
22 IBranchTarget,27 IBranchTarget,
23 )28 )
24from lp.code.interfaces.codeimport import ICodeImportSet29from lp.code.interfaces.codeimport import ICodeImportSet
30from lp.code.interfaces.linkedbranch import get_linked_to_branch
25from lp.registry.interfaces.pocket import PackagePublishingPocket31from lp.registry.interfaces.pocket import PackagePublishingPocket
32from lp.registry.interfaces.series import SeriesStatus
2633
2734
28def branch_to_target(branch):35def branch_to_target(branch):
@@ -45,6 +52,14 @@
45 registrant, self, branch_name, rcs_type, url=url,52 registrant, self, branch_name, rcs_type, url=url,
46 cvs_root=cvs_root, cvs_module=cvs_module, owner=owner)53 cvs_root=cvs_root, cvs_module=cvs_module, owner=owner)
4754
55 def getRelatedSeriesBranchInfo(self, parent_branch, limit_results=None):
56 """See `IBranchTarget`."""
57 return []
58
59 def getRelatedPackageBranchInfo(self, parent_branch, limit_results=None):
60 """See `IBranchTarget`."""
61 return []
62
4863
49class PackageBranchTarget(_BaseBranchTarget):64class PackageBranchTarget(_BaseBranchTarget):
50 implements(IBranchTarget)65 implements(IBranchTarget)
@@ -159,6 +174,26 @@
159 branch.distroseries = self.sourcepackage.distroseries174 branch.distroseries = self.sourcepackage.distroseries
160 branch.sourcepackagename = self.sourcepackage.sourcepackagename175 branch.sourcepackagename = self.sourcepackage.sourcepackagename
161176
177 def getRelatedPackageBranchInfo(self, parent_branch, limit_results=None):
178 """See `IBranchTarget`."""
179 result = []
180 linked_branches = self.sourcepackage.linkedBranches()
181 distroseries = self.sourcepackage.distroseries
182 result.extend(
183 [(branch, distroseries)
184 for branch in linked_branches.values()
185 if (check_permission('launchpad.View', branch) and
186 branch != parent_branch and
187 distroseries.status != SeriesStatus.OBSOLETE)])
188
189 result = sorted_version_numbers(result, key=lambda branch_info: (
190 getattr(branch_info[1], 'name')))
191
192 if limit_results is not None:
193 # We only want the most recent branches
194 result = result[:limit_results]
195 return result
196
162197
163class PersonBranchTarget(_BaseBranchTarget):198class PersonBranchTarget(_BaseBranchTarget):
164 implements(IBranchTarget)199 implements(IBranchTarget)
@@ -338,6 +373,58 @@
338 branch.distroseries = None373 branch.distroseries = None
339 branch.sourcepackagename = None374 branch.sourcepackagename = None
340375
376 def getRelatedSeriesBranchInfo(self, parent_branch, limit_results=None):
377 """See `IBranchTarget`."""
378 sorted_series = []
379 for series in self.product.series:
380 if (series.status != SeriesStatus.OBSOLETE
381 and series != self.product.development_focus):
382 sorted_series.append(series)
383 # Now sort the list by name with newer versions before older.
384 sorted_series = sorted_version_numbers(sorted_series,
385 key=attrgetter('name'))
386 # Add the development focus first.
387 sorted_series.insert(0, self.product.development_focus)
388
389 result = []
390 for series in sorted_series:
391 try:
392 branch = get_linked_to_branch(series).branch
393 if (branch not in result and branch != parent_branch and
394 check_permission('launchpad.View', branch)):
395 result.append((branch, series))
396 except NoLinkedBranch:
397 # If there's no branch for a particular series, we don't care.
398 pass
399
400 if limit_results is not None:
401 # We only want the most recent branches
402 result = result[:limit_results]
403 return result
404
405 def getRelatedPackageBranchInfo(self, parent_branch, limit_results=None):
406 """See `IBranchTarget`."""
407 result = []
408 for distrosourcepackage in self.product.distrosourcepackages:
409 try:
410 branch = get_linked_to_branch(distrosourcepackage).branch
411 series = distrosourcepackage.distribution.currentseries
412 if (branch != parent_branch and series is not None and
413 check_permission('launchpad.View', branch)):
414 result.append((branch, series))
415 except NoLinkedBranch:
416 # If there's no branch for a particular source package,
417 # we don't care.
418 pass
419
420 result = sorted_version_numbers(result, key=lambda branch_info: (
421 getattr(branch_info[1], 'name')))
422
423 if limit_results is not None:
424 # We only want the most recent branches
425 result = result[:limit_results]
426 return result
427
341428
342def get_canonical_url_data_for_target(branch_target):429def get_canonical_url_data_for_target(branch_target):
343 """Return the `ICanonicalUrlData` for an `IBranchTarget`."""430 """Return the `ICanonicalUrlData` for an `IBranchTarget`."""
@@ -348,6 +435,7 @@
348 """The Product itself is the branch target given a ProductSeries."""435 """The Product itself is the branch target given a ProductSeries."""
349 return ProductBranchTarget(product_series.product)436 return ProductBranchTarget(product_series.product)
350437
438
351def distribution_sourcepackage_to_branch_target(distro_sourcepackage):439def distribution_sourcepackage_to_branch_target(distro_sourcepackage):
352 """The development version of the distro sourcepackage is the target."""440 """The development version of the distro sourcepackage is the target."""
353 dev_version = distro_sourcepackage.development_version441 dev_version = distro_sourcepackage.development_version
354442
=== modified file 'lib/lp/code/model/tests/test_branchtarget.py'
--- lib/lp/code/model/tests/test_branchtarget.py 2010-10-04 19:50:45 +0000
+++ lib/lp/code/model/tests/test_branchtarget.py 2011-02-02 01:11:03 +0000
@@ -221,6 +221,30 @@
221 self.assertEqual(owner, code_import.branch.owner)221 self.assertEqual(owner, code_import.branch.owner)
222 self.assertEqual(self.target, code_import.branch.target)222 self.assertEqual(self.target, code_import.branch.target)
223223
224 def test_related_branches(self):
225 (branch, related_series_branch_info,
226 related_package_branches) = (
227 self.factory.makeRelatedBranchesForSourcePackage(
228 sourcepackage=self.original))
229 self.assertEqual(
230 related_series_branch_info,
231 self.target.getRelatedSeriesBranchInfo(branch))
232 self.assertEqual(
233 related_package_branches,
234 self.target.getRelatedPackageBranchInfo(branch))
235
236 def test_related_branches_with_private_branch(self):
237 (branch, related_series_branch_info,
238 related_package_branches) = (
239 self.factory.makeRelatedBranchesForSourcePackage(
240 sourcepackage=self.original, with_private_branches=True))
241 self.assertEqual(
242 related_series_branch_info,
243 self.target.getRelatedSeriesBranchInfo(branch))
244 self.assertEqual(
245 related_package_branches,
246 self.target.getRelatedPackageBranchInfo(branch))
247
224248
225class TestPersonBranchTarget(TestCaseWithFactory, BaseBranchTargetTests):249class TestPersonBranchTarget(TestCaseWithFactory, BaseBranchTargetTests):
226250
@@ -452,6 +476,42 @@
452 self.assertEqual(owner, code_import.branch.owner)476 self.assertEqual(owner, code_import.branch.owner)
453 self.assertEqual(self.target, code_import.branch.target)477 self.assertEqual(self.target, code_import.branch.target)
454478
479 def test_related_branches(self):
480 (branch, related_series_branch_info,
481 related_package_branches) = (
482 self.factory.makeRelatedBranchesForProduct(
483 product=self.original))
484 self.assertEqual(
485 related_series_branch_info,
486 self.target.getRelatedSeriesBranchInfo(branch))
487 self.assertEqual(
488 related_package_branches,
489 self.target.getRelatedPackageBranchInfo(branch))
490
491 def test_related_branches_with_private_branch(self):
492 (branch, related_series_branch_info,
493 related_package_branches) = (
494 self.factory.makeRelatedBranchesForProduct(
495 product=self.original, with_private_branches=True))
496 self.assertEqual(
497 related_series_branch_info,
498 self.target.getRelatedSeriesBranchInfo(branch))
499 self.assertEqual(
500 related_package_branches,
501 self.target.getRelatedPackageBranchInfo(branch))
502
503 def test_related_branches_with_limit(self):
504 (branch, related_series_branch_info,
505 related_package_branches) = (
506 self.factory.makeRelatedBranchesForProduct(
507 product=self.original))
508 self.assertEqual(
509 related_series_branch_info[:2],
510 self.target.getRelatedSeriesBranchInfo(branch, 2))
511 self.assertEqual(
512 related_package_branches[:2],
513 self.target.getRelatedPackageBranchInfo(branch, 2))
514
455515
456class TestCheckDefaultStackedOnBranch(TestCaseWithFactory):516class TestCheckDefaultStackedOnBranch(TestCaseWithFactory):
457 """Only certain branches are allowed to be default stacked-on branches."""517 """Only certain branches are allowed to be default stacked-on branches."""
458518
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-new.pt'
--- lib/lp/code/templates/sourcepackagerecipe-new.pt 2010-12-22 02:01:36 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-new.pt 2011-02-02 01:11:03 +0000
@@ -104,6 +104,9 @@
104 <tal:widget define="widget nocall:view/widgets/recipe_text">104 <tal:widget define="widget nocall:view/widgets/recipe_text">
105 <metal:block use-macro="context/@@launchpad_form/widget_row" />105 <metal:block use-macro="context/@@launchpad_form/widget_row" />
106 </tal:widget>106 </tal:widget>
107 <tal:widget define="widget nocall:view/widgets/related-branches">
108 <metal:block use-macro="context/@@launchpad_form/widget_row" />
109 </tal:widget>
107110
108 </table>111 </table>
109 </metal:formbody>112 </metal:formbody>
110113
=== added file 'lib/lp/code/templates/sourcepackagerecipe-related-branches.pt'
--- lib/lp/code/templates/sourcepackagerecipe-related-branches.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-related-branches.pt 2011-02-02 01:11:03 +0000
@@ -0,0 +1,75 @@
1<fieldset id="related-branches"
2 class="collapsible collapsed" style="width: 60em"
3 tal:define="seriesBranches view/related_series_branch_info;
4 packageBranches view/related_package_branch_info"
5 tal:condition="python: seriesBranches or packageBranches">
6 <legend>Related Branches</legend>
7 <table>
8
9 <div tal:condition="packageBranches" id="related-package-branches">
10 <h2>Source package branches</h2>
11 <table class="listing" id="related-package-branches-listing">
12 <thead>
13 <tr>
14 <th>Branch URL</th>
15 <th>Distro series</th>
16 </tr>
17 </thead>
18 <tbody>
19 <tr tal:repeat="branch_info packageBranches">
20 <tal:defines define="branch python:branch_info[0];
21 distro_series python:branch_info[1];">
22 <td width="80%">
23 <a href="#"
24 tal:content="branch/displayname"
25 tal:attributes="href branch/fmt:url">
26 source package branch
27 </a>
28 </td>
29 <td>
30 <a href="#"
31 tal:attributes="href distro_series/fmt:url"
32 tal:content="distro_series/name">
33 distro series
34 </a>
35 </td>
36 </tal:defines>
37 </tr>
38 </tbody>
39 </table>
40 </div>
41
42 <div tal:condition="seriesBranches" id="related-series-branches">
43 <h2 style="margin-top: 1.5em;">Product series branches</h2>
44 <table class="listing" id="related-series-branches-listing">
45 <thead>
46 <tr>
47 <th>Branch URL</th>
48 <th>Product series</th>
49 </tr>
50 </thead>
51 <tbody>
52 <tr tal:repeat="branch_info seriesBranches">
53 <tal:defines define="branch python:branch_info[0];
54 product_series python:branch_info[1];">
55 <td width="80%">
56 <a href="#"
57 tal:content="branch/displayname"
58 tal:attributes="href branch/fmt:url">
59 product series branch
60 </a>
61 </td>
62 <td>
63 <a href="#"
64 tal:content="product_series/name"
65 tal:attributes="href product_series/fmt:url">
66 product series
67 </a>
68 </td>
69 </tal:defines>
70 </tr>
71 </tbody>
72 </table>
73 </div>
74 </table>
75</fieldset>
076
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-01-31 18:00:50 +0000
+++ lib/lp/testing/factory.py 2011-02-02 01:11:03 +0000
@@ -100,6 +100,7 @@
100 MAIN_STORE,100 MAIN_STORE,
101 OAuthPermission,101 OAuthPermission,
102 )102 )
103from canonical.launchpad.webapp.sorting import sorted_version_numbers
103from lp.app.enums import ServiceUsage104from lp.app.enums import ServiceUsage
104from lp.archiveuploader.dscfile import DSCFile105from lp.archiveuploader.dscfile import DSCFile
105from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy106from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
@@ -147,6 +148,7 @@
147from lp.code.interfaces.codeimportevent import ICodeImportEventSet148from lp.code.interfaces.codeimportevent import ICodeImportEventSet
148from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet149from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet
149from lp.code.interfaces.codeimportresult import ICodeImportResultSet150from lp.code.interfaces.codeimportresult import ICodeImportResultSet
151from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
150from lp.code.interfaces.revision import IRevisionSet152from lp.code.interfaces.revision import IRevisionSet
151from lp.code.interfaces.sourcepackagerecipe import (153from lp.code.interfaces.sourcepackagerecipe import (
152 ISourcePackageRecipeSource,154 ISourcePackageRecipeSource,
@@ -1148,6 +1150,157 @@
1148 name, owner, registrant, description, configuration, branches)1150 name, owner, registrant, description, configuration, branches)
1149 return queue1151 return queue
11501152
1153 def makeRelatedBranchesForSourcePackage(self, sourcepackage=None,
1154 **kwargs):
1155 """Create some branches associated with a sourcepackage."""
1156
1157 reference_branch = self.makePackageBranch(sourcepackage=sourcepackage)
1158 return self.makeRelatedBranches(
1159 reference_branch=reference_branch, **kwargs)
1160
1161 def makeRelatedBranchesForProduct(self, product=None, **kwargs):
1162 """Create some branches associated with a product."""
1163
1164 reference_branch = self.makeProductBranch(product=product)
1165 return self.makeRelatedBranches(
1166 reference_branch=reference_branch, **kwargs)
1167
1168 def makeRelatedBranches(self, reference_branch=None,
1169 with_series_branches=True,
1170 with_package_branches=True,
1171 with_private_branches=False):
1172 """Create some branches associated with a reference branch.
1173 The other branches are:
1174 - series branches: a set of branches associated with product
1175 series of the same product as the reference branch.
1176 - package branches: a set of branches associated with packagesource
1177 entities of the same product as the reference branch or the same
1178 sourcepackage depending on what type of branch it is.
1179
1180 If no reference branch is supplied, create one.
1181
1182 Returns: a tuple consisting of
1183 (reference_branch, related_series_branches, related_package_branches)
1184
1185 """
1186 related_series_branch_info = []
1187 related_package_branch_info = []
1188 # Make the base_branch if required and find the product if one exists.
1189 naked_product = None
1190 if reference_branch is None:
1191 naked_product = removeSecurityProxy(self.makeProduct())
1192 # Create the 'source' branch ie the base branch of a recipe.
1193 reference_branch = self.makeProductBranch(
1194 name="reference_branch",
1195 product=naked_product)
1196 elif reference_branch.product is not None:
1197 naked_product = removeSecurityProxy(reference_branch.product)
1198
1199 related_branch_owner = self.makePerson()
1200 # Only branches related to products have related series branches.
1201 if with_series_branches and naked_product is not None:
1202 series_branch_info = []
1203 # Add some product series
1204 def makeSeriesBranch(name, is_private=False):
1205 branch = self.makeBranch(
1206 name=name,
1207 product=naked_product, owner=related_branch_owner,
1208 private=is_private)
1209 series = self.makeProductSeries(
1210 product=naked_product, branch=branch)
1211 return branch, series
1212 for x in range(4):
1213 is_private = x == 0 and with_private_branches
1214 (branch, series) = makeSeriesBranch(
1215 name=("series_branch_%s" % x), is_private=is_private)
1216 if not is_private:
1217 series_branch_info.append((branch, series))
1218
1219 # Sort them
1220 related_series_branch_info = sorted_version_numbers(
1221 series_branch_info, key=lambda branch_info: (
1222 getattr(branch_info[1], 'name')))
1223
1224 # Add a development branch at the start of the list.
1225 naked_product.development_focus.name = 'trunk'
1226 devel_branch = self.makeProductBranch(
1227 product=naked_product, name='trunk_branch',
1228 owner=related_branch_owner)
1229 linked_branch = ICanHasLinkedBranch(naked_product)
1230 linked_branch.setBranch(devel_branch)
1231 related_series_branch_info.insert(0,
1232 (devel_branch, naked_product.development_focus))
1233
1234 if with_package_branches:
1235 # Create related package branches if the base_branch is
1236 # associated with a product.
1237 if naked_product is not None:
1238
1239 def makePackageBranch(name, is_private=False):
1240 distro = self.makeDistribution()
1241 distroseries = self.makeDistroSeries(
1242 distribution=distro)
1243 sourcepackagename = self.makeSourcePackageName()
1244
1245 suitesourcepackage = self.makeSuiteSourcePackage(
1246 sourcepackagename=sourcepackagename,
1247 distroseries=distroseries,
1248 pocket=PackagePublishingPocket.RELEASE)
1249 naked_sourcepackage = removeSecurityProxy(
1250 suitesourcepackage)
1251
1252 branch = self.makePackageBranch(
1253 name=name, owner=related_branch_owner,
1254 sourcepackagename=sourcepackagename,
1255 distroseries=distroseries, private=is_private)
1256 linked_branch = ICanHasLinkedBranch(naked_sourcepackage)
1257 with celebrity_logged_in('admin'):
1258 linked_branch.setBranch(branch, related_branch_owner)
1259
1260 series = self.makeProductSeries(product=naked_product)
1261 self.makePackagingLink(
1262 distroseries=distroseries, productseries=series,
1263 sourcepackagename=sourcepackagename)
1264 return branch, distroseries
1265
1266 for x in range(5):
1267 is_private = x == 0 and with_private_branches
1268 branch, distroseries = makePackageBranch(
1269 name=("product_package_branch_%s" % x),
1270 is_private=is_private)
1271 if not is_private:
1272 related_package_branch_info.append(
1273 (branch, distroseries))
1274
1275 # Create related package branches if the base_branch is
1276 # associated with a sourcepackage.
1277 if reference_branch.sourcepackage is not None:
1278 distroseries = reference_branch.sourcepackage.distroseries
1279 for pocket in [
1280 PackagePublishingPocket.RELEASE,
1281 PackagePublishingPocket.UPDATES,
1282 ]:
1283 branch = self.makePackageBranch(
1284 name="package_branch_%s" % pocket.name,
1285 distroseries=distroseries)
1286 with celebrity_logged_in('admin'):
1287 reference_branch.sourcepackage.setBranch(
1288 pocket, branch,
1289 related_branch_owner)
1290
1291 related_package_branch_info.append(
1292 (branch, distroseries))
1293
1294 related_package_branch_info = sorted_version_numbers(
1295 related_package_branch_info, key=lambda branch_info: (
1296 getattr(branch_info[1], 'name')))
1297
1298
1299 return (
1300 reference_branch,
1301 related_series_branch_info,
1302 related_package_branch_info)
1303
1151 def enableDefaultStackingForProduct(self, product, branch=None):1304 def enableDefaultStackingForProduct(self, product, branch=None):
1152 """Give 'product' a default stacked-on branch.1305 """Give 'product' a default stacked-on branch.
11531306