Merge lp:~bac/launchpad/bug-643538-code into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 11656
Proposed branch: lp:~bac/launchpad/bug-643538-code
Merge into: lp:launchpad
Diff against target: 1761 lines (+770/-224)
22 files modified
lib/canonical/launchpad/icing/style.css (+19/-9)
lib/lp/code/browser/branch.py (+35/-26)
lib/lp/code/browser/branchlisting.py (+42/-13)
lib/lp/code/browser/configure.zcml (+13/-1)
lib/lp/code/browser/tests/test_branch.py (+6/-3)
lib/lp/code/browser/tests/test_product.py (+166/-15)
lib/lp/code/stories/branches/xx-branch-deletion.txt (+29/-15)
lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt (+6/-6)
lib/lp/code/stories/branches/xx-creating-branches.txt (+47/-3)
lib/lp/code/stories/branches/xx-person-branches.txt (+2/-2)
lib/lp/code/stories/branches/xx-private-branch-listings.txt (+20/-10)
lib/lp/code/stories/branches/xx-product-branches.txt (+114/-40)
lib/lp/code/stories/codeimport/xx-create-codeimport.txt (+21/-1)
lib/lp/code/templates/branch-listing.pt (+4/-4)
lib/lp/code/templates/product-branch-summary.pt (+70/-29)
lib/lp/code/templates/product-branches.pt (+55/-29)
lib/lp/code/templates/product-portlet-codestatistics-content.pt (+55/-0)
lib/lp/code/templates/product-portlet-codestatistics.pt (+11/-0)
lib/lp/registry/browser/product.py (+5/-10)
lib/lp/registry/browser/tests/pillar-views.txt (+11/-0)
lib/lp/registry/model/product.py (+2/-1)
lib/lp/registry/tests/test_service_usage.py (+37/-7)
To merge this branch: bzr merge lp:~bac/launchpad/bug-643538-code
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) Approve
Curtis Hovey (community) code Approve
Matthew Revell (community) text Approve
Gavin Panella (community) code Approve
Matthew Revell text Pending
Review via email: mp+36377@code.launchpad.net

Commit message

State the project's code usage, either Launchpad or elsewhere.

Description of the change

= Summary =

Launchpad must state the project's code usage. The product +code-index page needs to clearly state if the project is using Launchpad. If it isn't it should refer to where it is hosted.

== Proposed fix ==

Conditionally present the correct message. Also moved some of the code summary information into portlets in order to be more like the other application areas.

== Pre-implementation notes ==

Chats with Curtis, Edwin, and Jon.

== Implementation details ==

As above.

== Tests ==

Basically all of the code tests need to run:
bin/test -vvm lp.code

== Demo and Q/A ==

Create a new project and visit:
https://code.launchpad.dev/mynewproject

= Launchpad lint =

The lint issues are pretty much intractable. I cleaned up a lot.

Linting changed files:
  lib/lp/code/browser/configure.zcml
  lib/canonical/launchpad/icing/style.css
  lib/lp/code/templates/product-branches.pt
  lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt
  lib/lp/code/browser/branch.py
  lib/lp/code/browser/tests/test_product.py
  lib/lp/registry/browser/product.py
  lib/lp/code/browser/tests/test_branch.py
  lib/lp/code/stories/branches/xx-product-branches.txt
  lib/lp/code/templates/product-portlet-codestatistics-content.pt
  lib/lp/registry/browser/pillar.py
  lib/lp/code/stories/branches/xx-person-branches.txt
  lib/lp/code/templates/product-portlet-codestatistics.pt
  lib/lp/code/templates/product-branch-summary.pt
  lib/lp/registry/browser/tests/pillar-views.txt
  lib/lp/code/stories/branches/xx-private-branch-listings.txt
  lib/lp/registry/model/product.py
  lib/lp/registry/tests/test_service_usage.py
  lib/lp/code/browser/branchlisting.py

./lib/lp/code/stories/branches/xx-private-branch-listings.txt
      83: want exceeds 78 characters.
      93: want exceeds 78 characters.
./lib/lp/code/browser/branchlisting.py
     409: Line exceeds 78 characters.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :
Revision history for this message
Brad Crittenden (bac) wrote :

I've noticed on the screen where no code has been registered[1] there is a link to +set-branch (Configure code hosting) but no link to +add-branch (seen later as "Register a branch"). This seems like an oversight because using the former you cannot mirror a branch, only set in Launchpad and import.

[1] http://people.canonical.com/~bac/code_usage/0-nocode-owner.png

Revision history for this message
Gavin Panella (allenap) wrote :

Cool branch :)

The comments I have are all really minor.

Gavin.

[1]

     def getDistroDevelSeries(self, distribution):
- """Since distribution.currentseries hits the DB every time, cache it."""
+ """distribution.currentseries hits the DB every time so cache it."""
         self._distro_series_map = {}
         try:
             return self._distro_series_map[distribution]
         except KeyError:
             result = distribution.currentseries
             self._distro_series_map[distribution] = result
             return result

I think your change doesn't describe what the code does... but that's
because the code is broken! The following line is the culprit:

         self._distro_series_map = {}

Every time the method is called the cache gets reset. I think this
line can be removed.

[2]

+ self.branch = self.development_focus_branch

development_focus_branch is a cachedproperty, so perhaps branch should
be a property so that development_focus_branch is not computed until
it is needed.

[3]

+ self.assertEqual(None, find_tag_by_id(contents, 'branch-portlet'))
...
+ self.assertNotEqual(None, find_tag_by_id(contents, 'branch-portlet'))

I think this (and others that test against None) should be done with
assertIs(None, ...). Can't remember what difference is makes though :)

[4]

+ def test_portlets_shown_for_EXTERNAL(self):
+ # If the BranchUsage is HOSTED then the portlets are shown.

s/HOSTED/EXTERNAL/

[5]

+ <a tal:attributes="href view/mirror_location"><tal:mirror replace="view/mirror_location"/></a>.

Doesn't really matter, but it might be clearer expressed as:

        <a tal:attributes="href view/mirror_location"
           tal:content="view/mirror_location"/>.

[6]

+ <div tal:condition="not:view/new_branches_are_private" id="privacy-text">
+ <p>
+ New branches you create for <tal:name replace="context/displayname"/>
+ are <strong>public</strong> initially.
+ </p>
+ </div>
+ <div tal:condition="view/new_branches_are_private" id="privacy-text">
+ <p>
+ New branches you create for <tal:name replace="context/displayname"/>
+ are <strong>private</strong> initially.
+ </p>
+ </div>

The tal:condition could go on the subordinate <p> tags, then there's
only one <div>.

[7]

