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
1=== modified file 'lib/canonical/launchpad/icing/style.css'
2--- lib/canonical/launchpad/icing/style.css 2010-05-28 19:47:23 +0000
3+++ lib/canonical/launchpad/icing/style.css 2010-09-28 22:26:02 +0000
4@@ -550,6 +550,16 @@
5 color: black !important;
6 }
7
8+.code-links td.code-count {
9+ text-align: right;
10+ padding-right: 0.5em;
11+}
12+
13+.code-links td.code-link {
14+ text-align: left;
15+ margin: 0 0 0 0;
16+}
17+
18
19 /* === Bugs === */
20 /* The Launchpad Bugs application uses a maroon color: */
21@@ -585,6 +595,15 @@
22 padding-right: 1em;
23 }
24
25+.bug-links td.bugs-count {
26+ text-align: right;
27+ padding-right: 0.5em;
28+}
29+
30+.bug-links td.bugs-link {
31+ text-align: left;
32+}
33+
34 /* --- Blueprints --- */
35
36 body.tab-specifications #actions, body.tab-specifications .results {
37@@ -652,15 +671,6 @@
38 margin: 0.5em;
39 }
40
41-.bug-links td.bugs-count {
42- text-align: right;
43- padding-right: 0.5em;
44-}
45-
46-.bug-links td.bugs-link {
47- text-align: left;
48-}
49-
50 /* ====== Content area styles ====== */
51
52 /* -- Front pages -- */
53
54=== modified file 'lib/lp/code/browser/branch.py'
55--- lib/lp/code/browser/branch.py 2010-08-24 10:45:57 +0000
56+++ lib/lp/code/browser/branch.py 2010-09-28 22:26:02 +0000
57@@ -16,6 +16,7 @@
58 'BranchReviewerEditView',
59 'BranchMergeQueueView',
60 'BranchMirrorStatusView',
61+ 'BranchMirrorMixin',
62 'BranchNameValidationMixin',
63 'BranchNavigation',
64 'BranchEditMenu',
65@@ -374,7 +375,39 @@
66 return Link('+new-recipe', text, enabled=enabled, icon='add')
67
68
69-class BranchView(LaunchpadView, FeedsMixin):
70+class BranchMirrorMixin:
71+ """Provide mirror_location property.
72+
73+ Requires self.branch to be set by the class using this mixin.
74+ """
75+
76+ @property
77+ def mirror_location(self):
78+ """Check the mirror location to see if it is a private one."""
79+ branch = self.branch
80+
81+ # If the user has edit permissions, then show the actual location.
82+ if check_permission('launchpad.Edit', branch):
83+ return branch.url
84+
85+ # XXX: Tim Penhey, 2008-05-30
86+ # Instead of a configuration hack we should support the users
87+ # specifying whether or not they want the mirror location
88+ # hidden or not. Given that this is a database patch,
89+ # it isn't going to happen today.
90+ # See bug 235916
91+ hosts = config.codehosting.private_mirror_hosts.split(',')
92+ private_mirror_hosts = [name.strip() for name in hosts]
93+
94+ uri = URI(branch.url)
95+ for private_host in private_mirror_hosts:
96+ if uri.underDomain(private_host):
97+ return '<private server>'
98+
99+ return branch.url
100+
101+
102+class BranchView(LaunchpadView, FeedsMixin, BranchMirrorMixin):
103
104 feed_types = (
105 BranchFeedLink,
106@@ -387,6 +420,7 @@
107 label = page_title
108
109 def initialize(self):
110+ self.branch = self.context
111 self.notices = []
112 # Replace our context with a decorated branch, if it is not already
113 # decorated.
114@@ -586,31 +620,6 @@
115 return url.startswith("http")
116
117 @property
118- def mirror_location(self):
119- """Check the mirror location to see if it is a private one."""
120- branch = self.context
121-
122- # If the user has edit permissions, then show the actual location.
123- if check_permission('launchpad.Edit', branch):
124- return branch.url
125-
126- # XXX: Tim Penhey, 2008-05-30
127- # Instead of a configuration hack we should support the users
128- # specifying whether or not they want the mirror location
129- # hidden or not. Given that this is a database patch,
130- # it isn't going to happen today.
131- # See bug 235916
132- hosts = config.codehosting.private_mirror_hosts.split(',')
133- private_mirror_hosts = [name.strip() for name in hosts]
134-
135- uri = URI(branch.url)
136- for private_host in private_mirror_hosts:
137- if uri.underDomain(private_host):
138- return '<private server>'
139-
140- return branch.url
141-
142- @property
143 def show_merge_links(self):
144 """Return whether or not merge proposal links should be shown.
145
146
147=== modified file 'lib/lp/code/browser/branchlisting.py'
148--- lib/lp/code/browser/branchlisting.py 2010-08-31 11:11:09 +0000
149+++ lib/lp/code/browser/branchlisting.py 2010-09-28 22:26:02 +0000
150@@ -1,4 +1,4 @@
151-# Copyright 2009 Canonical Ltd. This software is licensed under the
152+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
153 # GNU Affero General Public License version 3 (see the file LICENSE).
154
155 """Base class view for branch listings."""
156@@ -83,15 +83,18 @@
157 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
158 from canonical.launchpad.webapp.publisher import LaunchpadView
159 from canonical.widgets import LaunchpadDropdownWidget
160+from lp.app.browser.tales import MenuAPI
161 from lp.blueprints.interfaces.specificationbranch import (
162 ISpecificationBranchSet,
163 )
164 from lp.bugs.interfaces.bugbranch import IBugBranchSet
165+from lp.code.browser.branch import BranchMirrorMixin
166 from lp.code.browser.branchmergeproposallisting import (
167 ActiveReviewsView,
168 PersonActiveReviewsView,
169 PersonProductActiveReviewsView,
170 )
171+from lp.code.browser.summary import BranchCountSummaryView
172 from lp.code.enums import (
173 BranchLifecycleStatus,
174 BranchLifecycleStatusFilter,
175@@ -124,7 +127,6 @@
176 IPersonProduct,
177 IPersonProductFactory,
178 )
179-from lp.registry.interfaces.pocket import PackagePublishingPocket
180 from lp.registry.interfaces.product import IProduct
181 from lp.registry.interfaces.series import SeriesStatus
182 from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
183@@ -421,8 +423,7 @@
184 return sorted(links, key=attrgetter('pocket'))
185
186 def getDistroDevelSeries(self, distribution):
187- """Since distribution.currentseries hits the DB every time, cache it."""
188- self._distro_series_map = {}
189+ """distribution.currentseries hits the DB every time so cache it."""
190 try:
191 return self._distro_series_map[distribution]
192 except KeyError:
193@@ -777,7 +778,7 @@
194 """A branch listing that has no associated product or person."""
195
196 field_names = ['lifecycle']
197- no_sort_by = (BranchListingSort.DEFAULT,)
198+ no_sort_by = (BranchListingSort.DEFAULT, )
199
200 no_branch_message = (
201 'There are no branches that match the current status filter.')
202@@ -932,8 +933,8 @@
203 def active_reviews(self):
204 text = get_plural_text(
205 self.active_review_count,
206- 'active review or unmerged proposal',
207- 'active reviews or unmerged proposals')
208+ 'active review',
209+ 'active reviews')
210 return Link('+activereviews', text)
211
212 def addbranch(self):
213@@ -1022,7 +1023,7 @@
214
215 page_title = _('Subscribed')
216 label_template = 'Bazaar branches subscribed to by %(displayname)s'
217- no_sort_by = (BranchListingSort.DEFAULT,)
218+ no_sort_by = (BranchListingSort.DEFAULT, )
219
220 def _getCollection(self):
221 return getUtility(IAllBranches).subscribedBy(self.context)
222@@ -1122,8 +1123,8 @@
223 def active_reviews(self):
224 text = get_plural_text(
225 self.active_review_count,
226- 'active review or unmerged proposal',
227- 'active reviews or unmerged proposals')
228+ 'Active review',
229+ 'Active reviews')
230 return Link('+activereviews', text, site='code')
231
232 @enabled_with_permission('launchpad.Commercial')
233@@ -1140,7 +1141,7 @@
234 """A base class for product branch listings."""
235
236 show_series_links = True
237- no_sort_by = (BranchListingSort.PRODUCT,)
238+ no_sort_by = (BranchListingSort.PRODUCT, )
239 label_template = 'Bazaar branches of %(displayname)s'
240
241 def _getCollection(self):
242@@ -1180,8 +1181,23 @@
243 return message % self.context.displayname
244
245
246+class ProductBranchStatisticsView(BranchCountSummaryView,
247+ ProductBranchListingView):
248+ """Portlet containing branch statistics."""
249+
250+ @property
251+ def branch_text(self):
252+ text = super(ProductBranchStatisticsView, self).branch_text
253+ return text.capitalize()
254+
255+ @property
256+ def commit_text(self):
257+ text = super(ProductBranchStatisticsView, self).commit_text
258+ return text.capitalize()
259+
260+
261 class ProductCodeIndexView(ProductBranchListingView, SortSeriesMixin,
262- ProductDownloadFileMixin):
263+ ProductDownloadFileMixin, BranchMirrorMixin):
264 """Initial view for products on the code virtual host."""
265
266 show_set_development_focus = True
267@@ -1193,6 +1209,10 @@
268 self.revision_cache = revision_cache.inProduct(self.product)
269
270 @property
271+ def branch(self):
272+ return self.development_focus_branch
273+
274+ @property
275 def form_action(self):
276 return "+branches"
277
278@@ -1240,6 +1260,7 @@
279 # skip subsequent series where the lifecycle status is Merged or
280 # Abandoned
281 sorted_series = self.sorted_active_series_list
282+
283 def show_branch(branch):
284 if self.selected_lifecycle_status is None:
285 return True
286@@ -1323,6 +1344,14 @@
287 def committer_text(self):
288 return get_plural_text(self.committer_count, _('person'), _('people'))
289
290+ @property
291+ def configure_codehosting(self):
292+ """Get the menu link for configuring code hosting."""
293+ series_menu = MenuAPI(self.context.development_focus).overview
294+ set_branch = series_menu['set_branch']
295+ set_branch.text = 'Configure code hosting'
296+ return set_branch
297+
298
299 class ProductBranchesView(ProductBranchListingView):
300 """View for branch listing for a product."""
301@@ -1353,7 +1382,7 @@
302 class ProjectBranchesView(BranchListingView):
303 """View for branch listing for a project."""
304
305- no_sort_by = (BranchListingSort.DEFAULT,)
306+ no_sort_by = (BranchListingSort.DEFAULT, )
307 extra_columns = ('author', 'product')
308 label_template = 'Bazaar branches of %(displayname)s'
309 show_series_links = True
310
311=== modified file 'lib/lp/code/browser/configure.zcml'
312--- lib/lp/code/browser/configure.zcml 2010-09-22 18:37:57 +0000
313+++ lib/lp/code/browser/configure.zcml 2010-09-28 22:26:02 +0000
314@@ -1,4 +1,4 @@
315-<!-- Copyright 2009 Canonical Ltd. This software is licensed under the
316+<!-- Copyright 2009-2010 Canonical Ltd. This software is licensed under the
317 GNU Affero General Public License version 3 (see the file LICENSE).
318 -->
319
320@@ -961,6 +961,18 @@
321 permission="zope.Public"
322 name="+code-index"
323 template="../templates/product-branches.pt"/>
324+ <browser:page
325+ for="lp.registry.interfaces.product.IProduct"
326+ class="lp.code.browser.branchlisting.ProductBranchStatisticsView"
327+ permission="zope.Public"
328+ name="+portlet-product-codestatistics"
329+ template="../templates/product-portlet-codestatistics.pt"/>
330+ <browser:page
331+ for="lp.registry.interfaces.product.IProduct"
332+ class="lp.code.browser.branchlisting.ProductBranchStatisticsView"
333+ permission="zope.Public"
334+ name="+portlet-product-codestatistics-content"
335+ template="../templates/product-portlet-codestatistics-content.pt"/>
336 <browser:page
337 for="lp.registry.interfaces.product.IProduct"
338 layer="lp.code.publisher.CodeLayer"
339
340=== modified file 'lib/lp/code/browser/tests/test_branch.py'
341--- lib/lp/code/browser/tests/test_branch.py 2010-08-20 20:31:18 +0000
342+++ lib/lp/code/browser/tests/test_branch.py 2010-09-28 22:26:02 +0000
343@@ -45,7 +45,6 @@
344 )
345 from lp.code.interfaces.branchtarget import IBranchTarget
346 from lp.testing import (
347- ANONYMOUS,
348 login,
349 login_person,
350 logout,
351@@ -78,6 +77,7 @@
352 branch_type=BranchType.MIRRORED,
353 url="http://example.com/good/mirror")
354 view = BranchView(branch, LaunchpadTestRequest())
355+ view.initialize()
356 self.assertTrue(view.user is None)
357 self.assertEqual(
358 "http://example.com/good/mirror", view.mirror_location)
359@@ -89,6 +89,7 @@
360 branch_type=BranchType.MIRRORED,
361 url="http://private.example.com/bzr-mysql/mysql-5.0")
362 view = BranchView(branch, LaunchpadTestRequest())
363+ view.initialize()
364 self.assertTrue(view.user is None)
365 self.assertEqual(
366 "<private server>", view.mirror_location)
367@@ -106,6 +107,7 @@
368 logout()
369 login('eric@example.com')
370 view = BranchView(branch, LaunchpadTestRequest())
371+ view.initialize()
372 self.assertEqual(view.user, owner)
373 self.assertEqual(
374 "http://private.example.com/bzr-mysql/mysql-5.0",
375@@ -126,6 +128,7 @@
376 logout()
377 login('other@example.com')
378 view = BranchView(branch, LaunchpadTestRequest())
379+ view.initialize()
380 self.assertEqual(view.user, other)
381 self.assertEqual(
382 "<private server>", view.mirror_location)
383@@ -160,7 +163,7 @@
384 len(branch.mirror_status_message)
385 <= branch_view.MAXIMUM_STATUS_MESSAGE_LENGTH,
386 "branch.mirror_status_message longer than expected: %r"
387- % (branch.mirror_status_message,))
388+ % (branch.mirror_status_message, ))
389 self.assertEqual(
390 branch.mirror_status_message, branch_view.mirror_status_message)
391 self.assertEqual(
392@@ -185,7 +188,7 @@
393 'whiteboard': '',
394 'owner': arbitrary_person,
395 'author': arbitrary_person,
396- 'product': arbitrary_product
397+ 'product': arbitrary_product,
398 }
399 add_view.add_action.success(data)
400 # Make sure that next_mirror_time is a datetime, not an sqlbuilder
401
402=== modified file 'lib/lp/code/browser/tests/test_product.py'
403--- lib/lp/code/browser/tests/test_product.py 2010-08-24 02:16:53 +0000
404+++ lib/lp/code/browser/tests/test_product.py 2010-09-28 22:26:02 +0000
405@@ -1,4 +1,4 @@
406-# Copyright 2009 Canonical Ltd. This software is licensed under the
407+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
408 # GNU Affero General Public License version 3 (see the file LICENSE).
409
410 """Tests for the product view classes and templates."""
411@@ -10,20 +10,25 @@
412 timedelta,
413 )
414 import unittest
415-
416 from mechanize import LinkNotFoundError
417 import pytz
418-from zope.component import (
419- getMultiAdapter,
420- getUtility,
421+from zope.component import getUtility
422+
423+from canonical.launchpad.testing.pages import (
424+ extract_text,
425+ find_tag_by_id,
426 )
427-
428 from canonical.launchpad.webapp import canonical_url
429-from canonical.launchpad.webapp.servers import LaunchpadTestRequest
430 from canonical.testing import DatabaseFunctionalLayer
431+from lp.app.enums import ServiceUsage
432+from lp.code.enums import (
433+ BranchType,
434+ BranchVisibilityRule,
435+ )
436 from lp.code.interfaces.revision import IRevisionSet
437 from lp.testing import (
438 ANONYMOUS,
439+ BrowserTestCase,
440 login,
441 login_person,
442 TestCaseWithFactory,
443@@ -32,9 +37,8 @@
444 from lp.testing.views import create_initialized_view
445
446
447-class TestProductCodeIndexView(TestCaseWithFactory):
448- """Tests for the product code home page."""
449-
450+class ProductTestBase(TestCaseWithFactory):
451+ """Common methods for tests herein."""
452 layer = DatabaseFunctionalLayer
453
454 def makeProductAndDevelopmentFocusBranch(self, **branch_args):
455@@ -49,6 +53,10 @@
456 product.development_focus.branch = branch
457 return product, branch
458
459+
460+class TestProductCodeIndexView(ProductTestBase):
461+ """Tests for the product code home page."""
462+
463 def getBranchSummaryBrowseLinkForProduct(self, product):
464 """Get the 'browse code' link from the product's code home.
465
466@@ -98,13 +106,15 @@
467
468 def test_initial_branches_contains_dev_focus_branch(self):
469 product, branch = self.makeProductAndDevelopmentFocusBranch()
470- view = create_initialized_view(product, '+code-index', rootsite='code')
471+ view = create_initialized_view(product, '+code-index',
472+ rootsite='code')
473 self.assertIn(branch, view.initial_branches)
474
475 def test_initial_branches_does_not_contain_private_dev_focus_branch(self):
476 product, branch = self.makeProductAndDevelopmentFocusBranch(
477 private=True)
478- view = create_initialized_view(product, '+code-index', rootsite='code')
479+ view = create_initialized_view(product, '+code-index',
480+ rootsite='code')
481 self.assertNotIn(branch, view.initial_branches)
482
483 def test_committer_count_with_revision_authors(self):
484@@ -120,7 +130,8 @@
485 date_generator=date_generator)
486 getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)
487
488- view = create_initialized_view(product, '+code-index', rootsite='code')
489+ view = create_initialized_view(product, '+code-index',
490+ rootsite='code')
491 self.assertEqual(view.committer_count, 1)
492
493 def test_committers_count_private_branch(self):
494@@ -138,10 +149,150 @@
495 date_generator=date_generator)
496 getUtility(IRevisionSet).updateRevisionCacheForBranch(branch)
497
498- view = create_initialized_view(product, '+code-index', rootsite='code')
499+ view = create_initialized_view(product, '+code-index',
500+ rootsite='code')
501 self.assertEqual(view.committer_count, 1)
502
503
504+class TestProductCodeIndexServiceUsages(ProductTestBase, BrowserTestCase):
505+ """Tests for the product code page, especially the usage messasges."""
506+
507+ def test_external_mirrored(self):
508+ # Test that the correct URL is displayed for a mirrored branch.
509+ product, branch = self.makeProductAndDevelopmentFocusBranch(
510+ branch_type=BranchType.MIRRORED,
511+ url="http://example.com/mybranch")
512+ self.assertEqual(ServiceUsage.EXTERNAL, product.codehosting_usage)
513+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
514+ login(ANONYMOUS)
515+ content = find_tag_by_id(browser.contents, 'external')
516+ text = extract_text(content)
517+ expected = ("%(product_title)s hosts its code at %(branch_url)s. "
518+ "Launchpad has a mirror of the master branch "
519+ "and you can create branches from it." % dict(
520+ product_title=product.title,
521+ branch_url=branch.url))
522+ self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
523+
524+ def test_external_remote(self):
525+ # Test that a remote branch is shown properly.
526+ product, branch = self.makeProductAndDevelopmentFocusBranch(
527+ branch_type=BranchType.REMOTE,
528+ url="http://example.com/mybranch")
529+ self.assertEqual(ServiceUsage.EXTERNAL,
530+ product.codehosting_usage)
531+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
532+ login(ANONYMOUS)
533+ content = find_tag_by_id(browser.contents, 'external')
534+ text = extract_text(content)
535+ expected = ("%(product_title)s hosts its code at %(branch_url)s. "
536+ "Launchpad does not have a copy of the remote "
537+ "branch." % dict(
538+ product_title=product.title,
539+ branch_url=branch.url))
540+ self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
541+
542+ def test_unknown(self):
543+ product = self.factory.makeProduct()
544+ self.assertEqual(ServiceUsage.UNKNOWN, product.codehosting_usage)
545+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
546+ login(ANONYMOUS)
547+ content = find_tag_by_id(browser.contents, 'unknown')
548+ text = extract_text(content)
549+ expected = (
550+ "Launchpad does not know where %(product_title)s "
551+ "hosts its code.*" %
552+ dict(product_title=product.title))
553+ self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
554+
555+ def test_on_launchpad(self):
556+ product, branch = self.makeProductAndDevelopmentFocusBranch()
557+ self.assertEqual(ServiceUsage.LAUNCHPAD, product.codehosting_usage)
558+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
559+ login(ANONYMOUS)
560+ text = extract_text(find_tag_by_id(
561+ browser.contents, 'branch-count-summary'))
562+ expected = "1 active branch owned by 1 person.*"
563+ self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
564+
565+ def test_view_mirror_location(self):
566+ url = "http://example.com/mybranch"
567+ product, branch = self.makeProductAndDevelopmentFocusBranch(
568+ branch_type=BranchType.MIRRORED,
569+ url=url)
570+ view = create_initialized_view(product, '+code-index',
571+ rootsite='code')
572+ self.assertEqual(url, view.mirror_location)
573+
574+
575+class TestProductBranchesViewPortlets(ProductTestBase, BrowserTestCase):
576+ """Tests for the portlets."""
577+
578+ def test_portlet_not_shown_for_UNKNOWN(self):
579+ # If the BranchUsage is UNKNOWN then the portlets are not shown.
580+ product = self.factory.makeProduct()
581+ self.assertEqual(ServiceUsage.UNKNOWN, product.codehosting_usage)
582+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
583+ contents = browser.contents
584+ self.assertIs(None, find_tag_by_id(contents, 'branch-portlet'))
585+ self.assertIs(None, find_tag_by_id(contents, 'privacy'))
586+ self.assertIs(None, find_tag_by_id(contents, 'involvement'))
587+ self.assertIs(None, find_tag_by_id(
588+ contents, 'portlet-product-codestatistics'))
589+
590+ def test_portlets_shown_for_HOSTED(self):
591+ # If the BranchUsage is HOSTED then the portlets are shown.
592+ product, branch = self.makeProductAndDevelopmentFocusBranch()
593+ self.assertEqual(ServiceUsage.LAUNCHPAD, product.codehosting_usage)
594+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
595+ contents = browser.contents
596+ self.assertIsNot(None, find_tag_by_id(contents, 'branch-portlet'))
597+ self.assertIsNot(None, find_tag_by_id(contents, 'privacy'))
598+ self.assertIsNot(None, find_tag_by_id(contents, 'involvement'))
599+ self.assertIsNot(None, find_tag_by_id(
600+ contents, 'portlet-product-codestatistics'))
601+
602+ def test_portlets_shown_for_EXTERNAL(self):
603+ # If the BranchUsage is EXTERNAL then the portlets are shown.
604+ url = "http://example.com/mybranch"
605+ product, branch = self.makeProductAndDevelopmentFocusBranch(
606+ branch_type=BranchType.MIRRORED,
607+ url=url)
608+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
609+ contents = browser.contents
610+ self.assertIsNot(None, find_tag_by_id(contents, 'branch-portlet'))
611+ self.assertIsNot(None, find_tag_by_id(contents, 'privacy'))
612+ self.assertIsNot(None, find_tag_by_id(contents, 'involvement'))
613+ self.assertIsNot(None, find_tag_by_id(
614+ contents, 'portlet-product-codestatistics'))
615+
616+ def test_is_private(self):
617+ team_owner = self.factory.makePerson()
618+ team = self.factory.makeTeam(team_owner)
619+ product = self.factory.makeProduct(owner=team_owner)
620+ branch = self.factory.makeProductBranch(product=product)
621+ login_person(product.owner)
622+ product.development_focus.branch = branch
623+ product.setBranchVisibilityTeamPolicy(
624+ team, BranchVisibilityRule.PRIVATE)
625+ view = create_initialized_view(
626+ product, '+code-index', rootsite='code', principal=product.owner)
627+ text = extract_text(find_tag_by_id(view.render(), 'privacy'))
628+ expected = ("New branches you create for %(name)s are private "
629+ "initially.*" % dict(name=product.displayname))
630+ self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
631+
632+ def test_is_public(self):
633+ product = self.factory.makeProduct()
634+ branch = self.factory.makeProductBranch(product=product)
635+ login_person(product.owner)
636+ product.development_focus.branch = branch
637+ browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
638+ text = extract_text(find_tag_by_id(browser.contents, 'privacy'))
639+ expected = ("New branches you create for %(name)s are public "
640+ "initially.*" % dict(name=product.displayname))
641+ self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
642+
643+
644 def test_suite():
645 return unittest.TestLoader().loadTestsFromName(__name__)
646-
647
648=== modified file 'lib/lp/code/stories/branches/xx-branch-deletion.txt'
649--- lib/lp/code/stories/branches/xx-branch-deletion.txt 2010-05-13 16:22:19 +0000
650+++ lib/lp/code/stories/branches/xx-branch-deletion.txt 2010-09-28 22:26:02 +0000
651@@ -4,20 +4,34 @@
652 can be deleted. The main use for this is to allow users to delete
653 branches that have been created in error.
654
655- >>> browser = setupBrowser(auth="Basic test@canonical.com:test")
656- >>> browser.open('http://code.launchpad.dev/firefox')
657+ >>> from lp.code.enums import BranchType
658+ >>> login(ANONYMOUS)
659+ >>> alice = factory.makePerson(name="alice", password="test",
660+ ... email="alice@example.com")
661+ >>> product = factory.makeProduct(
662+ ... name='earthlynx', displayname="Earth Lynx", owner=alice)
663+ >>> branch = factory.makeProductBranch(
664+ ... product=product, branch_type=BranchType.HOSTED)
665+ >>> productseries = factory.makeProductSeries(
666+ ... product=product, branch=branch)
667+ >>> login_person(alice)
668+ >>> product.development_focus = productseries
669+ >>> logout()
670+
671+ >>> browser = setupBrowser(auth="Basic alice@example.com:test")
672+ >>> browser.open('http://code.launchpad.dev/earthlynx')
673 >>> browser.getLink("Register a branch").click()
674 >>> browser.getControl('Branch URL').value = 'http://foo.bar.com/oops'
675 >>> browser.getControl('Name').value = 'to-delete'
676 >>> browser.getControl('Register Branch').click()
677 >>> print browser.title
678- to-delete : Code : Mozilla Firefox
679+ to-delete : Code : Earth Lynx
680
681 The newly created branch has an action 'Delete branch'.
682
683 >>> delete_link = browser.getLink('Delete branch')
684 >>> print delete_link.url
685- http://code.launchpad.dev/~name12/firefox/to-delete/+delete
686+ http://code.launchpad.dev/~alice/earthlynx/to-delete/+delete
687
688 When the user clicks on the link, they are informed what will happen if they
689 delete the branch.
690@@ -25,7 +39,7 @@
691 >>> delete_link.click()
692 >>> print extract_text(find_main_content(browser.contents))
693 Delete branch
694- Mozilla Firefox...
695+ Earth Lynx...
696 Branch deletion is permanent.
697 or Cancel
698
699@@ -35,15 +49,15 @@
700
701 >>> browser.getControl('Delete').click()
702 >>> print browser.url
703- http://code.launchpad.dev/firefox
704+ http://code.launchpad.dev/earthlynx
705 >>> for message in get_feedback_messages(browser.contents):
706 ... print message
707- Branch ~name12/firefox/to-delete deleted...
708+ Branch ~alice/earthlynx/to-delete deleted...
709
710 If the branch is junk, then the user is taken back to the code listing for
711 the deleted branch's owner.
712
713- >>> browser.open('http://code.launchpad.dev/~name12')
714+ >>> browser.open('http://code.launchpad.dev/~alice')
715 >>> browser.getLink("Register a branch").click()
716 >>> browser.getControl('Hosted').click()
717 >>> browser.getControl('Name').value = 'to-delete'
718@@ -51,14 +65,15 @@
719 >>> browser.getLink('Delete branch').click()
720 >>> browser.getControl('Delete').click()
721 >>> print browser.url
722- http://code.launchpad.dev/~name12
723+ http://code.launchpad.dev/~alice
724 >>> for message in get_feedback_messages(browser.contents):
725 ... print message
726- Branch ~name12/+junk/to-delete deleted...
727+ Branch ~alice/+junk/to-delete deleted...
728
729 Branches that are stacked upon cannot be deleted.
730
731- >>> login('admin@canonical.com')
732+ >>> from lp.testing.sampledata import ADMIN_EMAIL
733+ >>> login(ADMIN_EMAIL)
734 >>> stacked_upon = factory.makeAnyBranch()
735 >>> stacked = factory.makeAnyBranch(stacked_on=stacked_upon)
736 >>> branch_location = canonical_url(stacked_upon)
737@@ -82,12 +97,11 @@
738 >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
739 >>> from lp.registry.interfaces.person import IPersonSet
740 >>> login(ANONYMOUS)
741- >>> name12 = getUtility(IPersonSet).getByName('name12')
742 >>> ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
743 >>> ignored = removeSecurityProxy(ubuntu_branches).addMember(
744- ... name12, ubuntu_branches.teamowner)
745- >>> login_person(name12)
746- >>> branch = factory.makePackageBranch(owner=name12)
747+ ... alice, ubuntu_branches.teamowner)
748+ >>> login_person(alice)
749+ >>> branch = factory.makePackageBranch(owner=alice)
750 >>> package = branch.sourcepackage
751 >>> package.setBranch(
752 ... PackagePublishingPocket.RELEASE, branch, branch.registrant)
753
754=== modified file 'lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt'
755--- lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt 2010-01-14 23:39:06 +0000
756+++ lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt 2010-09-28 22:26:02 +0000
757@@ -63,11 +63,11 @@
758
759 >>> browser.open('http://code.launchpad.dev/fooix')
760 >>> print_tag_with_id(browser.contents, 'merge-counts')
761- 3 active reviews or unmerged proposals
762-
763-The 'active reviews or unmerged proposals' text links to the active reviews page.
764-
765- >>> browser.getLink('active reviews or unmerged proposals').click()
766+ 3 Active reviews
767+
768+The 'active reviews' text links to the active reviews page.
769+
770+ >>> browser.getLink('Active reviews').click()
771 >>> print browser.title
772 Active code reviews for Fooix...
773
774@@ -94,7 +94,7 @@
775 >>> browser.open('http://code.launchpad.dev/~albert')
776 >>> print_tag_with_id(browser.contents, 'page-summary')
777 1 owned branch, 1 registered branch, 1 subscribed branch
778- 1 active review or unmerged proposal
779+ 1 active review
780
781 The person's active reviews also lists all of their currently requested
782 reviews.
783
784=== modified file 'lib/lp/code/stories/branches/xx-creating-branches.txt'
785--- lib/lp/code/stories/branches/xx-creating-branches.txt 2010-09-27 19:39:21 +0000
786+++ lib/lp/code/stories/branches/xx-creating-branches.txt 2010-09-28 22:26:02 +0000
787@@ -15,6 +15,19 @@
788
789 Hosted branches use Launchpad as their primary location.
790
791+ >>> from lp.registry.interfaces.product import IProductSet
792+ >>> from lp.code.enums import BranchType
793+ >>> from zope.component import getUtility
794+ >>> login(ANONYMOUS)
795+ >>> redfish = getUtility(IProductSet).getByName('redfish')
796+ >>> branch = factory.makeProductBranch(
797+ ... product=redfish, branch_type=BranchType.HOSTED)
798+ >>> productseries = factory.makeProductSeries(
799+ ... product=redfish, branch=branch)
800+ >>> login_person(redfish.owner)
801+ >>> redfish.development_focus = productseries
802+ >>> logout()
803+
804 >>> browser = setupBrowser(auth="Basic test@canonical.com:test")
805 >>> browser.open('http://code.launchpad.dev/redfish')
806 >>> browser.getLink("Register a branch").click()
807@@ -122,6 +135,29 @@
808 Let's make sure we can load the branch creation form on a product.
809
810 >>> user_browser.getLink("Register a branch").click()
811+ Traceback (most recent call last):
812+ ...
813+ LinkNotFoundError
814+
815+The link is not there because the product has not been configured to
816+do code hosting yet. The development focus must be set with a branch first.
817+
818+ >>> owner_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test")
819+ >>> owner_browser.open('http://code.launchpad.dev/applets')
820+ >>> owner_browser.getLink('Configure code hosting').click()
821+ >>> print owner_browser.url
822+ http://code.launchpad.dev/applets/trunk/+setbranch
823+
824+ >>> owner_browser.getControl(
825+ ... 'Create a new, empty branch').click()
826+ >>> owner_browser.getControl('Branch name').value = 'trunk'
827+ >>> owner_browser.getControl('Update').click()
828+
829+Now that code hosting has been configured, a regular user will be able
830+to register a branch.
831+
832+ >>> user_browser.open('http://code.launchpad.dev/applets')
833+ >>> user_browser.getLink("Register a branch").click()
834 >>> print user_browser.url
835 http://code.launchpad.dev/applets/+addbranch
836
837@@ -313,7 +349,7 @@
838 URL validation should check that the entered URL is not the root of a
839 site.
840
841- >>> user_browser.open('http://code.launchpad.dev/firefox')
842+ >>> user_browser.open('http://code.launchpad.dev/applets')
843 >>> user_browser.getLink("Register a branch").click()
844 >>> user_browser.getControl('Branch URL').value = 'http://example.com'
845 >>> user_browser.getControl('Name').value = 'unique-name'
846@@ -326,7 +362,7 @@
847
848 URL validation should check that the entered URL is not from Launchpad.
849
850- >>> user_browser.open('http://code.launchpad.dev/firefox')
851+ >>> user_browser.open('http://code.launchpad.dev/applets')
852 >>> user_browser.getLink("Register a branch").click()
853 >>> user_browser.getControl('Branch URL').value = (
854 ... 'http://code.launchpad.dev/~testuser/')
855@@ -341,7 +377,7 @@
856 As well as checking against the root site set in the config, a check is
857 also done against the value stored as a database constraint.
858
859- >>> user_browser.open('http://code.launchpad.dev/firefox')
860+ >>> user_browser.open('http://code.launchpad.dev/applets')
861 >>> user_browser.getLink("Register a branch").click()
862 >>> user_browser.getControl('Branch URL').value = (
863 ... 'http://bazaar.launchpad.net/foo/bar/')
864@@ -377,6 +413,14 @@
865 When registering a branch from the product pages, there is no product
866 widget, so errors are set at the page level.
867
868+ >>> owner_browser = setupBrowser(auth="Basic test@canonical.com:test")
869+ >>> owner_browser.open('http://code.launchpad.dev/landscape')
870+ >>> owner_browser.getLink('Configure code hosting').click()
871+ >>> owner_browser.getControl(
872+ ... 'Create a new, empty branch').click()
873+ >>> owner_browser.getControl('Branch name').value = 'trunk'
874+ >>> owner_browser.getControl('Update').click()
875+
876 >>> user_browser.open('http://code.launchpad.dev/landscape')
877 >>> user_browser.getLink("Register a branch").click()
878 >>> user_browser.getControl('Branch URL').value = 'http://foo.com/bar'
879
880=== modified file 'lib/lp/code/stories/branches/xx-person-branches.txt'
881--- lib/lp/code/stories/branches/xx-person-branches.txt 2010-05-27 02:19:27 +0000
882+++ lib/lp/code/stories/branches/xx-person-branches.txt 2010-09-28 22:26:02 +0000
883@@ -101,7 +101,7 @@
884 >>> eric_browser.open('http://code.launchpad.dev/~eric')
885 >>> print_tag_with_id(eric_browser.contents, 'page-summary')
886 1 owned branch, 1 registered branch, 1 subscribed branch
887- 0 active reviews or unmerged proposals
888+ 0 active reviews
889
890 Now we'll create another branch, and unsubscribe the owner from it.
891
892@@ -113,4 +113,4 @@
893 >>> eric_browser.open('http://code.launchpad.dev/~eric')
894 >>> print_tag_with_id(eric_browser.contents, 'page-summary')
895 2 owned branches, 2 registered branches, 1 subscribed branch
896- 0 active reviews or unmerged proposals
897+ 0 active reviews
898
899=== modified file 'lib/lp/code/stories/branches/xx-private-branch-listings.txt'
900--- lib/lp/code/stories/branches/xx-private-branch-listings.txt 2010-09-03 00:25:07 +0000
901+++ lib/lp/code/stories/branches/xx-private-branch-listings.txt 2010-09-28 22:26:02 +0000
902@@ -1,4 +1,5 @@
903-= Private Branch Listings =
904+Private Branch Listings
905+=======================
906
907 All pages that show branch listings to users should only show branches
908 that the user is allowed to see.
909@@ -15,7 +16,8 @@
910 ... reset_all_branch_last_modified)
911 >>> reset_all_branch_last_modified()
912
913-== Additional sample data ==
914+Additional sample data
915+----------------------
916
917 Adding a private branch that is only visible by No Privileges Person
918 (and Launchpad administrators).
919@@ -37,7 +39,8 @@
920 >>> logout()
921
922
923-== The code home page ==
924+The code home page
925+------------------
926
927 The code home page shows lists of recently imported, changed, and
928 registered branches.
929@@ -60,7 +63,8 @@
930 Logged in users should only be able to see public branches, and private
931 branches that they are subscribed to or are the owner of.
932
933- >>> no_priv_browser = setupBrowser(auth='Basic no-priv@canonical.com:test')
934+ >>> no_priv_browser = setupBrowser(
935+ ... auth='Basic no-priv@canonical.com:test')
936 >>> print_recently_registered_branches(no_priv_browser)
937 '...~no-priv/landscape/testing-branch...<span...class="sprite private"...'
938 '...~mark/+junk/testdoc...'
939@@ -91,7 +95,8 @@
940 '...~name12/gnome-terminal/scanned...'
941
942
943-== Landscape code listing page ==
944+Landscape code listing page
945+---------------------------
946
947 One of the most obvious places to hide private branches are the code
948 listing tab.
949@@ -103,12 +108,13 @@
950 ... # So print the text shown in the application summary.
951 ... if table is None:
952 ... print extract_text(find_tag_by_id(
953- ... browser.contents, 'application-summary'))
954+ ... browser.contents, 'branch-summary'))
955 ... else:
956 ... for row in table.tbody.fetch('tr'):
957 ... print extract_text(row)
958
959 >>> print_landscape_code_listing(anon_browser)
960+ Launchpad does not know where The Landscape Project hosts its code...
961 There are no branches for The Landscape Project in Launchpad...
962
963 >>> print_landscape_code_listing(no_priv_browser)
964@@ -124,7 +130,8 @@
965 lp://dev/~no-priv/landscape/testing-branch Development ...
966
967
968-== Person code listing pages ==
969+Person code listing pages
970+-------------------------
971
972 The person code listings is the other obvious place to filter out the
973 viewable branches.
974@@ -173,7 +180,8 @@
975 >>> print_person_code_listing(landscape_dev_browser, '/+ownedbranches')
976 Total of 10 branches listed
977 lp://dev/~name12/landscape/feature-x Development ...
978- >>> print_person_code_listing(landscape_dev_browser, '/+registeredbranches')
979+ >>> print_person_code_listing(landscape_dev_browser,
980+ ... '/+registeredbranches')
981 Total of 11 branches listed
982 lp://dev/~landscape-developers/landscape/trunk Development ...
983 lp://dev/~name12/landscape/feature-x Development ...
984@@ -190,7 +198,8 @@
985 lp://dev/~name12/landscape/feature-x Development ...
986
987
988-== Bug branch links ==
989+Bug branch links
990+----------------
991
992 When a private branch is linked to a bug, the bug branch link is only
993 visible to those that would be able to see the branch.
994@@ -227,7 +236,8 @@
995 No bug branch links
996
997
998-== Branches set as primary branches for product series ==
999+Branches set as primary branches for product series
1000+---------------------------------------------------
1001
1002 When a branch is set as the user branch for product series, the details
1003 must be visible to those that are entitled to see it, but hidden from
1004
1005=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
1006--- lib/lp/code/stories/branches/xx-product-branches.txt 2010-08-19 14:22:01 +0000
1007+++ lib/lp/code/stories/branches/xx-product-branches.txt 2010-09-28 22:26:02 +0000
1008@@ -34,25 +34,34 @@
1009 If there are not any branches, a helpful message is shown.
1010
1011 >>> def get_summary(browser):
1012- ... return find_tag_by_id(browser.contents, 'application-summary')
1013+ ... return find_tag_by_id(browser.contents, 'branch-summary')
1014 >>> summary = get_summary(browser)
1015 >>> print extract_text(summary)
1016- There are no branches for Gnome Applets in Launchpad.
1017+ Launchpad does not know where The Gnome Panel Applets hosts its code.
1018+ There are no branches for Gnome Applets in Launchpad. You can change this by:
1019+ activating code hosting directly on Launchpad. (read more)
1020+ asking Launchpad to mirror a Bazaar branch hosted elsewhere. (read more)
1021+ asking Launchpad to import code from Git, Subversion, or CVS into a
1022+ Bazaar branch. (read more)
1023+ Getting started with code hosting in Launchpad.
1024+
1025+
1026 If there are Bazaar branches of Gnome Applets in a publicly
1027 accessible location, Launchpad can act as a mirror of the branch
1028 by registering a Mirrored branch. Read more.
1029 Launchpad can also act as a primary location for Bazaar branches of
1030 Gnome Applets. Read more.
1031 Launchpad can import code from CVS, Subversion, Mercurial or Git
1032- into Bazaar branches. Read more.
1033+ into Bazaar branches. Read more...
1034
1035 The 'Read more' links go to the help wiki.
1036
1037 >>> for anchor in summary.fetch('a'):
1038 ... print anchor['href']
1039+ https://help.launchpad.net/Code/UploadingABranch
1040 https://help.launchpad.net/Code/MirroredBranches
1041- https://help.launchpad.net/Code/UploadingABranch
1042 https://help.launchpad.net/VcsImports
1043+ https://help.launchpad.net/Code
1044
1045
1046 Link to the product downloads
1047@@ -63,6 +72,7 @@
1048
1049 >>> browser.open('http://code.launchpad.dev/netapplet')
1050 >>> print extract_text(get_summary(browser))
1051+ Launchpad does not know where Network Applet hosts its code...
1052 There are no branches for NetApplet in Launchpad.
1053 ...
1054 There are download files available for NetApplet.
1055@@ -83,8 +93,6 @@
1056 >>> browser.open('http://code.launchpad.dev/evolution')
1057 >>> summary = get_summary(browser)
1058 >>> print extract_text(get_summary(browser))
1059- 3 active branches ...
1060- 0 active reviews or unmerged proposals
1061 You can get a copy of the development focus branch using the
1062 command:
1063 bzr branch lp://dev/evolution
1064@@ -124,7 +132,8 @@
1065 Firstly lets associate release--0.9.1 with the 1.0 series.
1066
1067 >>> admin_browser.open('http://launchpad.dev/firefox/1.0/+linkbranch')
1068- >>> admin_browser.getControl('Branch').value = '~mark/firefox/release--0.9.1'
1069+ >>> admin_browser.getControl('Branch').value = (
1070+ ... '~mark/firefox/release--0.9.1')
1071 >>> admin_browser.getControl('Update').click()
1072
1073 >>> browser.open('http://code.launchpad.dev/firefox')
1074@@ -155,32 +164,78 @@
1075 lp://dev/~mark/firefox/release-0.8 Development ...
1076
1077
1078-Floating buttons
1079-================
1080+Involvement portlet
1081+===================
1082
1083-There are two buttons that show on the right hand side of the screen
1084-for project branch listings. 'Register a branch' and 'Import a branch'.
1085+There are several links in the side portlet: 'Register a branch',
1086+'Import a branch', 'Configure code hosting', and 'Define branch
1087+visibility'. The links are only shown if the user has permission to
1088+perform the task.
1089
1090 >>> from zope.component import getUtility
1091 >>> from lp.registry.interfaces.product import IProductSet
1092- >>> login('admin@canonical.com')
1093+ >>> login(ANONYMOUS)
1094 >>> product = getUtility(IProductSet).getByName('firefox')
1095 >>> old_branch = product.development_focus.branch
1096+ >>> login_person(product.owner)
1097 >>> product.development_focus.branch = None
1098 >>> logout()
1099 >>> def print_links(browser):
1100- ... links = find_tag_by_id(browser.contents, 'floating-links')
1101+ ... links = find_tag_by_id(browser.contents, 'involvement')
1102+ ... if links is None:
1103+ ... print 'None'
1104+ ... return
1105 ... for link in links.findAll('a'):
1106 ... print extract_text(link)
1107- >>> browser.open('http://code.launchpad.dev/firefox')
1108- >>> print_links(browser)
1109+
1110+ >>> def setup_code_hosting(productname):
1111+ ... admin_browser.open('http://code.launchpad.dev/%s' % productname)
1112+ ... admin_browser.getLink('Configure code hosting').click()
1113+ ... admin_browser.getControl(
1114+ ... 'Create a new, empty branch').click()
1115+ ... admin_browser.getControl('Branch name').value = 'trunk'
1116+ ... admin_browser.getControl('Update').click()
1117+
1118+The involvement portlet is not shown if the product does not have code
1119+hosting configured or if it is not using Launchpad.
1120+
1121+ >>> print product.codehosting_usage.name
1122+ UNKNOWN
1123+ >>> admin_browser.open('http://code.launchpad.dev/firefox')
1124+ >>> print_links(admin_browser)
1125+ None
1126+
1127+ >>> setup_code_hosting('firefox')
1128+ >>> print product.codehosting_usage.name
1129+ LAUNCHPAD
1130+ >>> admin_browser.open('http://code.launchpad.dev/firefox')
1131+ >>> print_links(admin_browser)
1132+ Register a branch
1133+ Import a branch
1134+ Configure code hosting
1135+ Define branch visibility
1136+
1137+The owner of the project sees the links for the activities he can
1138+perform, everything except defining branch visibility.
1139+
1140+ >>> owner_browser = setupBrowser(auth='Basic test@canonical.com:test')
1141+ >>> owner_browser.open('http://code.launchpad.dev/firefox')
1142+ >>> print_links(owner_browser)
1143+ Register a branch
1144+ Import a branch
1145+ Configure code hosting
1146+
1147+And a regular user can only register and import branches.
1148+
1149+ >>> user_browser.open('http://code.launchpad.dev/firefox')
1150+ >>> print_links(user_browser)
1151 Register a branch
1152 Import a branch
1153
1154 If the product specifies that it officially uses Launchpad code, then
1155 the 'Import a branch' button is still shown.
1156
1157- >>> login('admin@canonical.com')
1158+ >>> login_person(product.owner)
1159 >>> product.development_focus.branch = old_branch
1160 >>> logout()
1161 >>> browser.open('http://code.launchpad.dev/firefox')
1162@@ -189,34 +244,54 @@
1163 Import a branch
1164
1165
1166-Nice wording of summary numbers
1167-===============================
1168+The statistics portlet
1169+======================
1170
1171 The text that is shown giving a summary of the number of branches
1172 shows correct singular and plural forms.
1173
1174- >>> def print_summary(product):
1175+ >>> def get_stats_portlet(browser):
1176+ ... return find_tag_by_id(
1177+ ... browser.contents,
1178+ ... 'portlet-product-codestatistics')
1179+ >>> def print_portlet(product):
1180 ... browser.open('http://code.launchpad.dev/%s' % product)
1181- ... print extract_text(get_summary(browser))
1182-
1183- >>> print_summary('gnome-terminal')
1184- 8 active branches owned by 1 person and 2 teams, 0 commits in the last month
1185- ...
1186+ ... portlet = get_stats_portlet(browser)
1187+ ... if portlet is None:
1188+ ... print 'None'
1189+ ... else:
1190+ ... print extract_text(portlet)
1191+
1192+ >>> setup_code_hosting('gnome-terminal')
1193+ >>> print_portlet('gnome-terminal')
1194+ 0 Active reviews
1195+ 9 Active branches owned by 2 people and 2 teams
1196+ 0 Commits in the last month
1197+
1198 >>> from lp.testing import ANONYMOUS, login, logout
1199 >>> login(ANONYMOUS)
1200 >>> fooix = factory.makeProduct('fooix')
1201 >>> ignored = factory.makeProductBranch(fooix)
1202- >>> ignored = factory.makeProductBranch(fooix)
1203- >>> logout()
1204- >>> print_summary('fooix')
1205- 2 active branches owned by 2 people, 0 commits in the last month
1206- ...
1207- >>> print_summary('evolution')
1208- 3 active branches owned by 1 person and 1 team, 0 commits in the last month
1209- ...
1210- >>> print_summary('iso-codes')
1211- 1 active branch owned by 1 person, 0 commits in the last month
1212- ...
1213+ >>> logout()
1214+ >>> setup_code_hosting('fooix')
1215+ >>> print_portlet('fooix')
1216+ 0 Active reviews
1217+ 2 Active branches owned by 2 people
1218+ 0 Commits in the last month
1219+
1220+ >>> print_portlet('evolution')
1221+ 0 Active reviews
1222+ 3 Active branches owned by 1 person and 1 team
1223+ 0 Commits in the last month
1224+
1225+ >>> login(ANONYMOUS)
1226+ >>> dinky = factory.makeProduct('dinky')
1227+ >>> logout()
1228+ >>> setup_code_hosting('dinky')
1229+ >>> print_portlet('dinky')
1230+ 0 Active reviews
1231+ 1 Active branch owned by 1 person
1232+ 0 Commits in the last month
1233
1234
1235 Product has Branches, but none initially visible
1236@@ -224,17 +299,16 @@
1237
1238 It is a bit of an edge case, but if there are branches for a product but all
1239 of them are either merged or abandoned and there is no development focus
1240-branch, then they will not appear on the initial branch listing.
1241+branch, then they will not appear on the initial branch listing and
1242+the portlets will not be shown.
1243
1244 >>> admin_browser.open('http://code.launchpad.dev/~carlos/iso-codes/0.35')
1245 >>> admin_browser.getLink('Change branch details').click()
1246 >>> admin_browser.getControl('Abandoned').click()
1247 >>> admin_browser.getControl('Change Branch').click()
1248
1249- >>> browser.open('http://code.launchpad.dev/iso-codes')
1250- >>> print extract_text(get_summary(browser))
1251- 0 active branches, 0 commits in the last month
1252- 0 active reviews or unmerged proposals
1253+ >>> print_portlet('iso-codes')
1254+ None
1255
1256 >>> message = find_tag_by_id(browser.contents, 'no-branch-message')
1257 >>> print extract_text(message)
1258
1259=== modified file 'lib/lp/code/stories/codeimport/xx-create-codeimport.txt'
1260--- lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2010-08-14 16:39:07 +0000
1261+++ lib/lp/code/stories/codeimport/xx-create-codeimport.txt 2010-09-28 22:26:02 +0000
1262@@ -28,7 +28,27 @@
1263
1264 >>> browser.open('http://code.launchpad.dev/firefox')
1265 >>> browser.getLink('Import a branch').click()
1266-
1267+ Traceback (most recent call last):
1268+ ...
1269+ LinkNotFoundError
1270+
1271+The owner can configure code hosting for the project and then
1272+importing will be available to any user.
1273+
1274+ >>> owner_browser = setupBrowser(auth="Basic test@canonical.com:test")
1275+ >>> owner_browser.open('http://code.launchpad.dev/firefox')
1276+ >>> owner_browser.getLink('Configure code hosting').click()
1277+ >>> owner_browser.getControl(
1278+ ... 'Import a branch').click()
1279+ >>> owner_browser.getControl('Branch URL').value = 'git://example.com/firefox'
1280+ >>> owner_browser.getControl('Git').click()
1281+ >>> owner_browser.getControl('Branch name').value = 'trunk'
1282+ >>> owner_browser.getControl('Update').click()
1283+
1284+Now a regular user can import another branch.
1285+
1286+ >>> browser.open('http://code.launchpad.dev/firefox')
1287+ >>> browser.getLink('Import a branch').click()
1288
1289 Requesting a Subversion import
1290 ==============================
1291
1292=== modified file 'lib/lp/code/templates/branch-listing.pt'
1293--- lib/lp/code/templates/branch-listing.pt 2010-04-11 22:45:09 +0000
1294+++ lib/lp/code/templates/branch-listing.pt 2010-09-28 22:26:02 +0000
1295@@ -74,16 +74,16 @@
1296 </tal:ignore>
1297 <tr tal:condition="product/required:launchpad.Edit"
1298 tal:define="edit_link product/development_focus/fmt:url/+linkbranchtoseries">
1299- <td colspan="5" class="branch-no-dev-focus">A development focus branch hasn't
1300+ <td colspan="5" class="branch-no-dev-focus">A development focus branch hasn't
1301 been specified, <a tal:attributes="href edit_link">set it now</a>.</td>
1302 </tr>
1303 </tal:missing-dev-focus>
1304 </tal:allow-setting-dev-focus>
1305 <tr tal:repeat="branch context/branches">
1306 <td>
1307- <img src="/@@/branch" /> <a tal:attributes="href branch/fmt:url"
1308- tal:content="branch/bzr_identity">
1309- Name
1310+ <a tal:attributes="href branch/fmt:url"
1311+ tal:content="structure branch/bzr_identity/fmt:break-long-words"
1312+ class="sprite branch">Name
1313 </a>
1314 <tal:associated-series repeat="series branch/active_series"
1315 condition="context/view/show_series_links">
1316
1317=== modified file 'lib/lp/code/templates/product-branch-summary.pt'
1318--- lib/lp/code/templates/product-branch-summary.pt 2010-08-13 16:09:45 +0000
1319+++ lib/lp/code/templates/product-branch-summary.pt 2010-09-28 22:26:02 +0000
1320@@ -7,42 +7,59 @@
1321 lang="en"
1322 dir="ltr"
1323 i18n:domain="launchpad"
1324- id="application-summary">
1325+ id="branch-summary">
1326+
1327+ <div id="unknown" tal:condition="context/codehosting_usage/enumvalue:UNKNOWN">
1328+ <p>
1329+ <strong>
1330+ Launchpad does not know where <tal:project_title replace="context/title" />
1331+ hosts its code.
1332+ </strong>
1333+ </p>
1334+ </div>
1335+
1336+ <div id="external"
1337+ tal:condition="context/codehosting_usage/enumvalue:EXTERNAL">
1338+ <p>
1339+ <strong>
1340+ <tal:project_title replace="context/title" /> hosts its code at
1341+ <a tal:attributes="href view/mirror_location"
1342+ tal:content="view/mirror_location"></a>.
1343+ </strong>
1344+ </p>
1345+ <p tal:condition="context/homepageurl">
1346+ You can learn more at the project's
1347+ <a tal:attributes="href context/homepageurl">web page</a>.
1348+ </p>
1349+ <p tal:condition="view/branch/branch_type/enumvalue:MIRRORED">
1350+ Launchpad has a mirror of the master branch and you can create branches
1351+ from it.
1352+ </p>
1353+ <p tal:condition="view/branch/branch_type/enumvalue:REMOTE">
1354+ Launchpad does not have a copy of the remote branch.
1355+ </p>
1356+ </div>
1357
1358 <tal:no-branches condition="not: view/branch_count">
1359 There are no branches for <tal:project-name replace="context/displayname"/>
1360- in Launchpad.
1361- <ul>
1362- <li>If there are Bazaar branches of
1363- <tal:project-name replace="context/displayname"/>
1364- in a publicly accessible location,
1365- Launchpad can act as a mirror of the branch by registering a
1366- <em>Mirrored</em> branch.
1367- <a href="https://help.launchpad.net/Code/MirroredBranches">Read more.</a>
1368- </li>
1369- <li>Launchpad can also act as a primary
1370- location for Bazaar branches of
1371- <tal:project-name replace="context/displayname"/>.
1372- <a href="https://help.launchpad.net/Code/UploadingABranch">Read more.</a>
1373- </li>
1374-
1375- <li>Launchpad can import code from CVS, Subversion, Mercurial or Git
1376- into Bazaar branches. <a
1377- href="https://help.launchpad.net/VcsImports">Read more.</a> </li>
1378-
1379+ in Launchpad. You can change this by:
1380+
1381+ <ul class="bulleted" style="margin-top:1em;">
1382+
1383+ <li>activating code hosting directly on
1384+ Launchpad. (<a href="https://help.launchpad.net/Code/UploadingABranch">read
1385+ more</a>)</li>
1386+
1387+ <li>asking Launchpad to mirror a Bazaar branch hosted
1388+ elsewhere. (<a href="https://help.launchpad.net/Code/MirroredBranches">read
1389+ more</a>)</li>
1390+
1391+ <li>asking Launchpad to import code from Git, Subversion, or CVS into a
1392+ Bazaar branch. (<a href="https://help.launchpad.net/VcsImports">read more</a>)</li>
1393 </ul>
1394 </tal:no-branches>
1395
1396 <tal:has-branches condition="view/branch_count">
1397- <p tal:replace="structure context/@@+count-summary"/>
1398- <p id="merge-counts"
1399- tal:define="menu context/menu:branches">
1400- <strong class="count" tal:content="menu/active_review_count">5</strong>
1401- <tal:link
1402- define="link menu/active_reviews"
1403- replace="structure link/render"
1404- />
1405- </p>
1406 <div tal:condition="view/has_development_focus_branch"
1407 style="margin: 1em 0"
1408 tal:define="config modules/canonical.config/config;
1409@@ -60,6 +77,30 @@
1410 </div>
1411
1412 </tal:has-branches>
1413+
1414+ <div tal:condition="context/codehosting_usage/enumvalue:UNKNOWN">
1415+ <div
1416+ tal:condition="not: context/codehosting_usage/enumvalue:LAUNCHPAD"
1417+ tal:define="configure_codehosting view/configure_codehosting |
1418+ nothing">
1419+ <p style="margin-top: 10px;">
1420+ <a class="sprite maybe"
1421+ href="https://help.launchpad.net/Code">Getting started
1422+ with code hosting in Launchpad</a>.</p>
1423+
1424+ <p tal:condition="context/required:launchpad.Edit"
1425+ id="no-code-edit"
1426+ >
1427+ <a tal:condition="configure_codehosting"
1428+ tal:replace="structure configure_codehosting/fmt:link"/>
1429+ </p>
1430+ <p tal:define="menu context/menu:branches;
1431+ link menu/branch_visibility"
1432+ tal:condition="link/enabled"
1433+ tal:content="structure link/render"></p>
1434+ </div>
1435+ </div>
1436+
1437 <p tal:condition="view/latest_release_with_download_files">
1438 <img src="/@@/download"/> There are
1439 <a tal:define="rooturl modules/canonical.launchpad.webapp.vhosts/allvhosts/configs/mainsite/rooturl"
1440
1441=== modified file 'lib/lp/code/templates/product-branches.pt'
1442--- lib/lp/code/templates/product-branches.pt 2009-09-17 00:27:40 +0000
1443+++ lib/lp/code/templates/product-branches.pt 2010-09-28 22:26:02 +0000
1444@@ -3,40 +3,66 @@
1445 xmlns:tal="http://xml.zope.org/namespaces/tal"
1446 xmlns:metal="http://xml.zope.org/namespaces/metal"
1447 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1448- metal:use-macro="view/macro:page/main_only"
1449+ metal:use-macro="view/macro:page/main_side"
1450 i18n:domain="launchpad"
1451 >
1452
1453 <body>
1454
1455-<div metal:fill-slot="main">
1456-
1457- <div style="float:right" id="floating-links"
1458- tal:define="menu context/menu:branches">
1459- <div tal:define="link menu/branch_add"
1460- tal:condition="link/enabled"
1461- tal:content="structure link/render" />
1462- <div tal:define="link menu/code_import"
1463- tal:condition="link/enabled"
1464- tal:content="structure link/render" />
1465- <div tal:define="link menu/branch_visibility"
1466- tal:condition="link/enabled"
1467- tal:content="structure link/render" />
1468- </div>
1469-
1470- <div id="private-policy" tal:condition="view/new_branches_are_private"
1471- class="informational message">
1472- New branches you create for <tal:name replace="context/displayname"/>
1473- are <strong>private</strong> initially.
1474- </div>
1475-
1476- <tal:branch-summary content="structure context/@@+branch-summary" />
1477-
1478- <tal:has-branches condition="view/branch_count"
1479- define="branches view/branches">
1480- <tal:branchlisting content="structure branches/@@+branch-listing" />
1481- </tal:has-branches>
1482-</div>
1483+ <metal:side fill-slot="side" tal:define="context_menu context/menu:context">
1484+ <div id="branch-portlet"
1485+ tal:condition="not: context/codehosting_usage/enumvalue:UNKNOWN">
1486+ <div id="privacy"
1487+ tal:define="are_private view/new_branches_are_private"
1488+ tal:attributes="class python: are_private and 'first portlet private' or 'first portlet public'">
1489+
1490+ <p tal:condition="not:view/new_branches_are_private" id="privacy-text">
1491+ New branches you create for <tal:name replace="context/displayname"/>
1492+ are <strong>public</strong> initially.
1493+ </p>
1494+
1495+ <p tal:condition="view/new_branches_are_private" id="privacy-text">
1496+ New branches you create for <tal:name replace="context/displayname"/>
1497+ are <strong>private</strong> initially.
1498+ </p>
1499+
1500+ </div>
1501+
1502+ <div id="involvement" class="portlet"
1503+ tal:define="menu context/menu:branches">
1504+ <ul class="involvement">
1505+ <li style="border: none">
1506+ <a href="+addbranch" class="menu-link-addbranch sprite code">
1507+ Register a branch
1508+ </a>
1509+ </li>
1510+ </ul>
1511+ <p style="margin-top:10px;"
1512+ tal:define="link menu/code_import"
1513+ tal:condition="link/enabled"
1514+ tal:content="structure link/render"></p>
1515+ <p tal:define="configure_codehosting view/configure_codehosting | nothing"
1516+ tal:condition="configure_codehosting"
1517+ tal:replace="structure configure_codehosting/fmt:link"></p>
1518+ <p tal:define="link menu/branch_visibility"
1519+ tal:condition="link/enabled"
1520+ tal:content="structure link/render"></p>
1521+ </div>
1522+
1523+ <div tal:replace="structure context/@@+portlet-product-codestatistics" />
1524+ </div>
1525+ </metal:side>
1526+
1527+ <tal:main metal:fill-slot="main">
1528+
1529+ <tal:branch-summary content="structure context/@@+branch-summary" />
1530+
1531+ <tal:has-branches condition="view/branch_count"
1532+ define="branches view/branches">
1533+ <tal:branchlisting content="structure branches/@@+branch-listing" />
1534+ </tal:has-branches>
1535+
1536+ </tal:main>
1537
1538 </body>
1539 </html>
1540
1541=== added file 'lib/lp/code/templates/product-portlet-codestatistics-content.pt'
1542--- lib/lp/code/templates/product-portlet-codestatistics-content.pt 1970-01-01 00:00:00 +0000
1543+++ lib/lp/code/templates/product-portlet-codestatistics-content.pt 2010-09-28 22:26:02 +0000
1544@@ -0,0 +1,55 @@
1545+<tal:portlet-product-codestatistics-content
1546+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1547+ xmlns:metal="http://xml.zope.org/namespaces/metal">
1548+
1549+ <tr tal:define="menu context/menu:branches" class="code-links"
1550+ id="merge-counts">
1551+ <td class="code-count"
1552+ tal:define="count menu/active_review_count"
1553+ tal:content="count" />
1554+ <td>
1555+ <tal:link
1556+ define="link menu/active_reviews"
1557+ replace="structure link/render"
1558+ />
1559+ </td>
1560+ </tr>
1561+
1562+ <tr class="code-links" id="branch-count-summary">
1563+ <td class="code-count"
1564+ tal:define="count view/branch_count"
1565+ tal:content="count" />
1566+ <td>
1567+ <tal:branches replace="view/branch_text">branches</tal:branches
1568+ ><tal:has-branches condition="view/branch_count">
1569+ owned by
1570+ <tal:individuals condition="view/person_owner_count">
1571+ <tal:owners content="view/person_owner_count">42</tal:owners>
1572+ <tal:people replace="view/person_text">people</tal:people
1573+ ></tal:individuals
1574+ ><tal:teams condition="view/team_owner_count">
1575+ <tal:individuals condition="view/person_owner_count">
1576+ and
1577+ </tal:individuals>
1578+ <tal:toc content="view/team_owner_count">1</tal:toc>
1579+ <tal:people replace="view/team_text">team</tal:people
1580+ ></tal:teams></tal:has-branches>
1581+ </td>
1582+ </tr>
1583+
1584+ <tr class="code-links">
1585+ <td class="code-count"
1586+ tal:define="count view/commit_count"
1587+ tal:content="count" />
1588+ <td>
1589+ <tal:commits replace="view/commit_text">commits</tal:commits>
1590+ <tal:has-committers condition="view/committer_count">
1591+ by
1592+ <tal:cc content="view/committer_count">4</tal:cc>
1593+ <tal:people replace="view/committer_text">people</tal:people>
1594+ </tal:has-committers>
1595+ in the last month
1596+ </td>
1597+ </tr>
1598+
1599+</tal:portlet-product-codestatistics-content>
1600
1601=== added file 'lib/lp/code/templates/product-portlet-codestatistics.pt'
1602--- lib/lp/code/templates/product-portlet-codestatistics.pt 1970-01-01 00:00:00 +0000
1603+++ lib/lp/code/templates/product-portlet-codestatistics.pt 2010-09-28 22:26:02 +0000
1604@@ -0,0 +1,11 @@
1605+<div
1606+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1607+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1608+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1609+ class="portlet" id="portlet-product-codestatistics">
1610+
1611+ <table class="code-links">
1612+ <tbody id="portlet-product-codestatistics"
1613+ tal:content="structure context/@@+portlet-product-codestatistics-content" />
1614+ </table>
1615+</div>
1616
1617=== modified file 'lib/lp/registry/browser/product.py'
1618--- lib/lp/registry/browser/product.py 2010-09-28 14:58:40 +0000
1619+++ lib/lp/registry/browser/product.py 2010-09-28 22:26:02 +0000
1620@@ -458,9 +458,7 @@
1621 # Add the branch configuration in separately.
1622 set_branch = series_menu['set_branch']
1623 set_branch.text = 'Configure project branch'
1624- set_branch.summary = "Specify the location of this projects code."
1625- set_branch.configured = (
1626- )
1627+ set_branch.summary = "Specify the location of this project's code."
1628 config_list.append(
1629 dict(link=set_branch,
1630 configured=config_statuses['configure_codehosting']))
1631@@ -469,11 +467,8 @@
1632 @property
1633 def registration_completeness(self):
1634 """The percent complete for registration."""
1635- configured = 0
1636 config_statuses = self.configuration_states
1637- for key, value in config_statuses.items():
1638- if value:
1639- configured += 1
1640+ configured = sum(1 for val in config_statuses.values() if val)
1641 scale = 100
1642 done = int(float(configured) / len(config_statuses) * scale)
1643 undone = scale - done
1644@@ -1396,9 +1391,9 @@
1645 def setUpFields(self):
1646 super(ProductConfigureBase, self).setUpFields()
1647 if self.usage_fieldname is not None:
1648- # The usage fields are shared among pillars. But when referring to
1649- # an individual object in Launchpad it is better to call it by its
1650- # real name, i.e. 'project' instead of 'pillar'.
1651+ # The usage fields are shared among pillars. But when referring
1652+ # to an individual object in Launchpad it is better to call it by
1653+ # its real name, i.e. 'project' instead of 'pillar'.
1654 usage_field = self.form_fields.get(self.usage_fieldname)
1655 if usage_field:
1656 usage_field.custom_widget = CustomWidgetFactory(
1657
1658=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
1659--- lib/lp/registry/browser/tests/pillar-views.txt 2010-09-25 14:29:32 +0000
1660+++ lib/lp/registry/browser/tests/pillar-views.txt 2010-09-28 22:26:02 +0000
1661@@ -182,6 +182,17 @@
1662 >>> print view.codehosting_usage.name
1663 LAUNCHPAD
1664
1665+ >>> from lp.code.enums import BranchType
1666+ >>> remote = factory.makeProduct()
1667+ >>> branch = factory.makeProductBranch(product=remote,
1668+ ... branch_type=BranchType.REMOTE)
1669+ >>> remote.official_codehosting
1670+ False
1671+ >>> view = create_view(remote, '+get-involved')
1672+ >>> print view.codehosting_usage.name
1673+ UNKNOWN
1674+
1675+
1676 Project groups cannot make links to register a branch, so
1677 official_codehosting is always false.
1678
1679
1680=== modified file 'lib/lp/registry/model/product.py'
1681--- lib/lp/registry/model/product.py 2010-09-27 18:16:28 +0000
1682+++ lib/lp/registry/model/product.py 2010-09-28 22:26:02 +0000
1683@@ -404,7 +404,8 @@
1684 return ServiceUsage.UNKNOWN
1685 elif self.development_focus.branch.branch_type == BranchType.HOSTED:
1686 return ServiceUsage.LAUNCHPAD
1687- elif self.development_focus.branch.branch_type == BranchType.MIRRORED:
1688+ elif self.development_focus.branch.branch_type in (
1689+ BranchType.MIRRORED, BranchType.REMOTE):
1690 return ServiceUsage.EXTERNAL
1691 return ServiceUsage.NOT_APPLICABLE
1692
1693
1694=== modified file 'lib/lp/registry/tests/test_service_usage.py'
1695--- lib/lp/registry/tests/test_service_usage.py 2010-09-22 00:52:15 +0000
1696+++ lib/lp/registry/tests/test_service_usage.py 2010-09-28 22:26:02 +0000
1697@@ -8,6 +8,7 @@
1698 from canonical.testing import DatabaseFunctionalLayer
1699
1700 from lp.app.enums import ServiceUsage
1701+from lp.code.enums import BranchType
1702 from lp.testing import (
1703 login_person,
1704 TestCaseWithFactory,
1705@@ -56,13 +57,6 @@
1706 True,
1707 self.target.official_answers)
1708
1709- def test_codehosting_usage(self):
1710- # Only test get for codehosting; this has no setter because the
1711- # state is derived from other data.
1712- self.assertEqual(
1713- ServiceUsage.UNKNOWN,
1714- self.target.codehosting_usage)
1715-
1716 def test_translations_usage_no_data(self):
1717 # By default, we don't know anything about a target
1718 self.assertEqual(
1719@@ -195,6 +189,42 @@
1720 super(TestProductUsageEnums, self).setUp()
1721 self.target = self.factory.makeProduct()
1722
1723+ def test_codehosting_unknown(self):
1724+ # A default product has UNKNOWN usage.
1725+ self.assertEqual(
1726+ ServiceUsage.UNKNOWN,
1727+ self.target.codehosting_usage)
1728+
1729+ def test_codehosting_mirrored_branch(self):
1730+ # A mirrored branch is EXTERNAL.
1731+ login_person(self.target.owner)
1732+ self.target.development_focus.branch = self.factory.makeProductBranch(
1733+ product=self.target,
1734+ branch_type=BranchType.MIRRORED)
1735+ self.assertEqual(
1736+ ServiceUsage.EXTERNAL,
1737+ self.target.codehosting_usage)
1738+
1739+ def test_codehosting_remote_branch(self):
1740+ # A remote branch is EXTERNAL.
1741+ login_person(self.target.owner)
1742+ self.target.development_focus.branch = self.factory.makeProductBranch(
1743+ product=self.target,
1744+ branch_type=BranchType.REMOTE)
1745+ self.assertEqual(
1746+ ServiceUsage.EXTERNAL,
1747+ self.target.codehosting_usage)
1748+
1749+ def test_codehosting_hosted_branch(self):
1750+ # A branch on Launchpad is HOSTED.
1751+ login_person(self.target.owner)
1752+ self.target.development_focus.branch = self.factory.makeProductBranch(
1753+ product=self.target,
1754+ branch_type=BranchType.HOSTED)
1755+ self.assertEqual(
1756+ ServiceUsage.LAUNCHPAD,
1757+ self.target.codehosting_usage)
1758+
1759
1760 class TestProductSeriesUsageEnums(
1761 TestCaseWithFactory,