+ <tal:comment condition="nothing">
+ The view/*_count|nothing expressions below are so that this
+ template can be rendered by a view that does not have count
+ information available.
+ </tal:comment>

There aren't any view/*_count|nothing expressions, so this comment can
go.

[8]

+ configured = sum([1 for v in config_statuses.values() if v])

Don't need the list comprehension.

review: Approve (code)
Revision history for this message
Brad Crittenden (bac) wrote :
Download full text (3.3 KiB)

On Sep 23, 2010, at 11:49 , Gavin Panella wrote:

> Review: Approve code
> Cool branch :)
>
> The comments I have are all really minor.

Thanks for the helpful review Gavin.

>
> Gavin.
>
>
> [1]
>
> def getDistroDevelSeries(self, distribution):
> - """Since distribution.currentseries hits the DB every time, cache it."""
> + """distribution.currentseries hits the DB every time so cache it."""
> self._distro_series_map = {}
> try:
> return self._distro_series_map[distribution]
> except KeyError:
> result = distribution.currentseries
> self._distro_series_map[distribution] = result
> return result
>
> I think your change doesn't describe what the code does... but that's
> because the code is broken! The following line is the culprit:
>
> self._distro_series_map = {}
>
> Every time the method is called the cache gets reset. I think this
> line can be removed.

Thanks for looking. My change was just to stop lint from complaining about the comment being too long -- I didn't even see the broken code.

>
>
> [2]
>
> + self.branch = self.development_focus_branch
>
> development_focus_branch is a cachedproperty, so perhaps branch should
> be a property so that development_focus_branch is not computed until
> it is needed.
>

Good catch.

>
> [3]
>
> + self.assertEqual(None, find_tag_by_id(contents, 'branch-portlet'))
> ...
> + self.assertNotEqual(None, find_tag_by_id(contents, 'branch-portlet'))
>
> I think this (and others that test against None) should be done with
> assertIs(None, ...). Can't remember what difference is makes though :)
>

Done

>
> [4]
>
> + def test_portlets_shown_for_EXTERNAL(self):
> + # If the BranchUsage is HOSTED then the portlets are shown.
>
> s/HOSTED/EXTERNAL/
>

Done

>
> [5]
>
> + <a tal:attributes="href view/mirror_location"><tal:mirror replace="view/mirror_location"/></a>.
>
> Doesn't really matter, but it might be clearer expressed as:
>
> <a tal:attributes="href view/mirror_location"
> tal:content="view/mirror_location"/>.
>
>

Yes, much nicer.

> [6]
>
> + <div tal:condition="not:view/new_branches_are_private" id="privacy-text">
> + <p>
> + New branches you create for <tal:name replace="context/displayname"/>
> + are <strong>public</strong> initially.
> + </p>
> + </div>
> + <div tal:condition="view/new_branches_are_private" id="privacy-text">
> + <p>
> + New branches you create for <tal:name replace="context/displayname"/>
> + are <strong>private</strong> initially.
> + </p>
> + </div>
>
> The tal:condition could go on the subordinate <p> tags, then there's
> only one <div>.
>

Done.

>
> [7]
>
> + <tal:comment condition="nothing">
> + The view/*_count|nothing expressions below are so that this
> + template can be rendered by a view that does not have count
> + information available.
> + </tal:comment>
>
> There aren't any view/*_count|nothing expressions, so this comment can
> go.
>
>
c-n-p error

> [8]
>
> ...

Read more...

Revision history for this message
Matthew Revell (matthew.revell) wrote :

Thanks for this work Brad. I have a couple of suggestions:

When no code hosting/mirroring is configured, I think the page would be easier to read if some of the text were split up.

For screen shots 0 and 1, I suggest the following:

  There are no branches of ticktock in Launchpad. You can change this by:

   * activating code hosting directly on Launchpad (read more)
   * asking Launchpad to mirror a Bazaar branch hosted elsewhere (read more)
   * asking Launchpad to import code from Git, Subversion or CVS into a Bazaar branch (read more).

I have removed Mercurial because Tim tells me it is still very much experimental (i.e. works in around 20% of cases).

Other than that, I'm Matthew Revell and I approve this message.

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

This looks good but I'm not sure the portlets will work nice with real data. For instance, if you see https://code.edge.launchpad.net/launchpad, most branches have long names, which cause most of the horizontal space to be used. How will that look with the portlets?

Also, it looks a bit weird having the two different styles of links (the Register a branch one uses a bigger font and the icon is to its left while the others use a smaller font and have the icon to the right) on the portlet. And by the way, are we moving the action links that were once inlined back into portlets?

review: Needs Information (ui*)
Revision history for this message
Brad Crittenden (bac) wrote :

Salgado thanks for the review and the questions.

I tried creating some branches with absurdly long names to see what happened. Using a browser that is almost 1400 pixels wide I get http://people.canonical.com/~bac/code_usage/5-code-still-fits.png. I should probably sees what that looks like on a netbook instead of my 27" monitor. :)

Shrinking the browser from there causes bad behavior seen at http://people.canonical.com/~bac/code_usage/6-too-narrow-overlap.png. I'll work with Curtis to see what can be done.

The other application pages have portlets similar to the one I'm proposing. The link styles are patterned after the involvement portlet on a project index page as seen by a project owner so it is on unprecedented. I think the layout needs some tweaking.

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

The branch urls/names/displayname/what-ever should be using fmt:break-long-words

I think the font size difference in the links is a real problem. They should be the same, particularly since both presentations were added for 3.0. I do not know which is wrong. The problem might be hard to find because one might be using styles from the deprecated style sheet. I cannot judge the scope of this issue. it might be worthy of a separate bug, and it may be in launchpad-web if it is a universal issue.

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Curtis, Matthew and Salgado.

I have made the changes as requested by you and they are shown in screenshots 7-9 at http://people.canonical.com/~bac/code_usage/.

* Bulleted list
* Capitalizing the items in the portlets
* Using break-long-words on the branch names in the table allows them to collapse when the window shrinks
* Don't

The requested changes required a number of test changes, especially the change to not show the portlets when code hosting has not yet been configured. Curtis in addition to mentoring the UI could you look at the latest code diff at http://pastebin.ubuntu.com/502288/ ?

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

The code looks fine. You fixed the sprite and comprehension issues I pointed out in the paste.

I was disappointed by the story additions, the stories did not really tell a story. I was also concerned that the /applets is owned by Foo Bar, so the test for an story about an owner was performed by an admin :(. We looked and did not see an intersection with the unittests in lp/code/browser/tests, so we decided to accept the changes. This branch is large and has a lot of value.

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

On Tue, 2010-09-28 at 19:50 +0000, Brad Crittenden wrote:
> Hi Curtis, Matthew and Salgado.
>
> I have made the changes as requested by you and they are shown in
> screenshots 7-9 at http://people.canonical.com/~bac/code_usage/.
>
> * Bulleted list
> * Capitalizing the items in the portlets
> * Using break-long-words on the branch names in the table allows them
> to collapse when the window shrinks

That certainly improves things, but my main concern was with the
horizontal space that is now taken by the portlets. Given that the
portlets have virtually no content (just action links and 3 lines of
statistics), we're wasting a significant amount (IMHO) of real state,
which will make the branches list a bit too narrow on a 800px wide
browser window, like I tend to use[1]
(<http://people.canonical.com/~salgado/branches.png>).

You've said that other application pages have portlets, and that's true
for the bugs app page, but in that case they have a significant amount
of content (the tag cloud, pre-defined searches and action links), which
is not the case here. Also, the translations app page, on the other
hand, doesn't have any portlets, so I'm still not convinced the code app
page should have them. Unless there's a policy I'm not aware of?

[1] Even though my monitor's resolution is higher than that, I tend to
use less than 800px wide browser windows because most pages work fine on
those and I think Launchpad should as well.

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

On Tue, 2010-09-28 at 21:07 +0000, Guilherme Salgado wrote:
> You've said that other application pages have portlets, and that's
> true
> for the bugs app page, but in that case they have a significant amount
> of content (the tag cloud, pre-defined searches and action links),
> which
> is not the case here. Also, the translations app page, on the other
> hand, doesn't have any portlets, so I'm still not convinced the code
> app
> page should have them. Unless there's a policy I'm not aware of?

The intent of the features is to make the root pages for projects to use
the same layouts and conventions. Code is using a 2.0 layout still.
Translations remains a conundrum. The user should trust that global
actions and set searches are in the right portlets.

This feature work is driven by our own strategists complaint that users
see conflicting designs when they work across applications.

{{{

Hello Curtis, Strategons,

Attached are screenshots of each of the six tabs of Launchpad for a
newly created project.

I want you to look at them. Make a note of the fonts, the styling of
the action portlets, the alignment of cells, the busyness of the
pages, the balance of the layouts, the colours used, the amount of
text on each page.

You will notice that there is significant difference between each
page. If you look at the pages with a detached eye, you may well come
to the conclusion that none of them are very nice to look at.

I think that this is a problem. I don't mean to put down anyone on the
team, but I would be embarrassed to present on these pages at a
conference.

...

}}}

This lead to an extension of the bridging-the-gap theme in June.
Launchpad must state where services are hosted so that they can complete
their task. Launchpad must state when the service is unknown, hosted, or
external. The applications must behave the same way when unknown,
external, or hosted so that users know what to expect when they use
multiple applications.
--
__Curtis C. Hovey_________
http://launchpad.net/

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

Thanks for the explanation, Curtis.

Since it looks like this has been thought through carefully, I think
it's reasonable to trade some screen real estate with better
consistency.

 review approve

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/icing/style.css'
--- lib/canonical/launchpad/icing/style.css 2010-05-28 19:47:23 +0000
+++ lib/canonical/launchpad/icing/style.css 2010-09-28 22:26:02 +0000
@@ -550,6 +550,16 @@
550 color: black !important;550 color: black !important;
551}551}
552552
553.code-links td.code-count {
554 text-align: right;
555 padding-right: 0.5em;
556}
557
558.code-links td.code-link {
559 text-align: left;
560 margin: 0 0 0 0;
561}
562
553563
554/* === Bugs === */564/* === Bugs === */
555/* The Launchpad Bugs application uses a maroon color: */565/* The Launchpad Bugs application uses a maroon color: */
@@ -585,6 +595,15 @@
585 padding-right: 1em;595 padding-right: 1em;
586}596}
587597
598.bug-links td.bugs-count {
599 text-align: right;
600 padding-right: 0.5em;
601}
602
603.bug-links td.bugs-link {
604 text-align: left;
605}
606
588/* --- Blueprints --- */607/* --- Blueprints --- */
589608
590body.tab-specifications #actions, body.tab-specifications .results {609body.tab-specifications #actions, body.tab-specifications .results {
@@ -652,15 +671,6 @@
652 margin: 0.5em;671 margin: 0.5em;
653}672}
654673
655.bug-links td.bugs-count {
656 text-align: right;
657 padding-right: 0.5em;
658}
659
660.bug-links td.bugs-link {
661 text-align: left;
662}
663
664/* ====== Content area styles ====== */674/* ====== Content area styles ====== */
665675
666/* -- Front pages -- */676/* -- Front pages -- */
667677
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2010-08-24 10:45:57 +0000
+++ lib/lp/code/browser/branch.py 2010-09-28 22:26:02 +0000
@@ -16,6 +16,7 @@
16 'BranchReviewerEditView',16 'BranchReviewerEditView',
17 'BranchMergeQueueView',17 'BranchMergeQueueView',
18 'BranchMirrorStatusView',18 'BranchMirrorStatusView',
19 'BranchMirrorMixin',
19 'BranchNameValidationMixin',20 'BranchNameValidationMixin',
20 'BranchNavigation',21 'BranchNavigation',
21 'BranchEditMenu',22 'BranchEditMenu',
@@ -374,7 +375,39 @@
374 return Link('+new-recipe', text, enabled=enabled, icon='add')375 return Link('+new-recipe', text, enabled=enabled, icon='add')
375376
376377
377class BranchView(LaunchpadView, FeedsMixin):378class BranchMirrorMixin:
379 """Provide mirror_location property.
380
381 Requires self.branch to be set by the class using this mixin.
382 """
383
384 @property
385 def mirror_location(self):
386 """Check the mirror location to see if it is a private one."""
387 branch = self.branch
388
389 # If the user has edit permissions, then show the actual location.
390 if check_permission('launchpad.Edit', branch):
391 return branch.url
392
393 # XXX: Tim Penhey, 2008-05-30
394 # Instead of a configuration hack we should support the users
395 # specifying whether or not they want the mirror location
396 # hidden or not. Given that this is a database patch,
397 # it isn't going to happen today.
398 # See bug 235916
399 hosts = config.codehosting.private_mirror_hosts.split(',')
400 private_mirror_hosts = [name.strip() for name in hosts]
401
402 uri = URI(branch.url)
403 for private_host in private_mirror_hosts:
404 if uri.underDomain(private_host):
405 return '<private server>'
406
407 return branch.url
408
409
410class BranchView(LaunchpadView, FeedsMixin, BranchMirrorMixin):
378411
379 feed_types = (412 feed_types = (
380 BranchFeedLink,413 BranchFeedLink,
@@ -387,6 +420,7 @@
387 label = page_title420 label = page_title
388421
389 def initialize(self):422 def initialize(self):
423 self.branch = self.context
390 self.notices = []424 self.notices = []
391 # Replace our context with a decorated branch, if it is not already425 # Replace our context with a decorated branch, if it is not already
392 # decorated.426 # decorated.
@@ -586,31 +620,6 @@
586 return url.startswith("http")620 return url.startswith("http")
587621
588 @property622 @property
589 def mirror_location(self):
590 """Check the mirror location to see if it is a private one."""
591 branch = self.context
592
593 # If the user has edit permissions, then show the actual location.
594 if check_permission('launchpad.Edit', branch):
595 return branch.url
596
597 # XXX: Tim Penhey, 2008-05-30
598 # Instead of a configuration hack we should support the users
599 # specifying whether or not they want the mirror location
600 # hidden or not. Given that this is a database patch,
601 # it isn't going to happen today.
602 # See bug 235916
603 hosts = config.codehosting.private_mirror_hosts.split(',')
604 private_mirror_hosts = [name.strip() for name in hosts]
605
606 uri = URI(branch.url)
607 for private_host in private_mirror_hosts:
608 if uri.underDomain(private_host):
609 return '<private server>'
610
611 return branch.url
612
613 @property
614 def show_merge_links(self):623 def show_merge_links(self):
615 """Return whether or not merge proposal links should be shown.624 """Return whether or not merge proposal links should be shown.
616625
617626
=== modified file 'lib/lp/code/browser/branchlisting.py'
--- lib/lp/code/browser/branchlisting.py 2010-08-31 11:11:09 +0000
+++ lib/lp/code/browser/branchlisting.py 2010-09-28 22:26:02 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Base class view for branch listings."""4"""Base class view for branch listings."""
@@ -83,15 +83,18 @@
83from canonical.launchpad.webapp.breadcrumb import Breadcrumb83from canonical.launchpad.webapp.breadcrumb import Breadcrumb
84from canonical.launchpad.webapp.publisher import LaunchpadView84from canonical.launchpad.webapp.publisher import LaunchpadView
85from canonical.widgets import LaunchpadDropdownWidget85from canonical.widgets import LaunchpadDropdownWidget
86from lp.app.browser.tales import MenuAPI
86from lp.blueprints.interfaces.specificationbranch import (87from lp.blueprints.interfaces.specificationbranch import (
87 ISpecificationBranchSet,88 ISpecificationBranchSet,
88 )89 )
89from lp.bugs.interfaces.bugbranch import IBugBranchSet90from lp.bugs.interfaces.bugbranch import IBugBranchSet
91from lp.code.browser.branch import BranchMirrorMixin
90from lp.code.browser.branchmergeproposallisting import (92from lp.code.browser.branchmergeproposallisting import (
91 ActiveReviewsView,93 ActiveReviewsView,
92 PersonActiveReviewsView,94 PersonActiveReviewsView,
93 PersonProductActiveReviewsView,95 PersonProductActiveReviewsView,
94 )96 )
97from lp.code.browser.summary import BranchCountSummaryView
95from lp.code.enums import (98from lp.code.enums import (
96 BranchLifecycleStatus,99 BranchLifecycleStatus,
97 BranchLifecycleStatusFilter,100 BranchLifecycleStatusFilter,
@@ -124,7 +127,6 @@
124 IPersonProduct,127 IPersonProduct,
125 IPersonProductFactory,128 IPersonProductFactory,
126 )129 )
127from lp.registry.interfaces.pocket import PackagePublishingPocket
128from lp.registry.interfaces.product import IProduct130from lp.registry.interfaces.product import IProduct
129from lp.registry.interfaces.series import SeriesStatus131from lp.registry.interfaces.series import SeriesStatus
130from lp.registry.interfaces.sourcepackage import ISourcePackageFactory132from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
@@ -421,8 +423,7 @@
421 return sorted(links, key=attrgetter('pocket'))423 return sorted(links, key=attrgetter('pocket'))
422424
423 def getDistroDevelSeries(self, distribution):425 def getDistroDevelSeries(self, distribution):
424 """Since distribution.currentseries hits the DB every time, cache it."""426 """distribution.currentseries hits the DB every time so cache it."""
425 self._distro_series_map = {}
426 try:427 try:
427 return self._distro_series_map[distribution]428 return self._distro_series_map[distribution]
428 except KeyError:429 except KeyError:
@@ -777,7 +778,7 @@
777 """A branch listing that has no associated product or person."""778 """A branch listing that has no associated product or person."""
778779
779 field_names = ['lifecycle']780 field_names = ['lifecycle']
780 no_sort_by = (BranchListingSort.DEFAULT,)781 no_sort_by = (BranchListingSort.DEFAULT, )
781782
782 no_branch_message = (783 no_branch_message = (
783 'There are no branches that match the current status filter.')784 'There are no branches that match the current status filter.')
@@ -932,8 +933,8 @@
932 def active_reviews(self):933 def active_reviews(self):
933 text = get_plural_text(934 text = get_plural_text(
934 self.active_review_count,935 self.active_review_count,
935 'active review or unmerged proposal',936 'active review',
936 'active reviews or unmerged proposals')937 'active reviews')
937 return Link('+activereviews', text)938 return Link('+activereviews', text)
938939
939 def addbranch(self):940 def addbranch(self):
@@ -1022,7 +1023,7 @@
10221023
1023 page_title = _('Subscribed')1024 page_title = _('Subscribed')
1024 label_template = 'Bazaar branches subscribed to by %(displayname)s'1025 label_template = 'Bazaar branches subscribed to by %(displayname)s'
1025 no_sort_by = (BranchListingSort.DEFAULT,)1026 no_sort_by = (BranchListingSort.DEFAULT, )
10261027
1027 def _getCollection(self):1028 def _getCollection(self):
1028 return getUtility(IAllBranches).subscribedBy(self.context)1029 return getUtility(IAllBranches).subscribedBy(self.context)
@@ -1122,8 +1123,8 @@
1122 def active_reviews(self):1123 def active_reviews(self):
1123 text = get_plural_text(1124 text = get_plural_text(
1124 self.active_review_count,1125 self.active_review_count,
1125 'active review or unmerged proposal',1126 'Active review',
1126 'active reviews or unmerged proposals')1127 'Active reviews')
1127 return Link('+activereviews', text, site='code')1128 return Link('+activereviews', text, site='code')
11281129
1129 @enabled_with_permission('launchpad.Commercial')1130 @enabled_with_permission('launchpad.Commercial')
@@ -1140,7 +1141,7 @@
1140 """A base class for product branch listings."""1141 """A base class for product branch listings."""
11411142
1142 show_series_links = True1143 show_series_links = True
1143 no_sort_by = (BranchListingSort.PRODUCT,)1144 no_sort_by = (BranchListingSort.PRODUCT, )
1144 label_template = 'Bazaar branches of %(displayname)s'1145 label_template = 'Bazaar branches of %(displayname)s'
11451146
1146 def _getCollection(self):1147 def _getCollection(self):
@@ -1180,8 +1181,23 @@
1180 return message % self.context.displayname1181 return message % self.context.displayname
11811182
11821183
1184class ProductBranchStatisticsView(BranchCountSummaryView,
1185 ProductBranchListingView):
1186 """Portlet containing branch statistics."""
1187
1188 @property
1189 def branch_text(self):
1190 text = super(ProductBranchStatisticsView, self).branch_text
1191 return text.capitalize()
1192
1193 @property
1194 def commit_text(self):
1195 text = super(ProductBranchStatisticsView, self).commit_text
1196 return text.capitalize()
1197
1198
1183class ProductCodeIndexView(ProductBranchListingView, SortSeriesMixin,1199class ProductCodeIndexView(ProductBranchListingView, SortSeriesMixin,
1184 ProductDownloadFileMixin):1200 ProductDownloadFileMixin, BranchMirrorMixin):
1185 """Initial view for products on the code virtual host."""1201 """Initial view for products on the code virtual host."""
11861202
1187 show_set_development_focus = True1203 show_set_development_focus = True
@@ -1193,6 +1209,10 @@
1193 self.revision_cache = revision_cache.inProduct(self.product)1209 self.revision_cache = revision_cache.inProduct(self.product)
11941210
1195 @property1211 @property
1212 def branch(self):
1213 return self.development_focus_branch
1214
1215 @property
1196 def form_action(self):1216 def form_action(self):
1197 return "+branches"1217 return "+branches"
11981218
@@ -1240,6 +1260,7 @@
1240 # skip subsequent series where the lifecycle status is Merged or1260 # skip subsequent series where the lifecycle status is Merged or
1241 # Abandoned1261 # Abandoned
1242 sorted_series = self.sorted_active_series_list1262 sorted_series = self.sorted_active_series_list
1263
1243 def show_branch(branch):1264 def show_branch(branch):
1244 if self.selected_lifecycle_status is None:1265 if self.selected_lifecycle_status is None:
1245 return True1266 return True
@@ -1323,6 +1344,14 @@
1323 def committer_text(self):1344 def committer_text(self):
1324 return get_plural_text(self.committer_count, _('person'), _('people'))1345 return get_plural_text(self.committer_count, _('person'), _('people'))
13251346
1347 @property
1348 def configure_codehosting(self):
1349 """Get the menu link for configuring code hosting."""
1350 series_menu = MenuAPI(self.context.development_focus).overview
1351 set_branch = series_menu['set_branch']
1352 set_branch.text = 'Configure code hosting'
1353 return set_branch
1354
13261355
1327class ProductBranchesView(ProductBranchListingView):1356class ProductBranchesView(ProductBranchListingView):
1328 """View for branch listing for a product."""1357 """View for branch listing for a product."""
@@ -1353,7 +1382,7 @@
1353class ProjectBranchesView(BranchListingView):1382class ProjectBranchesView(BranchListingView):
1354 """View for branch listing for a project."""1383 """View for branch listing for a project."""
13551384
1356 no_sort_by = (BranchListingSort.DEFAULT,)1385 no_sort_by = (BranchListingSort.DEFAULT, )
1357 extra_columns = ('author', 'product')1386 extra_columns = ('author', 'product')
1358 label_template = 'Bazaar branches of %(displayname)s'1387 label_template = 'Bazaar branches of %(displayname)s'
1359 show_series_links = True1388 show_series_links = True
13601389
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2010-09-22 18:37:57 +0000
+++ lib/lp/code/browser/configure.zcml 2010-09-28 22:26:02 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -961,6 +961,18 @@
961 permission="zope.Public"961 permission="zope.Public"
962 name="+code-index"962 name="+code-index"
963 template="../templates/product-branches.pt"/>963 template="../templates/product-branches.pt"/>
964 <browser:page
965 for="lp.registry.interfaces.product.IProduct"
966 class="lp.code.browser.branchlisting.ProductBranchStatisticsView"
967 permission="zope.Public"
968 name="+portlet-product-codestatistics"
969 template="../templates/product-portlet-codestatistics.pt"/>
970 <browser:page
971 for="lp.registry.interfaces.product.IProduct"
972 class="lp.code.browser.branchlisting.ProductBranchStatisticsView"
973 permission="zope.Public"
974 name="+portlet-product-codestatistics-content"
975 template="../templates/product-portlet-codestatistics-content.pt"/>
964 <browser:page976 <browser:page
965 for="lp.registry.interfaces.product.IProduct"977 for="lp.registry.interfaces.product.IProduct"
966 layer="lp.code.publisher.CodeLayer"978 layer="lp.code.publisher.CodeLayer"
967979
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/browser/tests/test_branch.py 2010-09-28 22:26:02 +0000
@@ -45,7 +45,6 @@
45 )45 )
46from lp.code.interfaces.branchtarget import IBranchTarget46from lp.code.interfaces.branchtarget import IBranchTarget
47from lp.testing import (47from lp.testing import (
48 ANONYMOUS,
49 login,48 login,
50 login_person,49 login_person,
51 logout,50 logout,
@@ -78,6 +77,7 @@
78 branch_type=BranchType.MIRRORED,77 branch_type=BranchType.MIRRORED,
79 url="http://example.com/good/mirror")78 url="http://example.com/good/mirror")
80 view = BranchView(branch, LaunchpadTestRequest())79 view = BranchView(branch, LaunchpadTestRequest())
80 view.initialize()
81 self.assertTrue(view.user is None)81 self.assertTrue(view.user is None)
82 self.assertEqual(82 self.assertEqual(
83 "http://example.com/good/mirror", view.mirror_location)83 "http://example.com/good/mirror", view.mirror_location)
@@ -89,6 +89,7 @@
89 branch_type=BranchType.MIRRORED,89 branch_type=BranchType.MIRRORED,
90 url="http://private.example.com/bzr-mysql/mysql-5.0")90 url="http://private.example.com/bzr-mysql/mysql-5.0")
91 view = BranchView(branch, LaunchpadTestRequest())91 view = BranchView(branch, LaunchpadTestRequest())
92 view.initialize()
92 self.assertTrue(view.user is None)93 self.assertTrue(view.user is None)
93 self.assertEqual(94 self.assertEqual(
94 "<private server>", view.mirror_location)95 "<private server>", view.mirror_location)
@@ -106,6 +107,7 @@
106 logout()107 logout()
107 login('eric@example.com')108 login('eric@example.com')
108 view = BranchView(branch, LaunchpadTestRequest())109 view = BranchView(branch, LaunchpadTestRequest())
110 view.initialize()
109 self.assertEqual(view.user, owner)111 self.assertEqual(view.user, owner)
110 self.assertEqual(112 self.assertEqual(
111 "http://private.example.com/bzr-mysql/mysql-5.0",113 "http://private.example.com/bzr-mysql/mysql-5.0",
@@ -126,6 +128,7 @@
126 logout()128 logout()
127 login('other@example.com')129 login('other@example.com')
128 view = BranchView(branch, LaunchpadTestRequest())130 view = BranchView(branch, LaunchpadTestRequest())
131 view.initialize()
129 self.assertEqual(view.user, other)132 self.assertEqual(view.user, other)
130 self.assertEqual(133 self.assertEqual(
131 "<private server>", view.mirror_location)134 "<private server>", view.mirror_location)
@@ -160,7 +163,7 @@
160 len(branch.mirror_status_message)163 len(branch.mirror_status_message)
161 <= branch_view.MAXIMUM_STATUS_MESSAGE_LENGTH,164 <= branch_view.MAXIMUM_STATUS_MESSAGE_LENGTH,
162 "branch.mirror_status_message longer than expected: %r"165 "branch.mirror_status_message longer than expected: %r"
163 % (branch.mirror_status_message,))166 % (branch.mirror_status_message, ))
164 self.assertEqual(167 self.assertEqual(
165 branch.mirror_status_message, branch_view.mirror_status_message)168 branch.mirror_status_message, branch_view.mirror_status_message)
166 self.assertEqual(169 self.assertEqual(
@@ -185,7 +188,7 @@
185 'whiteboard': '',188 'whiteboard': '',
186 'owner': arbitrary_person,189 'owner': arbitrary_person,
187 'author': arbitrary_person,190 'author': arbitrary_person,
188 'product': arbitrary_product191 'product': arbitrary_product,
189 }192 }
190 add_view.add_action.success(data)193 add_view.add_action.success(data)
191 # Make sure that next_mirror_time is a datetime, not an sqlbuilder194 # Make sure that next_mirror_time is a datetime, not an sqlbuilder
192195
=== modified file 'lib/lp/code/browser/tests/test_product.py'
--- lib/lp/code/browser/tests/test_product.py 2010-08-24 02:16:53 +0000
+++ lib/lp/code/browser/tests/test_product.py 2010-09-28 22:26:02 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the product view classes and templates."""4"""Tests for the product view classes and templates."""
@@ -10,20 +10,25 @@
10 timedelta,10 timedelta,
11 )11 )
12import unittest12import unittest
13
14from mechanize import LinkNotFoundError13from mechanize import LinkNotFoundError
15import pytz14import pytz
16from zope.component import (15from zope.component import getUtility
17 getMultiAdapter,16
18 getUtility,17from canonical.launchpad.testing.pages import (
18 extract_text,
19 find_tag_by_id,
19 )20 )
20
21from canonical.launchpad.webapp import canonical_url21from canonical.launchpad.webapp import canonical_url
22from canonical.launchpad.webapp.servers import LaunchpadTestRequest
23from canonical.testing import DatabaseFunctionalLayer22from canonical.testing import DatabaseFunctionalLayer
23from lp.app.enums import ServiceUsage
24from lp.code.enums import (
25 BranchType,
26 BranchVisibilityRule,
27 )
24from lp.code.interfaces.revision import IRevisionSet28from lp.code.interfaces.revision import IRevisionSet
25from lp.testing import (29from lp.testing import (
26 ANONYMOUS,30 ANONYMOUS,
31 BrowserTestCase,
27 login,32 login,
28 login_person,33 login_person,
29 TestCaseWithFactory,34 TestCaseWithFactory,
@@ -32,9 +37,8 @@
32from lp.testing.views import create_initialized_view37from lp.testing.views import create_initialized_view
3338
3439
35class TestProductCodeIndexView(TestCaseWithFactory):40class ProductTestBase(TestCaseWithFactory):
36 """Tests for the product code home page."""41 """Common methods for tests herein."""
37
38 layer = DatabaseFunctionalLayer42 layer = DatabaseFunctionalLayer
3943
40 def makeProductAndDevelopmentFocusBranch(self, **branch_args):44 def makeProductAndDevelopmentFocusBranch(self, **branch_args):
@@ -49,6 +53,10 @@
49 product.development_focus.branch = branch53 product.development_focus.branch = branch
50 return product, branch54 return product, branch
5155
56
57class TestProductCodeIndexView(ProductTestBase):
58 """Tests for the product code home page."""
59
52 def getBranchSummaryBrowseLinkForProduct(self, product):60 def getBranchSummaryBrowseLinkForProduct(self, product):
53 """Get the 'browse code' link from the product's code home.61 """Get the 'browse code' link from the product's code home.
5462
@@ -98,13 +106,15 @@
98106
99 def test_initial_branches_contains_dev_focus_branch(self):107 def test_initial_branches_contains_dev_focus_branch(self):
100 product, branch = self.makeProductAndDevelopmentFocusBranch()108 product, branch = self.makeProductAndDevelopmentFocusBranch()
101 view = create_initialized_view(product, '+code-index', rootsite='code')109 view = create_initialized_view(product, '+code-index',
110 rootsite='code')
102 self.assertIn(branch, view.initial_branches)111 self.assertIn(branch, view.initial_branches)
103112
104 def test_initial_branches_does_not_contain_private_dev_focus_branch(self):113 def test_initial_branches_does_not_contain_private_dev_focus_branch(self):
105 product, branch = self.makeProductAndDevelopmentFocusBranch(114 product, branch = self.makeProductAndDevelopmentFocusBranch(
106 private=True)115 private=True)
107 view = create_initialized_view(product, '+code-index', rootsite='code')116 view = create_initialized_view(product, '+code-index',
117 rootsite='code')
108 self.assertNotIn(branch, view.initial_branches)118 self.assertNotIn(branch, view.initial_branches)
109119
110 def test_committer_count_with_revision_authors(self):120 def test_committer_count_with_revision_authors(self):
@@ -120,7 +130,8 @@
120 date_generator=date_generator)130 date_generator=date_generator)
121 getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)131 getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)
122132
123 view = create_initialized_view(product, '+code-index', rootsite='code')133 view = create_initialized_view(product, '+code-index',
134 rootsite='code')
124 self.assertEqual(view.committer_count, 1)135 self.assertEqual(view.committer_count, 1)
125136
126 def test_committers_count_private_branch(self):137 def test_committers_count_private_branch(self):
@@ -138,10 +149,150 @@
138 date_generator=date_generator)149 date_generator=date_generator)
139 getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)150 getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)
140151
141 view = create_initialized_view(product, '+code-index', rootsite='code')152 view = create_initialized_view(product, '+code-index',
153 rootsite='code')
142 self.assertEqual(view.committer_count, 1)154 self.assertEqual(view.committer_count, 1)
143155
144156
157class TestProductCodeIndexServiceUsages(ProductTestBase, BrowserTestCase):
158 """Tests for the product code page, especially the usage messasges."""
159
160 def test_external_mirrored(self):
161 # Test that the correct URL is displayed for a mirrored branch.
162 product, branch = self.makeProductAndDevelopmentFocusBranch(
163 branch_type=BranchType.MIRRORED,
164 url="http://example.com/mybranch")
165 self.assertEqual(ServiceUsage.EXTERNAL, product.codehosting_usage)
166 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
167 login(ANONYMOUS)
168 content = find_tag_by_id(browser.contents, 'external')
169 text = extract_text(content)
170 expected = ("%(product_title)s hosts its code at %(branch_url)s. "
171 "Launchpad has a mirror of the master branch "
172 "and you can create branches from it." % dict(
173 product_title=product.title,
174 branch_url=branch.url))
175 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
176
177 def test_external_remote(self):
178 # Test that a remote branch is shown properly.
179 product, branch = self.makeProductAndDevelopmentFocusBranch(
180 branch_type=BranchType.REMOTE,
181 url="http://example.com/mybranch")
182 self.assertEqual(ServiceUsage.EXTERNAL,
183 product.codehosting_usage)
184 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
185 login(ANONYMOUS)
186 content = find_tag_by_id(browser.contents, 'external')
187 text = extract_text(content)
188 expected = ("%(product_title)s hosts its code at %(branch_url)s. "
189 "Launchpad does not have a copy of the remote "
190 "branch." % dict(
191 product_title=product.title,
192 branch_url=branch.url))
193 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
194
195 def test_unknown(self):
196 product = self.factory.makeProduct()
197 self.assertEqual(ServiceUsage.UNKNOWN, product.codehosting_usage)
198 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
199 login(ANONYMOUS)
200 content = find_tag_by_id(browser.contents, 'unknown')
201 text = extract_text(content)
202 expected = (
203 "Launchpad does not know where %(product_title)s "
204 "hosts its code.*" %
205 dict(product_title=product.title))
206 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
207
208 def test_on_launchpad(self):
209 product, branch = self.makeProductAndDevelopmentFocusBranch()
210 self.assertEqual(ServiceUsage.LAUNCHPAD, product.codehosting_usage)
211 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
212 login(ANONYMOUS)
213 text = extract_text(find_tag_by_id(
214 browser.contents, 'branch-count-summary'))
215 expected = "1 active branch owned by 1 person.*"
216 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
217
218 def test_view_mirror_location(self):
219 url = "http://example.com/mybranch"
220 product, branch = self.makeProductAndDevelopmentFocusBranch(
221 branch_type=BranchType.MIRRORED,
222 url=url)
223 view = create_initialized_view(product, '+code-index',
224 rootsite='code')
225 self.assertEqual(url, view.mirror_location)
226
227
228class TestProductBranchesViewPortlets(ProductTestBase, BrowserTestCase):
229 """Tests for the portlets."""
230
231 def test_portlet_not_shown_for_UNKNOWN(self):
232 # If the BranchUsage is UNKNOWN then the portlets are not shown.
233 product = self.factory.makeProduct()
234 self.assertEqual(ServiceUsage.UNKNOWN, product.codehosting_usage)
235 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
236 contents = browser.contents
237 self.assertIs(None, find_tag_by_id(contents, 'branch-portlet'))
238 self.assertIs(None, find_tag_by_id(contents, 'privacy'))
239 self.assertIs(None, find_tag_by_id(contents, 'involvement'))
240 self.assertIs(None, find_tag_by_id(
241 contents, 'portlet-product-codestatistics'))
242
243 def test_portlets_shown_for_HOSTED(self):
244 # If the BranchUsage is HOSTED then the portlets are shown.
245 product, branch = self.makeProductAndDevelopmentFocusBranch()
246 self.assertEqual(ServiceUsage.LAUNCHPAD, product.codehosting_usage)
247 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
248 contents = browser.contents
249 self.assertIsNot(None, find_tag_by_id(contents, 'branch-portlet'))
250 self.assertIsNot(None, find_tag_by_id(contents, 'privacy'))
251 self.assertIsNot(None, find_tag_by_id(contents, 'involvement'))
252 self.assertIsNot(None, find_tag_by_id(
253 contents, 'portlet-product-codestatistics'))
254
255 def test_portlets_shown_for_EXTERNAL(self):
256 # If the BranchUsage is EXTERNAL then the portlets are shown.
257 url = "http://example.com/mybranch"
258 product, branch = self.makeProductAndDevelopmentFocusBranch(
259 branch_type=BranchType.MIRRORED,
260 url=url)
261 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
262 contents = browser.contents
263 self.assertIsNot(None, find_tag_by_id(contents, 'branch-portlet'))
264 self.assertIsNot(None, find_tag_by_id(contents, 'privacy'))
265 self.assertIsNot(None, find_tag_by_id(contents, 'involvement'))
266 self.assertIsNot(None, find_tag_by_id(
267 contents, 'portlet-product-codestatistics'))
268
269 def test_is_private(self):
270 team_owner = self.factory.makePerson()
271 team = self.factory.makeTeam(team_owner)
272 product = self.factory.makeProduct(owner=team_owner)
273 branch = self.factory.makeProductBranch(product=product)
274 login_person(product.owner)
275 product.development_focus.branch = branch
276 product.setBranchVisibilityTeamPolicy(
277 team, BranchVisibilityRule.PRIVATE)
278 view = create_initialized_view(
279 product, '+code-index', rootsite='code', principal=product.owner)
280 text = extract_text(find_tag_by_id(view.render(), 'privacy'))
281 expected = ("New branches you create for %(name)s are private "
282 "initially.*" % dict(name=product.displayname))
283 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
284
285 def test_is_public(self):
286 product = self.factory.makeProduct()
287 branch = self.factory.makeProductBranch(product=product)
288 login_person(product.owner)
289 product.development_focus.branch = branch
290 browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
291 text = extract_text(find_tag_by_id(browser.contents, 'privacy'))
292 expected = ("New branches you create for %(name)s are public "
293 "initially.*" % dict(name=product.displayname))
294 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
295
296
145def test_suite():297def test_suite():
146 return unittest.TestLoader().loadTestsFromName(__name__)298 return unittest.TestLoader().loadTestsFromName(__name__)
147
148299
=== modified file 'lib/lp/code/stories/branches/xx-branch-deletion.txt'
--- lib/lp/code/stories/branches/xx-branch-deletion.txt 2010-05-13 16:22:19 +0000
+++ lib/lp/code/stories/branches/xx-branch-deletion.txt 2010-09-28 22:26:02 +0000
@@ -4,20 +4,34 @@
4can be deleted. The main use for this is to allow users to delete4can be deleted. The main use for this is to allow users to delete
5branches that have been created in error.5branches that have been created in error.
66
7 >>> browser = setupBrowser(auth="Basic test@canonical.com:test")7 >>> from lp.code.enums import BranchType
8 >>> browser.open('http://code.launchpad.dev/firefox')8 >>> login(ANONYMOUS)
9 >>> alice = factory.makePerson(name="alice", password="test",
10 ... email="alice@example.com")
11 >>> product = factory.makeProduct(
12 ... name='earthlynx', displayname="Earth Lynx", owner=alice)
13 >>> branch = factory.makeProductBranch(
14 ... product=product, branch_type=BranchType.HOSTED)
15 >>> productseries = factory.makeProductSeries(
16 ... product=product, branch=branch)
17 >>> login_person(alice)
18 >>> product.development_focus = productseries
19 >>> logout()
20
21 >>> browser = setupBrowser(auth="Basic alice@example.com:test")
22 >>> browser.open('http://code.launchpad.dev/earthlynx')
9 >>> browser.getLink("Register a branch").click()23 >>> browser.getLink("Register a branch").click()
10 >>> browser.getControl('Branch URL').value = 'http://foo.bar.com/oops'24 >>> browser.getControl('Branch URL').value = 'http://foo.bar.com/oops'
11 >>> browser.getControl('Name').value = 'to-delete'25 >>> browser.getControl('Name').value = 'to-delete'
12 >>> browser.getControl('Register Branch').click()26 >>> browser.getControl('Register Branch').click()
13 >>> print browser.title27 >>> print browser.title
14 to-delete : Code : Mozilla Firefox28 to-delete : Code : Earth Lynx
1529
16The newly created branch has an action 'Delete branch'.30The newly created branch has an action 'Delete branch'.
1731
18 >>> delete_link = browser.getLink('Delete branch')32 >>> delete_link = browser.getLink('Delete branch')
19 >>> print delete_link.url33 >>> print delete_link.url
20 http://code.launchpad.dev/~name12/firefox/to-delete/+delete34 http://code.launchpad.dev/~alice/earthlynx/to-delete/+delete
2135
22When the user clicks on the link, they are informed what will happen if they36When the user clicks on the link, they are informed what will happen if they
23delete the branch.37delete the branch.
@@ -25,7 +39,7 @@
25 >>> delete_link.click()39 >>> delete_link.click()
26 >>> print extract_text(find_main_content(browser.contents))40 >>> print extract_text(find_main_content(browser.contents))
27 Delete branch41 Delete branch
28 Mozilla Firefox...42 Earth Lynx...
29 Branch deletion is permanent.43 Branch deletion is permanent.
30 or Cancel44 or Cancel
3145
@@ -35,15 +49,15 @@
3549
36 >>> browser.getControl('Delete').click()50 >>> browser.getControl('Delete').click()
37 >>> print browser.url51 >>> print browser.url
38 http://code.launchpad.dev/firefox52 http://code.launchpad.dev/earthlynx
39 >>> for message in get_feedback_messages(browser.contents):53 >>> for message in get_feedback_messages(browser.contents):
40 ... print message54 ... print message
41 Branch ~name12/firefox/to-delete deleted...55 Branch ~alice/earthlynx/to-delete deleted...
4256
43If the branch is junk, then the user is taken back to the code listing for57If the branch is junk, then the user is taken back to the code listing for
44the deleted branch's owner.58the deleted branch's owner.
4559
46 >>> browser.open('http://code.launchpad.dev/~name12')60 >>> browser.open('http://code.launchpad.dev/~alice')
47 >>> browser.getLink("Register a branch").click()61 >>> browser.getLink("Register a branch").click()
48 >>> browser.getControl('Hosted').click()62 >>> browser.getControl('Hosted').click()
49 >>> browser.getControl('Name').value = 'to-delete'63 >>> browser.getControl('Name').value = 'to-delete'
@@ -51,14 +65,15 @@
51 >>> browser.getLink('Delete branch').click()65 >>> browser.getLink('Delete branch').click()
52 >>> browser.getControl('Delete').click()66 >>> browser.getControl('Delete').click()
53 >>> print browser.url67 >>> print browser.url
54 http://code.launchpad.dev/~name1268 http://code.launchpad.dev/~alice
55 >>> for message in get_feedback_messages(browser.contents):69 >>> for message in get_feedback_messages(browser.contents):
56 ... print message70 ... print message
57 Branch ~name12/+junk/to-delete deleted...71 Branch ~alice/+junk/to-delete deleted...
5872
59Branches that are stacked upon cannot be deleted.73Branches that are stacked upon cannot be deleted.
6074
61 >>> login('admin@canonical.com')75 >>> from lp.testing.sampledata import ADMIN_EMAIL
76 >>> login(ADMIN_EMAIL)
62 >>> stacked_upon = factory.makeAnyBranch()77 >>> stacked_upon = factory.makeAnyBranch()
63 >>> stacked = factory.makeAnyBranch(stacked_on=stacked_upon)78 >>> stacked = factory.makeAnyBranch(stacked_on=stacked_upon)
64 >>> branch_location = canonical_url(stacked_upon)79 >>> branch_location = canonical_url(stacked_upon)
@@ -82,12 +97,11 @@
82 >>> from lp.registry.interfaces.pocket import PackagePublishingPocket97 >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
83 >>> from lp.registry.interfaces.person import IPersonSet98 >>> from lp.registry.interfaces.person import IPersonSet
84 >>> login(ANONYMOUS)99 >>> login(ANONYMOUS)
85 >>> name12 = getUtility(IPersonSet).getByName('name12')
86 >>> ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches100 >>> ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
87 >>> ignored = removeSecurityProxy(ubuntu_branches).addMember(101 >>> ignored = removeSecurityProxy(ubuntu_branches).addMember(
88 ... name12, ubuntu_branches.teamowner)102 ... alice, ubuntu_branches.teamowner)
89 >>> login_person(name12)103 >>> login_person(alice)
90 >>> branch = factory.makePackageBranch(owner=name12)104 >>> branch = factory.makePackageBranch(owner=alice)
91 >>> package = branch.sourcepackage105 >>> package = branch.sourcepackage
92 >>> package.setBranch(106 >>> package.setBranch(
93 ... PackagePublishingPocket.RELEASE, branch, branch.registrant)107 ... PackagePublishingPocket.RELEASE, branch, branch.registrant)
94108
=== modified file 'lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt'
--- lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt 2010-01-14 23:39:06 +0000
+++ lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt 2010-09-28 22:26:02 +0000
@@ -63,11 +63,11 @@
6363
64 >>> browser.open('http://code.launchpad.dev/fooix')64 >>> browser.open('http://code.launchpad.dev/fooix')
65 >>> print_tag_with_id(browser.contents, 'merge-counts')65 >>> print_tag_with_id(browser.contents, 'merge-counts')
66 3 active reviews or unmerged proposals66 3 Active reviews
6767
68The 'active reviews or unmerged proposals' text links to the active reviews page.68The 'active reviews' text links to the active reviews page.
6969
70 >>> browser.getLink('active reviews or unmerged proposals').click()70 >>> browser.getLink('Active reviews').click()
71 >>> print browser.title71 >>> print browser.title
72 Active code reviews for Fooix...72 Active code reviews for Fooix...
7373
@@ -94,7 +94,7 @@
94 >>> browser.open('http://code.launchpad.dev/~albert')94 >>> browser.open('http://code.launchpad.dev/~albert')
95 >>> print_tag_with_id(browser.contents, 'page-summary')95 >>> print_tag_with_id(browser.contents, 'page-summary')
96 1 owned branch, 1 registered branch, 1 subscribed branch96 1 owned branch, 1 registered branch, 1 subscribed branch
97 1 active review or unmerged proposal97 1 active review
9898
99The person's active reviews also lists all of their currently requested99The person's active reviews also lists all of their currently requested
100reviews.100reviews.
101101
=== modified file 'lib/lp/code/stories/branches/xx-creating-branches.txt'
--- lib/lp/code/stories/branches/xx-creating-branches.txt 2010-09-27 19:39:21 +0000
+++ lib/lp/code/stories/branches/xx-creating-branches.txt 2010-09-28 22:26:02 +0000
@@ -15,6 +15,19 @@
1515
16Hosted branches use Launchpad as their primary location.16Hosted branches use Launchpad as their primary location.
1717
18 >>> from lp.registry.interfaces.product import IProductSet
19 >>> from lp.code.enums import BranchType
20 >>> from zope.component import getUtility
21 >>> login(ANONYMOUS)
22 >>> redfish = getUtility(IProductSet).getByName('redfish')
23 >>> branch = factory.makeProductBranch(
24 ... product=redfish, branch_type=BranchType.HOSTED)
25 >>> productseries = factory.makeProductSeries(
26 ... product=redfish, branch=branch)
27 >>> login_person(redfish.owner)
28 >>> redfish.development_focus = productseries
29 >>> logout()
30
18 >>> browser = setupBrowser(auth="Basic test@canonical.com:test")31 >>> browser = setupBrowser(auth="Basic test@canonical.com:test")
19 >>> browser.open('http://code.launchpad.dev/redfish')32 >>> browser.open('http://code.launchpad.dev/redfish')
20 >>> browser.getLink("Register a branch").click()33 >>> browser.getLink("Register a branch").click()
@@ -122,6 +135,29 @@
122Let's make sure we can load the branch creation form on a product.135Let's make sure we can load the branch creation form on a product.
123136
124 >>> user_browser.getLink("Register a branch").click()137 >>> user_browser.getLink("Register a branch").click()
138 Traceback (most recent call last):
139 ...
140 LinkNotFoundError
141
142The link is not there because the product has not been configured to
143do code hosting yet. The development focus must be set with a branch first.
144
145 >>> owner_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test")
146 >>> owner_browser.open('http://code.launchpad.dev/applets')
147 >>> owner_browser.getLink('Configure code hosting').click()
148 >>> print owner_browser.url
149 http://code.launchpad.dev/applets/trunk/+setbranch
150
151 >>> owner_browser.getControl(
152 ... 'Create a new, empty branch').click()
153 >>> owner_browser.getControl('Branch name').value = 'trunk'
154 >>> owner_browser.getControl('Update').click()
155
156Now that code hosting has been configured, a regular user will be able
157to register a branch.
158
159 >>> user_browser.open('http://code.launchpad.dev/applets')
160 >>> user_browser.getLink("Register a branch").click()
125 >>> print user_browser.url161 >>> print user_browser.url
126 http://code.launchpad.dev/applets/+addbranch162 http://code.launchpad.dev/applets/+addbranch
127163
@@ -313,7 +349,7 @@
313URL validation should check that the entered URL is not the root of a349URL validation should check that the entered URL is not the root of a
314site.350site.
315351
316 >>> user_browser.open('http://code.launchpad.dev/firefox')352 >>> user_browser.open('http://code.launchpad.dev/applets')
317 >>> user_browser.getLink("Register a branch").click()353 >>> user_browser.getLink("Register a branch").click()
318 >>> user_browser.getControl('Branch URL').value = 'http://example.com'354 >>> user_browser.getControl('Branch URL').value = 'http://example.com'
319 >>> user_browser.getControl('Name').value = 'unique-name'355 >>> user_browser.getControl('Name').value = 'unique-name'
@@ -326,7 +362,7 @@
326362
327URL validation should check that the entered URL is not from Launchpad.363URL validation should check that the entered URL is not from Launchpad.
328364
329 >>> user_browser.open('http://code.launchpad.dev/firefox')365 >>> user_browser.open('http://code.launchpad.dev/applets')
330 >>> user_browser.getLink("Register a branch").click()366 >>> user_browser.getLink("Register a branch").click()
331 >>> user_browser.getControl('Branch URL').value = (367 >>> user_browser.getControl('Branch URL').value = (
332 ... 'http://code.launchpad.dev/~testuser/')368 ... 'http://code.launchpad.dev/~testuser/')
@@ -341,7 +377,7 @@
341As well as checking against the root site set in the config, a check is377As well as checking against the root site set in the config, a check is
342also done against the value stored as a database constraint.378also done against the value stored as a database constraint.
343379
344 >>> user_browser.open('http://code.launchpad.dev/firefox')380 >>> user_browser.open('http://code.launchpad.dev/applets')
345 >>> user_browser.getLink("Register a branch").click()381 >>> user_browser.getLink("Register a branch").click()
346 >>> user_browser.getControl('Branch URL').value = (382 >>> user_browser.getControl('Branch URL').value = (
347 ... 'http://bazaar.launchpad.net/foo/bar/')383 ... 'http://bazaar.launchpad.net/foo/bar/')
@@ -377,6 +413,14 @@
377When registering a branch from the product pages, there is no product413When registering a branch from the product pages, there is no product
378widget, so errors are set at the page level.414widget, so errors are set at the page level.
379415
416 >>> owner_browser = setupBrowser(auth="Basic test@canonical.com:test")
417 >>> owner_browser.open('http://code.launchpad.dev/landscape')
418 >>> owner_browser.getLink('Configure code hosting').click()
419 >>> owner_browser.getControl(
420 ... 'Create a new, empty branch').click()
421 >>> owner_browser.getControl('Branch name').value = 'trunk'
422 >>> owner_browser.getControl('Update').click()
423
380 >>> user_browser.open('http://code.launchpad.dev/landscape')424 >>> user_browser.open('http://code.launchpad.dev/landscape')
381 >>> user_browser.getLink("Register a branch").click()425 >>> user_browser.getLink("Register a branch").click()
382 >>> user_browser.getControl('Branch URL').value = 'http://foo.com/bar'426 >>> user_browser.getControl('Branch URL').value = 'http://foo.com/bar'
383427
=== modified file 'lib/lp/code/stories/branches/xx-person-branches.txt'
--- lib/lp/code/stories/branches/xx-person-branches.txt 2010-05-27 02:19:27 +0000
+++ lib/lp/code/stories/branches/xx-person-branches.txt 2010-09-28 22:26:02 +0000
@@ -101,7 +101,7 @@
101 >>> eric_browser.open('http://code.launchpad.dev/~eric')101 >>> eric_browser.open('http://code.launchpad.dev/~eric')
102 >>> print_tag_with_id(eric_browser.contents, 'page-summary')102 >>> print_tag_with_id(eric_browser.contents, 'page-summary')
103 1 owned branch, 1 registered branch, 1 subscribed branch103 1 owned branch, 1 registered branch, 1 subscribed branch
104 0 active reviews or unmerged proposals104 0 active reviews
105105
106Now we'll create another branch, and unsubscribe the owner from it.106Now we'll create another branch, and unsubscribe the owner from it.
107107
@@ -113,4 +113,4 @@
113 >>> eric_browser.open('http://code.launchpad.dev/~eric')113 >>> eric_browser.open('http://code.launchpad.dev/~eric')
114 >>> print_tag_with_id(eric_browser.contents, 'page-summary')114 >>> print_tag_with_id(eric_browser.contents, 'page-summary')
115 2 owned branches, 2 registered branches, 1 subscribed branch115 2 owned branches, 2 registered branches, 1 subscribed branch
116 0 active reviews or unmerged proposals116 0 active reviews
117117
=== modified file 'lib/lp/code/stories/branches/xx-private-branch-listings.txt'
--- lib/lp/code/stories/branches/xx-private-branch-listings.txt 2010-09-03 00:25:07 +0000
+++ lib/lp/code/stories/branches/xx-private-branch-listings.txt 2010-09-28 22:26:02 +0000
@@ -1,4 +1,5 @@
1= Private Branch Listings =1Private Branch Listings
2=======================
23
3All pages that show branch listings to users should only show branches4All pages that show branch listings to users should only show branches
4that the user is allowed to see.5that the user is allowed to see.
@@ -15,7 +16,8 @@
15 ... reset_all_branch_last_modified)16 ... reset_all_branch_last_modified)
16 >>> reset_all_branch_last_modified()17 >>> reset_all_branch_last_modified()
1718
18== Additional sample data ==19Additional sample data
20----------------------
1921
20Adding a private branch that is only visible by No Privileges Person22Adding a private branch that is only visible by No Privileges Person
21(and Launchpad administrators).23(and Launchpad administrators).
@@ -37,7 +39,8 @@
37 >>> logout()39 >>> logout()
3840
3941
40== The code home page ==42The code home page
43------------------
4144
42The code home page shows lists of recently imported, changed, and45The code home page shows lists of recently imported, changed, and
43registered branches.46registered branches.
@@ -60,7 +63,8 @@
60Logged in users should only be able to see public branches, and private63Logged in users should only be able to see public branches, and private
61branches that they are subscribed to or are the owner of.64branches that they are subscribed to or are the owner of.
6265
63 >>> no_priv_browser = setupBrowser(auth='Basic no-priv@canonical.com:test')66 >>> no_priv_browser = setupBrowser(
67 ... auth='Basic no-priv@canonical.com:test')
64 >>> print_recently_registered_branches(no_priv_browser)68 >>> print_recently_registered_branches(no_priv_browser)
65 '...~no-priv/landscape/testing-branch...<span...class="sprite private"...'69 '...~no-priv/landscape/testing-branch...<span...class="sprite private"...'
66 '...~mark/+junk/testdoc...'70 '...~mark/+junk/testdoc...'
@@ -91,7 +95,8 @@
91 '...~name12/gnome-terminal/scanned...'95 '...~name12/gnome-terminal/scanned...'
9296
9397
94== Landscape code listing page ==98Landscape code listing page
99---------------------------
95100
96One of the most obvious places to hide private branches are the code101One of the most obvious places to hide private branches are the code
97listing tab.102listing tab.
@@ -103,12 +108,13 @@
103 ... # So print the text shown in the application summary.108 ... # So print the text shown in the application summary.
104 ... if table is None:109 ... if table is None:
105 ... print extract_text(find_tag_by_id(110 ... print extract_text(find_tag_by_id(
106 ... browser.contents, 'application-summary'))111 ... browser.contents, 'branch-summary'))
107 ... else:112 ... else:
108 ... for row in table.tbody.fetch('tr'):113 ... for row in table.tbody.fetch('tr'):
109 ... print extract_text(row)114 ... print extract_text(row)
110115
111 >>> print_landscape_code_listing(anon_browser)116 >>> print_landscape_code_listing(anon_browser)
117 Launchpad does not know where The Landscape Project hosts its code...
112 There are no branches for The Landscape Project in Launchpad...118 There are no branches for The Landscape Project in Launchpad...
113119
114 >>> print_landscape_code_listing(no_priv_browser)120 >>> print_landscape_code_listing(no_priv_browser)
@@ -124,7 +130,8 @@
124 lp://dev/~no-priv/landscape/testing-branch Development ...130 lp://dev/~no-priv/landscape/testing-branch Development ...
125131
126132
127== Person code listing pages ==133Person code listing pages
134-------------------------
128135
129The person code listings is the other obvious place to filter out the136The person code listings is the other obvious place to filter out the
130viewable branches.137viewable branches.
@@ -173,7 +180,8 @@
173 >>> print_person_code_listing(landscape_dev_browser, '/+ownedbranches')180 >>> print_person_code_listing(landscape_dev_browser, '/+ownedbranches')
174 Total of 10 branches listed181 Total of 10 branches listed
175 lp://dev/~name12/landscape/feature-x Development ...182 lp://dev/~name12/landscape/feature-x Development ...
176 >>> print_person_code_listing(landscape_dev_browser, '/+registeredbranches')183 >>> print_person_code_listing(landscape_dev_browser,
184 ... '/+registeredbranches')
177 Total of 11 branches listed185 Total of 11 branches listed
178 lp://dev/~landscape-developers/landscape/trunk Development ...186 lp://dev/~landscape-developers/landscape/trunk Development ...
179 lp://dev/~name12/landscape/feature-x Development ...187 lp://dev/~name12/landscape/feature-x Development ...
@@ -190,7 +198,8 @@
190 lp://dev/~name12/landscape/feature-x Development ...198 lp://dev/~name12/landscape/feature-x Development ...
191199
192200
193== Bug branch links ==201Bug branch links
202----------------
194203
195When a private branch is linked to a bug, the bug branch link is only204When a private branch is linked to a bug, the bug branch link is only
196visible to those that would be able to see the branch.205visible to those that would be able to see the branch.
@@ -227,7 +236,8 @@
227 No bug branch links236 No bug branch links
228237
229238
230== Branches set as primary branches for product series ==239Branches set as primary branches for product series
240---------------------------------------------------
231241
232When a branch is set as the user branch for product series, the details242When a branch is set as the user branch for product series, the details
233must be visible to those that are entitled to see it, but hidden from243must be visible to those that are entitled to see it, but hidden from
234244
=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
--- lib/lp/code/stories/branches/xx-product-branches.txt 2010-08-19 14:22:01 +0000
+++ lib/lp/code/stories/branches/xx-product-branches.txt 2010-09-28 22:26:02 +0000
@@ -34,25 +34,34 @@
34If there are not any branches, a helpful message is shown.34If there are not any branches, a helpful message is shown.
3535
36 >>> def get_summary(browser):36 >>> def get_summary(browser):
37 ... return find_tag_by_id(browser.contents, 'application-summary')37 ... return find_tag_by_id(browser.contents, 'branch-summary')
38 >>> summary = get_summary(browser)38 >>> summary = get_summary(browser)
39 >>> print extract_text(summary)39 >>> print extract_text(summary)
40 There are no branches for Gnome Applets in Launchpad.40 Launchpad does not know where The Gnome Panel Applets hosts its code.
41 There are no branches for Gnome Applets in Launchpad. You can change this by:
42 activating code hosting directly on Launchpad. (read more)
43 asking Launchpad to mirror a Bazaar branch hosted elsewhere. (read more)
44 asking Launchpad to import code from Git, Subversion, or CVS into a
45 Bazaar branch. (read more)
46 Getting started with code hosting in Launchpad.
47
48
41 If there are Bazaar branches of Gnome Applets in a publicly49 If there are Bazaar branches of Gnome Applets in a publicly
42 accessible location, Launchpad can act as a mirror of the branch50 accessible location, Launchpad can act as a mirror of the branch
43 by registering a Mirrored branch. Read more.51 by registering a Mirrored branch. Read more.
44 Launchpad can also act as a primary location for Bazaar branches of52 Launchpad can also act as a primary location for Bazaar branches of
45 Gnome Applets. Read more.53 Gnome Applets. Read more.
46 Launchpad can import code from CVS, Subversion, Mercurial or Git54 Launchpad can import code from CVS, Subversion, Mercurial or Git
47 into Bazaar branches. Read more.55 into Bazaar branches. Read more...
4856
49The 'Read more' links go to the help wiki.57The 'Read more' links go to the help wiki.
5058
51 >>> for anchor in summary.fetch('a'):59 >>> for anchor in summary.fetch('a'):
52 ... print anchor['href']60 ... print anchor['href']
61 https://help.launchpad.net/Code/UploadingABranch
53 https://help.launchpad.net/Code/MirroredBranches62 https://help.launchpad.net/Code/MirroredBranches
54 https://help.launchpad.net/Code/UploadingABranch
55 https://help.launchpad.net/VcsImports63 https://help.launchpad.net/VcsImports
64 https://help.launchpad.net/Code
5665
5766
58Link to the product downloads67Link to the product downloads
@@ -63,6 +72,7 @@
6372
64 >>> browser.open('http://code.launchpad.dev/netapplet')73 >>> browser.open('http://code.launchpad.dev/netapplet')
65 >>> print extract_text(get_summary(browser))74 >>> print extract_text(get_summary(browser))
75 Launchpad does not know where Network Applet hosts its code...
66 There are no branches for NetApplet in Launchpad.76 There are no branches for NetApplet in Launchpad.
67 ...77 ...
68 There are download files available for NetApplet.78 There are download files available for NetApplet.
@@ -83,8 +93,6 @@
83 >>> browser.open('http://code.launchpad.dev/evolution')93 >>> browser.open('http://code.launchpad.dev/evolution')
84 >>> summary = get_summary(browser)94 >>> summary = get_summary(browser)
85 >>> print extract_text(get_summary(browser))95 >>> print extract_text(get_summary(browser))
86 3 active branches ...
87 0 active reviews or unmerged proposals
88 You can get a copy of the development focus branch using the96 You can get a copy of the development focus branch using the
89 command:97 command:
90 bzr branch lp://dev/evolution98 bzr branch lp://dev/evolution
@@ -124,7 +132,8 @@
124Firstly lets associate release--0.9.1 with the 1.0 series.132Firstly lets associate release--0.9.1 with the 1.0 series.
125133
126 >>> admin_browser.open('http://launchpad.dev/firefox/1.0/+linkbranch')134 >>> admin_browser.open('http://launchpad.dev/firefox/1.0/+linkbranch')
127 >>> admin_browser.getControl('Branch').value = '~mark/firefox/release--0.9.1'135 >>> admin_browser.getControl('Branch').value = (
136 ... '~mark/firefox/release--0.9.1')
128 >>> admin_browser.getControl('Update').click()137 >>> admin_browser.getControl('Update').click()
129138
130 >>> browser.open('http://code.launchpad.dev/firefox')139 >>> browser.open('http://code.launchpad.dev/firefox')
@@ -155,32 +164,78 @@
155 lp://dev/~mark/firefox/release-0.8 Development ...164 lp://dev/~mark/firefox/release-0.8 Development ...
156165
157166
158Floating buttons167Involvement portlet
159================168===================
160169
161There are two buttons that show on the right hand side of the screen170There are several links in the side portlet: 'Register a branch',
162for project branch listings. 'Register a branch' and 'Import a branch'.171'Import a branch', 'Configure code hosting', and 'Define branch
172visibility'. The links are only shown if the user has permission to
173perform the task.
163174
164 >>> from zope.component import getUtility175 >>> from zope.component import getUtility
165 >>> from lp.registry.interfaces.product import IProductSet176 >>> from lp.registry.interfaces.product import IProductSet
166 >>> login('admin@canonical.com')177 >>> login(ANONYMOUS)
167 >>> product = getUtility(IProductSet).getByName('firefox')178 >>> product = getUtility(IProductSet).getByName('firefox')
168 >>> old_branch = product.development_focus.branch179 >>> old_branch = product.development_focus.branch
180 >>> login_person(product.owner)
169 >>> product.development_focus.branch = None181 >>> product.development_focus.branch = None
170 >>> logout()182 >>> logout()
171 >>> def print_links(browser):183 >>> def print_links(browser):
172 ... links = find_tag_by_id(browser.contents, 'floating-links')184 ... links = find_tag_by_id(browser.contents, 'involvement')
185 ... if links is None:
186 ... print 'None'
187 ... return
173 ... for link in links.findAll('a'):188 ... for link in links.findAll('a'):
174 ... print extract_text(link)189 ... print extract_text(link)
175 >>> browser.open('http://code.launchpad.dev/firefox')190
176 >>> print_links(browser)191 >>> def setup_code_hosting(productname):
192 ... admin_browser.open('http://code.launchpad.dev/%s' % productname)
193 ... admin_browser.getLink('Configure code hosting').click()
194 ... admin_browser.getControl(
195 ... 'Create a new, empty branch').click()
196 ... admin_browser.getControl('Branch name').value = 'trunk'
197 ... admin_browser.getControl('Update').click()
198
199The involvement portlet is not shown if the product does not have code
200hosting configured or if it is not using Launchpad.
201
202 >>> print product.codehosting_usage.name
203 UNKNOWN
204 >>> admin_browser.open('http://code.launchpad.dev/firefox')
205 >>> print_links(admin_browser)
206 None
207
208 >>> setup_code_hosting('firefox')
209 >>> print product.codehosting_usage.name
210 LAUNCHPAD
211 >>> admin_browser.open('http://code.launchpad.dev/firefox')
212 >>> print_links(admin_browser)
213 Register a branch
214 Import a branch
215 Configure code hosting
216 Define branch visibility
217
218The owner of the project sees the links for the activities he can
219perform, everything except defining branch visibility.
220
221 >>> owner_browser = setupBrowser(auth='Basic test@canonical.com:test')
222 >>> owner_browser.open('http://code.launchpad.dev/firefox')
223 >>> print_links(owner_browser)
224 Register a branch
225 Import a branch
226 Configure code hosting
227
228And a regular user can only register and import branches.
229
230 >>> user_browser.open('http://code.launchpad.dev/firefox')
231 >>> print_links(user_browser)
177 Register a branch232 Register a branch
178 Import a branch233 Import a branch
179234
180If the product specifies that it officially uses Launchpad code, then235If the product specifies that it officially uses Launchpad code, then
181the 'Import a branch' button is still shown.236the 'Import a branch' button is still shown.
182237
183 >>> login('admin@canonical.com')238 >>> login_person(product.owner)
184 >>> product.development_focus.branch = old_branch239 >>> product.development_focus.branch = old_branch
185 >>> logout()240 >>> logout()
186 >>> browser.open('http://code.launchpad.dev/firefox')241 >>> browser.open('http://code.launchpad.dev/firefox')
@@ -189,34 +244,54 @@
189 Import a branch244 Import a branch
190245
191246
192Nice wording of summary numbers247The statistics portlet
193===============================248======================
194249
195The text that is shown giving a summary of the number of branches250The text that is shown giving a summary of the number of branches
196shows correct singular and plural forms.251shows correct singular and plural forms.
197252
198 >>> def print_summary(product):253 >>> def get_stats_portlet(browser):
254 ... return find_tag_by_id(
255 ... browser.contents,
256 ... 'portlet-product-codestatistics')
257 >>> def print_portlet(product):
199 ... browser.open('http://code.launchpad.dev/%s' % product)258 ... browser.open('http://code.launchpad.dev/%s' % product)
200 ... print extract_text(get_summary(browser))259 ... portlet = get_stats_portlet(browser)
201260 ... if portlet is None:
202 >>> print_summary('gnome-terminal')261 ... print 'None'
203 8 active branches owned by 1 person and 2 teams, 0 commits in the last month262 ... else:
204 ...263 ... print extract_text(portlet)
264
265 >>> setup_code_hosting('gnome-terminal')
266 >>> print_portlet('gnome-terminal')
267 0 Active reviews
268 9 Active branches owned by 2 people and 2 teams
269 0 Commits in the last month
270
205 >>> from lp.testing import ANONYMOUS, login, logout271 >>> from lp.testing import ANONYMOUS, login, logout
206 >>> login(ANONYMOUS)272 >>> login(ANONYMOUS)
207 >>> fooix = factory.makeProduct('fooix')273 >>> fooix = factory.makeProduct('fooix')
208 >>> ignored = factory.makeProductBranch(fooix)274 >>> ignored = factory.makeProductBranch(fooix)
209 >>> ignored = factory.makeProductBranch(fooix)275 >>> logout()
210 >>> logout()276 >>> setup_code_hosting('fooix')
211 >>> print_summary('fooix')277 >>> print_portlet('fooix')
212 2 active branches owned by 2 people, 0 commits in the last month278 0 Active reviews
213 ...279 2 Active branches owned by 2 people
214 >>> print_summary('evolution')280 0 Commits in the last month
215 3 active branches owned by 1 person and 1 team, 0 commits in the last month281
216 ...282 >>> print_portlet('evolution')
217 >>> print_summary('iso-codes')283 0 Active reviews
218 1 active branch owned by 1 person, 0 commits in the last month284 3 Active branches owned by 1 person and 1 team
219 ...285 0 Commits in the last month
286
287 >>> login(ANONYMOUS)
288 >>> dinky = factory.makeProduct('dinky')
289 >>> logout()
290 >>> setup_code_hosting('dinky')
291 >>> print_portlet('dinky')
292 0 Active reviews
293 1 Active branch owned by 1 person
294 0 Commits in the last month
220295
221296
222Product has Branches, but none initially visible297Product has Branches, but none initially visible
@@ -224,17 +299,16 @@
224299
225It is a bit of an edge case, but if there are branches for a product but all300It is a bit of an edge case, but if there are branches for a product but all
226of them are either merged or abandoned and there is no development focus301of them are either merged or abandoned and there is no development focus
227branch, then they will not appear on the initial branch listing.302branch, then they will not appear on the initial branch listing and
303the portlets will not be shown.
228304
229 >>> admin_browser.open('http://code.launchpad.dev/~carlos/iso-codes/0.35')305 >>> admin_browser.open('http://code.launchpad.dev/~carlos/iso-codes/0.35')
230 >>> admin_browser.getLink('Change branch details').click()306 >>> admin_browser.getLink('Change branch details').click()
231 >>> admin_browser.getControl('Abandoned').click()307 >>> admin_browser.getControl('Abandoned').click()
232 >>> admin_browser.getControl('Change Branch').click()308 >>> admin_browser.getControl('Change Branch').click()
233309
234 >>> browser.open('http://code.launchpad.dev/iso-codes')310 >>> print_portlet('iso-codes')
235 >>> print extract_text(get_summary(browser))311 None
236 0 active branches, 0 commits in the last month
237 0 active reviews or unmerged proposals
238312
239 >>> message = find_tag_by_id(browser.contents, 'no-branch-message')313 >>> message = find_tag_by_id(browser.contents, 'no-branch-message')
240 >>> print extract_text(message)314 >>> print extract_text(message)
241315
=== modified file 'lib/lp/code/stories/codeimport/xx-create-codeimport.txt'
--- lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2010-08-14 16:39:07 +0000
+++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2010-09-28 22:26:02 +0000
@@ -28,7 +28,27 @@
2828
29 >>> browser.open('http://code.launchpad.dev/firefox')29 >>> browser.open('http://code.launchpad.dev/firefox')
30 >>> browser.getLink('Import a branch').click()30 >>> browser.getLink('Import a branch').click()
3131 Traceback (most recent call last):
32 ...
33 LinkNotFoundError
34
35The owner can configure code hosting for the project and then
36importing will be available to any user.
37
38 >>> owner_browser = setupBrowser(auth="Basic test@canonical.com:test")
39 >>> owner_browser.open('http://code.launchpad.dev/firefox')
40 >>> owner_browser.getLink('Configure code hosting').click()
41 >>> owner_browser.getControl(
42 ... 'Import a branch').click()
43 >>> owner_browser.getControl('Branch URL').value = 'git://example.com/firefox'
44 >>> owner_browser.getControl('Git').click()
45 >>> owner_browser.getControl('Branch name').value = 'trunk'
46 >>> owner_browser.getControl('Update').click()
47
48Now a regular user can import another branch.
49
50 >>> browser.open('http://code.launchpad.dev/firefox')
51 >>> browser.getLink('Import a branch').click()
3252
33Requesting a Subversion import53Requesting a Subversion import
34==============================54==============================
3555
=== modified file 'lib/lp/code/templates/branch-listing.pt'
--- lib/lp/code/templates/branch-listing.pt 2010-04-11 22:45:09 +0000
+++ lib/lp/code/templates/branch-listing.pt 2010-09-28 22:26:02 +0000
@@ -74,16 +74,16 @@
74 </tal:ignore>74 </tal:ignore>
75 <tr tal:condition="product/required:launchpad.Edit"75 <tr tal:condition="product/required:launchpad.Edit"
76 tal:define="edit_link product/development_focus/fmt:url/+linkbranchtoseries">76 tal:define="edit_link product/development_focus/fmt:url/+linkbranchtoseries">
77 <td colspan="5" class="branch-no-dev-focus">A development focus branch hasn't 77 <td colspan="5" class="branch-no-dev-focus">A development focus branch hasn't
78 been specified, <a tal:attributes="href edit_link">set it now</a>.</td>78 been specified, <a tal:attributes="href edit_link">set it now</a>.</td>
79 </tr>79 </tr>
80 </tal:missing-dev-focus>80 </tal:missing-dev-focus>
81 </tal:allow-setting-dev-focus>81 </tal:allow-setting-dev-focus>
82 <tr tal:repeat="branch context/branches">82 <tr tal:repeat="branch context/branches">
83 <td>83 <td>
84 <img src="/@@/branch" /> <a tal:attributes="href branch/fmt:url"84 <a tal:attributes="href branch/fmt:url"
85 tal:content="branch/bzr_identity">85 tal:content="structure branch/bzr_identity/fmt:break-long-words"
86 Name86 class="sprite branch">Name
87 </a>87 </a>
88 <tal:associated-series repeat="series branch/active_series"88 <tal:associated-series repeat="series branch/active_series"
89 condition="context/view/show_series_links">89 condition="context/view/show_series_links">
9090
=== modified file 'lib/lp/code/templates/product-branch-summary.pt'
--- lib/lp/code/templates/product-branch-summary.pt 2010-08-13 16:09:45 +0000
+++ lib/lp/code/templates/product-branch-summary.pt 2010-09-28 22:26:02 +0000
@@ -7,42 +7,59 @@
7 lang="en"7 lang="en"
8 dir="ltr"8 dir="ltr"
9 i18n:domain="launchpad"9 i18n:domain="launchpad"
10 id="application-summary">10 id="branch-summary">
11
12 <div id="unknown" tal:condition="context/codehosting_usage/enumvalue:UNKNOWN">
13 <p>
14 <strong>
15 Launchpad does not know where <tal:project_title replace="context/title" />
16 hosts its code.
17 </strong>
18 </p>
19 </div>
20
21 <div id="external"
22 tal:condition="context/codehosting_usage/enumvalue:EXTERNAL">
23 <p>
24 <strong>
25 <tal:project_title replace="context/title" /> hosts its code at
26 <a tal:attributes="href view/mirror_location"
27 tal:content="view/mirror_location"></a>.
28 </strong>
29 </p>
30 <p tal:condition="context/homepageurl">
31 You can learn more at the project's
32 <a tal:attributes="href context/homepageurl">web page</a>.
33 </p>
34 <p tal:condition="view/branch/branch_type/enumvalue:MIRRORED">
35 Launchpad has a mirror of the master branch and you can create branches
36 from it.
37 </p>
38 <p tal:condition="view/branch/branch_type/enumvalue:REMOTE">
39 Launchpad does not have a copy of the remote branch.
40 </p>
41 </div>
1142
12 <tal:no-branches condition="not: view/branch_count">43 <tal:no-branches condition="not: view/branch_count">
13 There are no branches for <tal:project-name replace="context/displayname"/>44 There are no branches for <tal:project-name replace="context/displayname"/>
14 in Launchpad.45 in Launchpad. You can change this by:
15 <ul>46
16 <li>If there are Bazaar branches of47 <ul class="bulleted" style="margin-top:1em;">
17 <tal:project-name replace="context/displayname"/>48
18 in a publicly accessible location,49 <li>activating code hosting directly on
19 Launchpad can act as a mirror of the branch by registering a50 Launchpad. (<a href="https://help.launchpad.net/Code/UploadingABranch">read
20 <em>Mirrored</em> branch.51 more</a>)</li>
21 <a href="https://help.launchpad.net/Code/MirroredBranches">Read more.</a>52
22 </li>53 <li>asking Launchpad to mirror a Bazaar branch hosted
23 <li>Launchpad can also act as a primary54 elsewhere. (<a href="https://help.launchpad.net/Code/MirroredBranches">read
24 location for Bazaar branches of55 more</a>)</li>
25 <tal:project-name replace="context/displayname"/>.56
26 <a href="https://help.launchpad.net/Code/UploadingABranch">Read more.</a>57 <li>asking Launchpad to import code from Git, Subversion, or CVS into a
27 </li>58 Bazaar branch. (<a href="https://help.launchpad.net/VcsImports">read more</a>)</li>
28
29 <li>Launchpad can import code from CVS, Subversion, Mercurial or Git
30 into Bazaar branches. <a
31 href="https://help.launchpad.net/VcsImports">Read more.</a> </li>
32
33 </ul>59 </ul>
34 </tal:no-branches>60 </tal:no-branches>
3561
36 <tal:has-branches condition="view/branch_count">62 <tal:has-branches condition="view/branch_count">
37 <p tal:replace="structure context/@@+count-summary"/>
38 <p id="merge-counts"
39 tal:define="menu context/menu:branches">
40 <strong class="count" tal:content="menu/active_review_count">5</strong>
41 <tal:link
42 define="link menu/active_reviews"
43 replace="structure link/render"
44 />
45 </p>
46 <div tal:condition="view/has_development_focus_branch"63 <div tal:condition="view/has_development_focus_branch"
47 style="margin: 1em 0"64 style="margin: 1em 0"
48 tal:define="config modules/canonical.config/config;65 tal:define="config modules/canonical.config/config;
@@ -60,6 +77,30 @@
60 </div>77 </div>
6178
62 </tal:has-branches>79 </tal:has-branches>
80
81 <div tal:condition="context/codehosting_usage/enumvalue:UNKNOWN">
82 <div
83 tal:condition="not: context/codehosting_usage/enumvalue:LAUNCHPAD"
84 tal:define="configure_codehosting view/configure_codehosting |
85 nothing">
86 <p style="margin-top: 10px;">
87 <a class="sprite maybe"
88 href="https://help.launchpad.net/Code">Getting started
89 with code hosting in Launchpad</a>.</p>
90
91 <p tal:condition="context/required:launchpad.Edit"
92 id="no-code-edit"
93 >
94 <a tal:condition="configure_codehosting"
95 tal:replace="structure configure_codehosting/fmt:link"/>
96 </p>
97 <p tal:define="menu context/menu:branches;
98 link menu/branch_visibility"
99 tal:condition="link/enabled"
100 tal:content="structure link/render"></p>
101 </div>
102 </div>
103
63 <p tal:condition="view/latest_release_with_download_files">104 <p tal:condition="view/latest_release_with_download_files">
64 <img src="/@@/download"/> There are105 <img src="/@@/download"/> There are
65 <a tal:define="rooturl modules/canonical.launchpad.webapp.vhosts/allvhosts/configs/mainsite/rooturl"106 <a tal:define="rooturl modules/canonical.launchpad.webapp.vhosts/allvhosts/configs/mainsite/rooturl"
66107
=== modified file 'lib/lp/code/templates/product-branches.pt'
--- lib/lp/code/templates/product-branches.pt 2009-09-17 00:27:40 +0000
+++ lib/lp/code/templates/product-branches.pt 2010-09-28 22:26:02 +0000
@@ -3,40 +3,66 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"6 metal:use-macro="view/macro:page/main_side"
7 i18n:domain="launchpad"7 i18n:domain="launchpad"
8>8>
99
10<body>10<body>
1111
12<div metal:fill-slot="main">12 <metal:side fill-slot="side" tal:define="context_menu context/menu:context">
1313 <div id="branch-portlet"
14 <div style="float:right" id="floating-links"14 tal:condition="not: context/codehosting_usage/enumvalue:UNKNOWN">
15 tal:define="menu context/menu:branches">15 <div id="privacy"
16 <div tal:define="link menu/branch_add"16 tal:define="are_private view/new_branches_are_private"
17 tal:condition="link/enabled"17 tal:attributes="class python: are_private and 'first portlet private' or 'first portlet public'">
18 tal:content="structure link/render" />18
19 <div tal:define="link menu/code_import"19 <p tal:condition="not:view/new_branches_are_private" id="privacy-text">
20 tal:condition="link/enabled"20 New branches you create for <tal:name replace="context/displayname"/>
21 tal:content="structure link/render" />21 are <strong>public</strong> initially.
22 <div tal:define="link menu/branch_visibility"22 </p>
23 tal:condition="link/enabled"23
24 tal:content="structure link/render" />24 <p tal:condition="view/new_branches_are_private" id="privacy-text">
25 </div>25 New branches you create for <tal:name replace="context/displayname"/>
2626 are <strong>private</strong> initially.
27 <div id="private-policy" tal:condition="view/new_branches_are_private"27 </p>
28 class="informational message">28
29 New branches you create for <tal:name replace="context/displayname"/>29 </div>
30 are <strong>private</strong> initially.30
31 </div>31 <div id="involvement" class="portlet"
3232 tal:define="menu context/menu:branches">
33 <tal:branch-summary content="structure context/@@+branch-summary" />33 <ul class="involvement">
3434 <li style="border: none">
35 <tal:has-branches condition="view/branch_count"35 <a href="+addbranch" class="menu-link-addbranch sprite code">
36 define="branches view/branches">36 Register a branch
37 <tal:branchlisting content="structure branches/@@+branch-listing" />37 </a>
38 </tal:has-branches>38 </li>
39</div>39 </ul>
40 <p style="margin-top:10px;"
41 tal:define="link menu/code_import"
42 tal:condition="link/enabled"
43 tal:content="structure link/render"></p>
44 <p tal:define="configure_codehosting view/configure_codehosting | nothing"
45 tal:condition="configure_codehosting"
46 tal:replace="structure configure_codehosting/fmt:link"></p>
47 <p tal:define="link menu/branch_visibility"
48 tal:condition="link/enabled"
49 tal:content="structure link/render"></p>
50 </div>
51
52 <div tal:replace="structure context/@@+portlet-product-codestatistics" />
53 </div>
54 </metal:side>
55
56 <tal:main metal:fill-slot="main">
57
58 <tal:branch-summary content="structure context/@@+branch-summary" />
59
60 <tal:has-branches condition="view/branch_count"
61 define="branches view/branches">
62 <tal:branchlisting content="structure branches/@@+branch-listing" />
63 </tal:has-branches>
64
65 </tal:main>
4066
41</body>67</body>
42</html>68</html>
4369
=== added file 'lib/lp/code/templates/product-portlet-codestatistics-content.pt'
--- lib/lp/code/templates/product-portlet-codestatistics-content.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/product-portlet-codestatistics-content.pt 2010-09-28 22:26:02 +0000
@@ -0,0 +1,55 @@
1<tal:portlet-product-codestatistics-content
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal">
4
5 <tr tal:define="menu context/menu:branches" class="code-links"
6 id="merge-counts">
7 <td class="code-count"
8 tal:define="count menu/active_review_count"
9 tal:content="count" />
10 <td>
11 <tal:link
12 define="link menu/active_reviews"
13 replace="structure link/render"
14 />
15 </td>
16 </tr>
17
18 <tr class="code-links" id="branch-count-summary">
19 <td class="code-count"
20 tal:define="count view/branch_count"
21 tal:content="count" />
22 <td>
23 <tal:branches replace="view/branch_text">branches</tal:branches
24 ><tal:has-branches condition="view/branch_count">
25 owned by
26 <tal:individuals condition="view/person_owner_count">
27 <tal:owners content="view/person_owner_count">42</tal:owners>
28 <tal:people replace="view/person_text">people</tal:people
29 ></tal:individuals
30 ><tal:teams condition="view/team_owner_count">
31 <tal:individuals condition="view/person_owner_count">
32 and
33 </tal:individuals>
34 <tal:toc content="view/team_owner_count">1</tal:toc>
35 <tal:people replace="view/team_text">team</tal:people
36 ></tal:teams></tal:has-branches>
37 </td>
38 </tr>
39
40 <tr class="code-links">
41 <td class="code-count"
42 tal:define="count view/commit_count"
43 tal:content="count" />
44 <td>
45 <tal:commits replace="view/commit_text">commits</tal:commits>
46 <tal:has-committers condition="view/committer_count">
47 by
48 <tal:cc content="view/committer_count">4</tal:cc>
49 <tal:people replace="view/committer_text">people</tal:people>
50 </tal:has-committers>
51 in the last month
52 </td>
53 </tr>
54
55</tal:portlet-product-codestatistics-content>
056
=== added file 'lib/lp/code/templates/product-portlet-codestatistics.pt'
--- lib/lp/code/templates/product-portlet-codestatistics.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/code/templates/product-portlet-codestatistics.pt 2010-09-28 22:26:02 +0000
@@ -0,0 +1,11 @@
1<div
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 class="portlet" id="portlet-product-codestatistics">
6
7 <table class="code-links">
8 <tbody id="portlet-product-codestatistics"
9 tal:content="structure context/@@+portlet-product-codestatistics-content" />
10 </table>
11</div>
012
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2010-09-28 14:58:40 +0000
+++ lib/lp/registry/browser/product.py 2010-09-28 22:26:02 +0000
@@ -458,9 +458,7 @@
458 # Add the branch configuration in separately.458 # Add the branch configuration in separately.
459 set_branch = series_menu['set_branch']459 set_branch = series_menu['set_branch']
460 set_branch.text = 'Configure project branch'460 set_branch.text = 'Configure project branch'
461 set_branch.summary = "Specify the location of this projects code."461 set_branch.summary = "Specify the location of this project's code."
462 set_branch.configured = (
463 )
464 config_list.append(462 config_list.append(
465 dict(link=set_branch,463 dict(link=set_branch,
466 configured=config_statuses['configure_codehosting']))464 configured=config_statuses['configure_codehosting']))
@@ -469,11 +467,8 @@
469 @property467 @property
470 def registration_completeness(self):468 def registration_completeness(self):
471 """The percent complete for registration."""469 """The percent complete for registration."""
472 configured = 0
473 config_statuses = self.configuration_states470 config_statuses = self.configuration_states
474 for key, value in config_statuses.items():471 configured = sum(1 for val in config_statuses.values() if val)
475 if value:
476 configured += 1
477 scale = 100472 scale = 100
478 done = int(float(configured) / len(config_statuses) * scale)473 done = int(float(configured) / len(config_statuses) * scale)
479 undone = scale - done474 undone = scale - done
@@ -1396,9 +1391,9 @@
1396 def setUpFields(self):1391 def setUpFields(self):
1397 super(ProductConfigureBase, self).setUpFields()1392 super(ProductConfigureBase, self).setUpFields()
1398 if self.usage_fieldname is not None:1393 if self.usage_fieldname is not None:
1399 # The usage fields are shared among pillars. But when referring to1394 # The usage fields are shared among pillars. But when referring
1400 # an individual object in Launchpad it is better to call it by its1395 # to an individual object in Launchpad it is better to call it by
1401 # real name, i.e. 'project' instead of 'pillar'.1396 # its real name, i.e. 'project' instead of 'pillar'.
1402 usage_field = self.form_fields.get(self.usage_fieldname)1397 usage_field = self.form_fields.get(self.usage_fieldname)
1403 if usage_field:1398 if usage_field:
1404 usage_field.custom_widget = CustomWidgetFactory(1399 usage_field.custom_widget = CustomWidgetFactory(
14051400
=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
--- lib/lp/registry/browser/tests/pillar-views.txt 2010-09-25 14:29:32 +0000
+++ lib/lp/registry/browser/tests/pillar-views.txt 2010-09-28 22:26:02 +0000
@@ -182,6 +182,17 @@
182 >>> print view.codehosting_usage.name182 >>> print view.codehosting_usage.name
183 LAUNCHPAD183 LAUNCHPAD
184184
185 >>> from lp.code.enums import BranchType
186 >>> remote = factory.makeProduct()
187 >>> branch = factory.makeProductBranch(product=remote,
188 ... branch_type=BranchType.REMOTE)
189 >>> remote.official_codehosting
190 False
191 >>> view = create_view(remote, '+get-involved')
192 >>> print view.codehosting_usage.name
193 UNKNOWN
194
195
185Project groups cannot make links to register a branch, so196Project groups cannot make links to register a branch, so
186official_codehosting is always false.197official_codehosting is always false.
187198
188199
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2010-09-27 18:16:28 +0000
+++ lib/lp/registry/model/product.py 2010-09-28 22:26:02 +0000
@@ -404,7 +404,8 @@
404 return ServiceUsage.UNKNOWN404 return ServiceUsage.UNKNOWN
405 elif self.development_focus.branch.branch_type == BranchType.HOSTED:405 elif self.development_focus.branch.branch_type == BranchType.HOSTED:
406 return ServiceUsage.LAUNCHPAD406 return ServiceUsage.LAUNCHPAD
407 elif self.development_focus.branch.branch_type == BranchType.MIRRORED:407 elif self.development_focus.branch.branch_type in (
408 BranchType.MIRRORED, BranchType.REMOTE):
408 return ServiceUsage.EXTERNAL409 return ServiceUsage.EXTERNAL
409 return ServiceUsage.NOT_APPLICABLE410 return ServiceUsage.NOT_APPLICABLE
410411
411412
=== modified file 'lib/lp/registry/tests/test_service_usage.py'
--- lib/lp/registry/tests/test_service_usage.py 2010-09-22 00:52:15 +0000
+++ lib/lp/registry/tests/test_service_usage.py 2010-09-28 22:26:02 +0000
@@ -8,6 +8,7 @@
8from canonical.testing import DatabaseFunctionalLayer8from canonical.testing import DatabaseFunctionalLayer
99
10from lp.app.enums import ServiceUsage10from lp.app.enums import ServiceUsage
11from lp.code.enums import BranchType
11from lp.testing import (12from lp.testing import (
12 login_person,13 login_person,
13 TestCaseWithFactory,14 TestCaseWithFactory,
@@ -56,13 +57,6 @@
56 True,57 True,
57 self.target.official_answers)58 self.target.official_answers)
5859
59 def test_codehosting_usage(self):
60 # Only test get for codehosting; this has no setter because the
61 # state is derived from other data.
62 self.assertEqual(
63 ServiceUsage.UNKNOWN,
64 self.target.codehosting_usage)
65
66 def test_translations_usage_no_data(self):60 def test_translations_usage_no_data(self):
67 # By default, we don't know anything about a target61 # By default, we don't know anything about a target
68 self.assertEqual(62 self.assertEqual(
@@ -195,6 +189,42 @@
195 super(TestProductUsageEnums, self).setUp()189 super(TestProductUsageEnums, self).setUp()
196 self.target = self.factory.makeProduct()190 self.target = self.factory.makeProduct()
197191
192 def test_codehosting_unknown(self):
193 # A default product has UNKNOWN usage.
194 self.assertEqual(
195 ServiceUsage.UNKNOWN,
196 self.target.codehosting_usage)
197
198 def test_codehosting_mirrored_branch(self):
199 # A mirrored branch is EXTERNAL.
200 login_person(self.target.owner)
201 self.target.development_focus.branch = self.factory.makeProductBranch(
202 product=self.target,
203 branch_type=BranchType.MIRRORED)
204 self.assertEqual(
205 ServiceUsage.EXTERNAL,
206 self.target.codehosting_usage)
207
208 def test_codehosting_remote_branch(self):
209 # A remote branch is EXTERNAL.
210 login_person(self.target.owner)
211 self.target.development_focus.branch = self.factory.makeProductBranch(
212 product=self.target,
213 branch_type=BranchType.REMOTE)
214 self.assertEqual(
215 ServiceUsage.EXTERNAL,
216 self.target.codehosting_usage)
217
218 def test_codehosting_hosted_branch(self):
219 # A branch on Launchpad is HOSTED.
220 login_person(self.target.owner)
221 self.target.development_focus.branch = self.factory.makeProductBranch(
222 product=self.target,
223 branch_type=BranchType.HOSTED)
224 self.assertEqual(
225 ServiceUsage.LAUNCHPAD,
226 self.target.codehosting_usage)
227
198228
199class TestProductSeriesUsageEnums(229class TestProductSeriesUsageEnums(
200 TestCaseWithFactory,230 TestCaseWithFactory,