Merge lp:~sinzui/launchpad/related-software-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 11134
Proposed branch: lp:~sinzui/launchpad/related-software-0
Merge into: lp:launchpad
Diff against target: 2416 lines (+608/-519)
19 files modified
configs/development/launchpad-lazr.conf (+1/-0)
lib/canonical/config/schema-lazr.conf (+3/-0)
lib/lp/registry/browser/branding.py (+1/-1)
lib/lp/registry/browser/person.py (+20/-16)
lib/lp/registry/browser/tests/person-karma-views.txt (+1/-1)
lib/lp/registry/browser/tests/person-views.txt (+187/-198)
lib/lp/registry/browser/tests/test_branding.py (+33/-0)
lib/lp/registry/browser/tests/test_person_view.py (+147/-1)
lib/lp/registry/browser/tests/user-to-user-views.txt (+93/-66)
lib/lp/registry/doc/person-account.txt (+9/-5)
lib/lp/registry/doc/person.txt (+9/-12)
lib/lp/registry/interfaces/person.py (+2/-3)
lib/lp/registry/model/person.py (+59/-81)
lib/lp/registry/stories/person/xx-person-packages.txt (+0/-16)
lib/lp/registry/stories/person/xx-person-projects.txt (+8/-7)
lib/lp/registry/stories/person/xx-user-to-user.txt (+1/-79)
lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt (+14/-8)
lib/lp/registry/templates/person-related-software.pt (+3/-3)
lib/lp/soyuz/stories/soyuz/xx-person-packages.txt (+17/-22)
To merge this branch: bzr merge lp:~sinzui/launchpad/related-software-0
Reviewer Review Type Date Requested Status
Brad Crittenden (community) Approve
Review via email: mp+29621@code.launchpad.net

Description of the change

This is my branch to prevent timeouts on +related-software. The page title
distract inflated the real work of the branch, sorry.

    lp:~sinzui/launchpad/related-software-0
    Diff size: 784
    Launchpad bug:
          https://bugs.launchpad.net/bugs/592555
          https://bugs.launchpad.net/bugs/251670
          https://bugs.launchpad.net/bugs/236683
          https://bugs.launchpad.net/bugs/516491
    Test command: ./bin/test -vv \
           -t person-karma-views -t person-views.txt -t test_person_view \
           -t test_branding -t user-to-user-views -t doc/person \
           -t xx-person-projects -t xx-person-packages
    Pre-implementation: no one.
    Target release: 10.08

Prevent timeouts on +related-software
-------------------------------------

Bug #592555 [Timeout oops on related_projects]
    The timeouts are cause by a tremendous number of queries. Looking at
    the page, it appears to be showing more content than intended. This
    is a summary page, but it includes the entire content of what it
    summarises. Each block should be limited to the recent 10 and provide
    a link to see all the data. The related projects content is bad because
    it contains the project owned and driven by teams, which can be in
    the hundreds, where user really own dozens.

Bug #251670 [My "related software" page shows irrelevant projects]
    The listing includes pillars owned or driven by the user's teams.

Bug #236683 ["Related projects" section on a user's page doesn't seem to
    be sorted]
    It is sorted by pillar kind, which is random to 99% of users.

Bug #516491 [User Name shown twice in 'Software Related', 'Launchpad Karma',
    'Change Branding' and 'Contact This User' Pages title]
    The user name name is already in the page title and bread crumbs, but the
    view uses the user name in the page_title attribute

Rules
-----

Bug #592555 [Timeout oops on related_projects]
    * Limit each section to 10 on +related-software

Bug #251670 [My "related software" page shows irrelevant projects]
    * Limit the pillars to those that are directly owned or driven by
      the user.

Bug #236683 ["Related projects" section on a user's page doesn't seem to
    be sorted]
    * Sort by kind, then by displayname.

Bug #516491 [User Name shown twice in 'Software Related', 'Launchpad Karma',
    'Change Branding' and 'Contact This User' Pages title]
    * The users suggestions are correct--just use the short title that is
      used in the link name
    * This also affects all the +related software/package pages.

QA
--

Bug #592555 [Timeout oops on related_projects]
    * Visit https://edge.launchpad.net/~vorlon/+related-software
    * Verify it does not timeout
    * Visit https://edge.launchpad.net/~kees/+related-software
    * Verify it does not timeout

Bug #251670 [My "related software" page shows irrelevant projects]
    * https://edge.launchpad.net/~sinzui/+related-projects
      or your own +related-projects page
    * Verify it lists project the user owns and drives
    * Verify it does not list The Fink Project.

Bug #236683 ["Related projects" section on a user's page doesn't seem to
    be sorted]
    * https://edge.launchpad.net/~sinzui/+related-projects
    * Verify that Gedit developer plugins appears before pocket lint.

Bug #516491 [User Name shown twice in 'Software Related', 'Launchpad Karma',
    'Change Branding' and 'Contact This User' Pages title]
    * Visit https://edge.launchpad.net/~kees/+related-software
    * Verify the bread crumbs end with >> Related software
    * Choose Related PPA packages
    * Verify the bread crumbs end with >> Related PPA packages
    * Choose Uploaded packages
    * Verify the bread crumbs end with >> Uploaded packages
    * Choose Maintained packages
    * Verify the bread crumbs end with >> Maintained packages
    * Visit https://edge.launchpad.net/~kees/+karma
    * Verify the heading is Launchpad karma
    * https://edge.launchpad.net/~sinzui/+contactuser
    * Verify the bread crumbs end with >> Contact user
    * https://edge.launchpad.net/people/+me/+branding
    * Verify the bread crumbs end with >> Change branding

Lint
----

Linting changed files:
  configs/development/launchpad-lazr.conf
  lib/canonical/config/schema-lazr.conf
  lib/lp/registry/browser/branding.py
  lib/lp/registry/browser/person.py
  lib/lp/registry/browser/tests/person-karma-views.txt
  lib/lp/registry/browser/tests/person-views.txt
  lib/lp/registry/browser/tests/test_branding.py
  lib/lp/registry/browser/tests/test_person_view.py
  lib/lp/registry/browser/tests/user-to-user-views.txt
  lib/lp/registry/doc/person.txt
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/model/person.py
  lib/lp/registry/stories/person/xx-person-projects.txt
  lib/lp/registry/templates/person-related-software.pt
  lib/lp/soyuz/stories/soyuz/xx-person-packages.txt

^ the new make lint correctly reports a lot of bad doctest headings whitespace
  issues in ./lib/lp/registry/interfaces/person.py. I will fix these
  before I land. They will create about 300 lines of extra diff.

Test
----

Removed lib/lp/registry/stories/person/xx-person-packages.txt because it
was 100% identical to the start of the soyuz counterpart.

    * lib/lp/registry/browser/tests/person-karma-views.txt
      * Verify that the label does not repeat the user name.
    * lib/lp/registry/browser/tests/person-views.txt
      * Moved a test into a unit test so that I could extend it to
        verify the expected sizes of listings.
    * lib/lp/registry/browser/tests/test_person_view.py
      * Moved the +related-software test to TestPersonRelatedSoftwareView
        and extend it. Added test for the 4 subclasses.
    * lib/lp/registry/browser/tests/test_branding.py
      * Added a branding test (it is a base class) to verify the expected
        page title and heading.
    * lib/lp/registry/browser/tests/user-to-user-views.txt
      * Verified the user name does not appear twice in the heading and
        bread crumbs.
    * lib/lp/registry/doc/person.txt
      * Verified that getOwnedOrDrivenPillars() only contains pillars
        directly owned by the user. The listing also verifies the sort
        order.
    * lib/lp/registry/stories/person/xx-person-projects.txt
      * Verified the page titles and to sorting of listed projects.
    * lib/lp/soyuz/stories/soyuz/xx-person-packages.txt
      * Removed redundant and non-soyuz test.

Implementation
--------------

    * configs/development/launchpad-lazr.conf
      * Added summary_list_size and set the value to something that is
        visible in development and the testrunner
    * lib/canonical/config/schema-lazr.conf
      * Added summary_list_size to control the size of listings that
        are not intended to be complete.
    * lib/lp/registry/browser/branding.py
      * Gave the branding views a separate page_title to avoid redundant
        names in the page title and bread crumbs.
    * lib/lp/registry/browser/person.py
      * Remove the user name from the page_tittle to avoid redundant
        names in the page title and bread crumbs.
      * Update PersonRelatedSoftwareView (the base view for many views)
        to use summary_list_size so that the page will include a maximum
        of 40 items in production...it is current using 200 items
      * Fixed from bad property names...I could not get the bad names to
        fit in 78 characters in the test.
      * PersonMaintainedPackagesView, PersonUploadedPackagesView,
        PersonPPAPackagesView. PersonRelatedProjectsView, I added
        'default_batch_size' to preserve their rules to show 50 items.
        There are no oops to indicate they time out.
      * ^ removed the redundant user names from the bread crumbs.
    * lib/lp/registry/interfaces/person.py
      * Revised the interface documentation.
    * lib/lp/registry/model/person.py
      * Refactored getOwnedOrDrivenPillars() to only return pillars the
        user directly owns or drives. Stormified the query. Inline the
        name query to avoid doing two queries. Add sorting rules that users
        will understand.
    * lib/lp/registry/templates/person-related-software.pt
      * Updated the template to use the renamed properties.

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

Thanks Curtis.

=== modified file 'lib/canonical/config/schema-lazr.conf'
typo: sumarizes -> summarizes

=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
alphabetize:
from lp.testing.views import create_view, create_initialized_view

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configs/development/launchpad-lazr.conf'
2--- configs/development/launchpad-lazr.conf 2010-07-01 06:31:55 +0000
3+++ configs/development/launchpad-lazr.conf 2010-07-14 21:22:45 +0000
4@@ -139,6 +139,7 @@
5 mugshot_batch_size: 8
6 announcement_batch_size: 4
7 download_batch_size: 4
8+summary_list_size: 5
9 openid_preauthorization_acl:
10 localhost http://launchpad.dev/
11 max_bug_feed_cache_minutes: 30
12
13=== modified file 'lib/canonical/config/schema-lazr.conf'
14--- lib/canonical/config/schema-lazr.conf 2010-07-08 16:52:35 +0000
15+++ lib/canonical/config/schema-lazr.conf 2010-07-14 21:22:45 +0000
16@@ -994,6 +994,9 @@
17 # files. The releases are batched, not the individual files.
18 download_batch_size: 10
19
20+# The default size of a list that summarizes and introduces a larger list.
21+summary_list_size: 10
22+
23 # If restrict_to_team is set (such as on the beta
24 # website), then this indicates the hostname suffix for
25 # the non-restricted version of Launchpad. Replacing
26
27=== modified file 'lib/lp/registry/browser/branding.py'
28--- lib/lp/registry/browser/branding.py 2009-09-03 13:25:04 +0000
29+++ lib/lp/registry/browser/branding.py 2010-07-14 21:22:45 +0000
30@@ -28,7 +28,7 @@
31 return ('Change the images used to represent %s in Launchpad'
32 % self.context.displayname)
33
34- page_title = label
35+ page_title = "Change branding"
36
37 custom_widget('icon', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)
38 custom_widget('logo', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)
39
40=== modified file 'lib/lp/registry/browser/person.py'
41--- lib/lp/registry/browser/person.py 2010-07-13 14:50:47 +0000
42+++ lib/lp/registry/browser/person.py 2010-07-14 21:22:45 +0000
43@@ -2511,7 +2511,7 @@
44 if self.user == self.context:
45 return 'Your Launchpad Karma'
46 else:
47- return 'Launchpad Karma for %s' % self.context.displayname
48+ return 'Launchpad Karma'
49
50 @cachedproperty
51 def has_karma(self):
52@@ -5084,12 +5084,15 @@
53 class PersonRelatedSoftwareView(LaunchpadView):
54 """View for +related-software."""
55 implements(IPersonRelatedSoftwareMenu)
56+ _max_results_key = 'summary_list_size'
57
58- max_results_to_display = config.launchpad.default_batch_size
59+ @property
60+ def max_results_to_display(self):
61+ return config.launchpad[self._max_results_key]
62
63 @property
64 def page_title(self):
65- return "Software related to " + self.context.displayname
66+ return 'Related software'
67
68 @cachedproperty
69 def related_projects(self):
70@@ -5126,12 +5129,12 @@
71 @cachedproperty
72 def first_five_related_projects(self):
73 """Return first five projects owned or driven by this person."""
74- return list(self._related_projects()[:5])
75+ return self._related_projects()[:5]
76
77 @cachedproperty
78 def related_projects_count(self):
79 """The number of project owned or driven by this person."""
80- return self._related_projects().count()
81+ return len(self._related_projects())
82
83 @cachedproperty
84 def has_more_related_projects(self):
85@@ -5204,7 +5207,7 @@
86 return results, header_message
87
88 @property
89- def get_latest_uploaded_ppa_packages_with_stats(self):
90+ def latest_uploaded_ppa_packages_with_stats(self):
91 """Return the sourcepackagereleases uploaded to PPAs by this person.
92
93 Results are filtered according to the permission of the requesting
94@@ -5216,7 +5219,7 @@
95 return self.filterPPAPackageList(results)
96
97 @property
98- def get_latest_maintained_packages_with_stats(self):
99+ def latest_maintained_packages_with_stats(self):
100 """Return the latest maintained packages, including stats."""
101 packages = self.context.getLatestMaintainedPackages()
102 results, header_message = self._getDecoratedPackagesSummary(packages)
103@@ -5224,7 +5227,7 @@
104 return results
105
106 @property
107- def get_latest_uploaded_but_not_maintained_packages_with_stats(self):
108+ def latest_uploaded_but_not_maintained_packages_with_stats(self):
109 """Return the latest uploaded packages, including stats.
110
111 Don't include packages that are maintained by the user.
112@@ -5308,6 +5311,7 @@
113
114 class PersonMaintainedPackagesView(PersonRelatedSoftwareView):
115 """View for +maintained-packages."""
116+ _max_results_key = 'default_batch_size'
117
118 def initialize(self):
119 """Set up the batch navigation."""
120@@ -5316,11 +5320,12 @@
121
122 @property
123 def page_title(self):
124- return "Software maintained by " + self.context.displayname
125+ return "Maintained Packages"
126
127
128 class PersonUploadedPackagesView(PersonRelatedSoftwareView):
129 """View for +uploaded-packages."""
130+ _max_results_key = 'default_batch_size'
131
132 def initialize(self):
133 """Set up the batch navigation."""
134@@ -5329,11 +5334,12 @@
135
136 @property
137 def page_title(self):
138- return "Software uploaded by " + self.context.displayname
139+ return "Uploaded packages"
140
141
142 class PersonPPAPackagesView(PersonRelatedSoftwareView):
143 """View for +ppa-packages."""
144+ _max_results_key = 'default_batch_size'
145
146 def initialize(self):
147 """Set up the batch navigation."""
148@@ -5349,11 +5355,12 @@
149
150 @property
151 def page_title(self):
152- return "PPA packages related to " + self.context.displayname
153+ return "PPA packages"
154
155
156 class PersonRelatedProjectsView(PersonRelatedSoftwareView):
157 """View for +related-projects."""
158+ _max_results_key = 'default_batch_size'
159
160 def initialize(self):
161 """Set up the batch navigation."""
162@@ -5363,7 +5370,7 @@
163
164 @property
165 def page_title(self):
166- return "Projects related to " + self.context.displayname
167+ return "Related projects"
168
169
170 class PersonOAuthTokensView(LaunchpadView):
171@@ -5778,10 +5785,7 @@
172 # Subject and then Message fields.
173 self.form_fields = FormFields(*chain((field, ), self.form_fields))
174
175- @property
176- def label(self):
177- """The form label."""
178- return 'Contact ' + self.context.displayname
179+ label = 'Contact user'
180
181 @cachedproperty
182 def recipients(self):
183
184=== modified file 'lib/lp/registry/browser/tests/person-karma-views.txt'
185--- lib/lp/registry/browser/tests/person-karma-views.txt 2010-01-21 11:55:56 +0000
186+++ lib/lp/registry/browser/tests/person-karma-views.txt 2010-07-14 21:22:45 +0000
187@@ -18,4 +18,4 @@
188 >>> neil = factory.makePerson(name='neil', displayname='Neil Peart')
189 >>> view = create_initialized_view(neil, '+karma')
190 >>> print view.label
191- Launchpad Karma for Neil Peart
192+ Launchpad Karma
193
194=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
195--- lib/lp/registry/browser/tests/person-views.txt 2010-04-19 21:16:12 +0000
196+++ lib/lp/registry/browser/tests/person-views.txt 2010-07-14 21:22:45 +0000
197@@ -1,16 +1,17 @@
198-= Person Pages =
199+Person Pages
200+============
201
202-There are many views that wrap the Person object to display the
203-person's information.
204+There are many views that wrap the Person object to display the person's
205+information.
206
207
208 Probationary and invalid users
209 ------------------------------
210
211-The person +index view provides the is_probationary_or_invalid_user so that
212-page features can be disabled because the user may abuse them. Active
213-users with karma are not on probation; the user's homepage_content is
214-formatted as HTML.
215+The person +index view provides the is_probationary_or_invalid_user so
216+that page features can be disabled because the user may abuse them.
217+Active users with karma are not on probation; the user's
218+homepage_content is formatted as HTML.
219
220 >>> from lp.registry.interfaces.person import IPersonSet
221
222@@ -29,8 +30,8 @@
223 <BLANKLINE>
224 <p><a rel="nofollow" href="http://aa.aa/">http://<wbr></wbr>aa.aa/</a></p>
225
226-Teams are always valid and do not have probation rules; the homepage content
227-is formatted HTML.
228+Teams are always valid and do not have probation rules; the homepage
229+content is formatted HTML.
230
231 >>> team = factory.makeTeam()
232 >>> login_person(team.teamowner)
233@@ -59,13 +60,14 @@
234 <BLANKLINE>
235 http://aa.aa/
236
237-Inactive and suspended users are invalid; the homepage content is escaped
238-HTML.
239+Inactive and suspended users are invalid; the homepage content is
240+escaped HTML.
241
242 >>> from canonical.launchpad.interfaces.account import AccountStatus
243 >>> from canonical.launchpad.interfaces import IMasterObject
244
245 # Only admins can change an account.
246+
247 >>> admin_user = person_set.getByName('name16')
248 >>> login_person(admin_user)
249 >>> invalid_user = factory.makePerson(name="ugh")
250@@ -109,6 +111,7 @@
251 >>> mark = person_set.getByEmail('mark@example.com')
252 >>> mark.preferredemail.email
253 u'mark@example.com'
254+
255 >>> mark.hide_email_addresses
256 False
257
258@@ -119,21 +122,25 @@
259 >>> view = create_initialized_view(mark, '+index')
260 >>> view.email_address_visibility.is_login_required
261 True
262+
263 >>> print view.visible_email_address_description
264 None
265+
266 >>> view.visible_email_addresses
267 []
268
269-Logged in user can see Mark's email addresses. The email addresses
270-state is PUBLIC. There is a description of who can see the list of
271-email addresses.
272+Logged in user can see Mark's email addresses. The email addresses state
273+is PUBLIC. There is a description of who can see the list of email
274+addresses.
275
276 >>> login('test@canonical.com')
277 >>> view = create_initialized_view(mark, '+index')
278 >>> view.email_address_visibility.are_public
279 True
280+
281 >>> view.visible_email_address_description
282 'This email address is only visible to Launchpad users.'
283+
284 >>> view.visible_email_addresses
285 [u'mark@example.com']
286
287@@ -149,33 +156,37 @@
288 >>> view = create_initialized_view(sample_person, '+index')
289 >>> view.email_address_visibility.is_login_required
290 True
291+
292 >>> view.visible_email_addresses
293 []
294
295-No Privileges Person cannot see them either because the state is
296-HIDDEN. There is no description for the email addresses because
297-he cannot view them.
298+No Privileges Person cannot see them either because the state is HIDDEN.
299+There is no description for the email addresses because he cannot view
300+them.
301
302 >>> login('no-priv@canonical.com')
303 >>> view = create_initialized_view(sample_person, '+index')
304 >>> view.email_address_visibility.are_hidden
305 True
306+
307 >>> print view.visible_email_address_description
308 None
309+
310 >>> view.visible_email_addresses
311 []
312
313 Admins and commercial admins, like Foo Bar and Commercial Member, can
314-see Sample Person's email addresses because the state is ALLOWED.
315-The description states that the email addresses are not disclosed to
316-others.
317+see Sample Person's email addresses because the state is ALLOWED. The
318+description states that the email addresses are not disclosed to others.
319
320 >>> login('foo.bar@canonical.com')
321 >>> view = create_initialized_view(sample_person, '+index')
322 >>> view.email_address_visibility.are_allowed
323 True
324+
325 >>> view.visible_email_address_description
326 'This email address is not disclosed to others.'
327+
328 >>> view.visible_email_addresses
329 [u'test@canonical.com', u'testing@canonical.com']
330
331@@ -183,6 +194,7 @@
332 >>> view = create_initialized_view(sample_person, '+index')
333 >>> view.email_address_visibility.are_allowed
334 True
335+
336 >>> view.visible_email_addresses
337 [u'test@canonical.com', u'testing@canonical.com']
338
339@@ -194,22 +206,24 @@
340 >>> view = create_initialized_view(ubuntu_team, '+index')
341 >>> view.email_address_visibility.is_login_required
342 True
343+
344 >>> view.visible_email_addresses
345 []
346
347-A logged in user can see the team's contact address because it cannot
348-be hidden.
349+A logged in user can see the team's contact address because it cannot be
350+hidden.
351
352 >>> login('no-priv@canonical.com')
353 >>> view = create_initialized_view(ubuntu_team, '+index')
354 >>> view.email_address_visibility.are_public
355 True
356+
357 >>> view.visible_email_addresses
358 [u'support@ubuntu.com']
359
360-It is possible for a team to have more than two addresses (from a mailing
361-list), but only the preferred address is listed in the visible_email_addresses
362-property.
363+It is possible for a team to have more than two addresses (from a
364+mailing list), but only the preferred address is listed in the
365+visible_email_addresses property.
366
367 >>> email_address = factory.makeEmail(
368 ... 'ubuntu_team@canonical.com', ubuntu_team)
369@@ -227,16 +241,19 @@
370 >>> view = create_initialized_view(landscape_developers, '+index')
371 >>> view.email_address_visibility.are_none_available
372 True
373+
374 >>> print view.visible_email_address_description
375 None
376+
377 >>> view.visible_email_addresses
378 []
379
380
381-== Languages ==
382+Languages
383+---------
384
385-The PersonView provides a comma separated list of languages that a person
386-speaks. The contact details portlet displays the user languages.
387+The PersonView provides a comma separated list of languages that a
388+person speaks. The contact details portlet displays the user languages.
389
390 English is the default language in Launchpad. If the user has not set
391 his preferred languages, English is used.
392@@ -249,9 +266,9 @@
393 >>> print view.languages
394 English
395
396-This assumption is visible to the user when he views his own profile page,
397-and he can set his preferred languages if he wants to make a correction.
398-The list of languages is alphabetized.
399+This assumption is visible to the user when he views his own profile
400+page, and he can set his preferred languages if he wants to make a
401+correction. The list of languages is alphabetized.
402
403 >>> from lp.services.worlddata.interfaces.language import ILanguageSet
404
405@@ -274,8 +291,8 @@
406 English
407
408 Teams most often set just one language that is used for the Answers
409-application. If the language is a variant, the variation is shown
410-in parenthesis.
411+application. If the language is a variant, the variation is shown in
412+parenthesis.
413
414 >>> landscape_developers.addLanguage(
415 ... languageset.getLanguageByCode('pt_BR'))
416@@ -284,13 +301,14 @@
417 Portuguese (Brazil)
418
419
420-== Location ==
421+Location
422+--------
423
424 The Person profile page contains the location portlet that shows a map.
425 The map requires the google GMap JavaScript to display, so the views set
426 the state of the request's needs_gmap2 attribute to True only when the
427-user has set his latitude, it is visible, and the viewing user wishes
428-to see it. The map is not rendered if the user has not set his location.
429+user has set his latitude, it is visible, and the viewing user wishes to
430+see it. The map is not rendered if the user has not set his location.
431
432 >>> sample_person.latitude is None
433 True
434@@ -319,6 +337,7 @@
435 >>> person_view = create_initialized_view(sample_person, '+index')
436 >>> person_view.request.needs_gmap2
437 False
438+
439 >>> print person_view.map_portlet_html
440 Traceback (most recent call last):
441 ...
442@@ -334,20 +353,22 @@
443 >>> person_view = create_initialized_view(sample_person, '+index')
444 >>> person_view.request.needs_gmap2
445 True
446+
447 >>> print person_view.map_portlet_html
448 <script type="text/javascript">
449 YUI().use('node', 'lp.mapping', function(Y) { ...
450
451-The small_maps key in the launchpad_views cookie can be set of the viewing
452-user to 'false' to indicate that small maps are not wanted. While needs_gmap2
453-is False, the map_portlet_html property's markup is still needed to render
454-the 'Show maps' checkbox.
455+The small_maps key in the launchpad_views cookie can be set of the
456+viewing user to 'false' to indicate that small maps are not wanted.
457+While needs_gmap2 is False, the map_portlet_html property's markup is
458+still needed to render the 'Show maps' checkbox.
459
460 >>> cookie = 'launchpad_views=small_maps=false'
461 >>> person_view = create_initialized_view(
462 ... sample_person, '+index', cookie=cookie)
463 >>> person_view.request.needs_gmap2
464 False
465+
466 >>> print person_view.map_portlet_html
467 <script type="text/javascript">
468 YUI().use('node', 'lp.mapping', function(Y) { ...
469@@ -358,6 +379,7 @@
470 >>> user = factory.makePerson()
471 >>> user.latitude is None
472 True
473+
474 >>> login_person(user)
475 >>> person_view = create_initialized_view(
476 ... user, '+index')
477@@ -372,7 +394,8 @@
478 >>> person_view.should_show_map_portlet
479 False
480
481-If a user has a location set and it is visibible then the portlet is shown.
482+If a user has a location set and it is visibible then the portlet is
483+shown.
484
485 >>> person_view = create_initialized_view(
486 ... sample_person, '+index')
487@@ -380,33 +403,37 @@
488 True
489
490
491-== Things a person is working on ==
492+Things a person is working on
493+-----------------------------
494
495 PersonView is the base for many views for Person objects. It provides
496 several properties to help display things the user is working on.
497
498-The +portlet-currentfocus view is responsible for rendering the
499-"Working on..." section in the Person profile page (+index). Nothing
500-is rendered when the user does not have any assigned bug or specs
501-that are not in progress.
502+The +portlet-currentfocus view is responsible for rendering the "Working
503+on..." section in the Person profile page (+index). Nothing is rendered
504+when the user does not have any assigned bug or specs that are not in
505+progress.
506
507 >>> user = factory.makePerson(name='ken', password='test')
508 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
509 >>> view.has_assigned_bugs_or_specs_in_progress
510 False
511+
512 >>> len(view.assigned_bugs_in_progress)
513 0
514+
515 >>> len(view.assigned_specs_in_progress)
516 0
517+
518 >>> from canonical.launchpad.testing.pages import extract_text
519 >>> len(extract_text(view.render()))
520 0
521
522-Assigned specifications that do not display when they are not in an
523-in progress state.
524+Assigned specifications that do not display when they are not in an in
525+progress state.
526
527 >>> from canonical.launchpad.interfaces import (
528- ... SpecificationImplementationStatus)
529+ ... SpecificationImplementationStatus)
530
531 >>> login(user.preferredemail.email)
532 >>> product = factory.makeProduct(name="tool", owner=user)
533@@ -415,18 +442,20 @@
534 >>> spec.assignee = user
535 >>> view.has_assigned_bugs_or_specs_in_progress
536 False
537+
538 >>> len(view.assigned_bugs_in_progress)
539 0
540+
541 >>> len(view.assigned_specs_in_progress)
542 0
543
544 The specification is displayed only when it is in a in progress state
545-(The state may be any from STARTED though DEPLOYMENT). Below the
546-list of specifications is a link to show all the specifications that
547-the user is working on.
548+(The state may be any from STARTED though DEPLOYMENT). Below the list of
549+specifications is a link to show all the specifications that the user is
550+working on.
551
552 >>> from canonical.launchpad.interfaces import (
553- ... SpecificationDefinitionStatus)
554+ ... SpecificationDefinitionStatus)
555
556 >>> spec.definition_status = SpecificationDefinitionStatus.APPROVED
557 >>> newstate = spec.updateLifecycleStatus(user)
558@@ -435,10 +464,13 @@
559 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
560 >>> view.has_assigned_bugs_or_specs_in_progress
561 True
562+
563 >>> len(view.assigned_bugs_in_progress)
564 0
565+
566 >>> len(view.assigned_specs_in_progress)
567 1
568+
569 >>> print view.render()
570 <div id="working-on"...
571 <a href="/~ken/+specs?role=assignee"> All assigned blueprints </a>...
572@@ -453,26 +485,32 @@
573 >>> bug.bugtasks[0].transitionToAssignee(user)
574 >>> view.has_assigned_bugs_or_specs_in_progress
575 True
576+
577 >>> len(view.assigned_bugs_in_progress)
578 0
579+
580 >>> len(view.assigned_specs_in_progress)
581 1
582
583-The assigned bug is displayed in the "Working on..." section when
584-its status is in INPROGRESS.
585+The assigned bug is displayed in the "Working on..." section when its
586+status is in INPROGRESS.
587
588 >>> from canonical.launchpad.interfaces import BugTaskStatus
589 >>> bug.bugtasks[0].transitionToStatus(BugTaskStatus.INPROGRESS, user)
590
591 # Create a new view because we're testing some cached properties.
592+
593 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
594
595 >>> view.has_assigned_bugs_or_specs_in_progress
596 True
597+
598 >>> len(view.assigned_bugs_in_progress)
599 1
600+
601 >>> len(view.assigned_specs_in_progress)
602 1
603+
604 >>> print view.render()
605 <div id="working-on"...
606 <a href="http://launchpad.dev/~ken/+assignedbugs?...">
607@@ -494,12 +532,15 @@
608 ... BugTaskStatus.INPROGRESS, user)
609
610 # Create a new view because we're testing some cached properties.
611+
612 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
613
614 >>> view.has_assigned_bugs_or_specs_in_progress
615 True
616+
617 >>> len(view.assigned_bugs_in_progress)
618 2
619+
620 >>> len(view.assigned_specs_in_progress)
621 1
622
623@@ -508,106 +549,21 @@
624 >>> another_bug.duplicateof = bug
625
626 # Create a new view because we're testing some cached properties.
627+
628 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
629
630 >>> view.has_assigned_bugs_or_specs_in_progress
631 True
632+
633 >>> len(view.assigned_bugs_in_progress)
634 1
635+
636 >>> len(view.assigned_specs_in_progress)
637 1
638
639
640-== Person Packages ==
641-
642-The page at ~user/+related-software contains 4 sections,
643-"Maintained Packages", "Uploaded Packages", "PPA Packages" and "Related
644-projects".
645-
646-Each section is limited to displaying at most N packages, where N is the value
647-of config.launchpad.default_batch_size, so that the page does not time out
648-before Zope can render it.
649-
650-Before continuing, create lots of packages that will appear in each
651-section of Foo Bar's +related-software page, such that there's more available
652-than we're willing to display.
653-
654- >>> login("admin@canonical.com")
655- >>> from lp.registry.interfaces.distribution import (
656- ... IDistributionSet)
657- >>> from lp.soyuz.interfaces.publishing import (
658- ... PackagePublishingStatus)
659- >>> name16 = person_set.getByName('name16')
660- >>> mark = person_set.getByName('mark')
661- >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
662- >>> warty = ubuntu['warty']
663- >>> from lp.soyuz.tests.test_publishing import (
664- ... SoyuzTestPublisher)
665- >>> test_pub = SoyuzTestPublisher()
666- >>> test_pub.person = name16
667-
668- >>> view = create_initialized_view(name16, '+related-software')
669- >>> max_results = view.max_results_to_display
670- >>> for count in range(0, max_results + 3):
671- ... source_name = "foo" + str(count)
672- ... # Add the PPA packages.
673- ... discard = test_pub.getPubSource(
674- ... sourcename=source_name,
675- ... status=PackagePublishingStatus.PUBLISHED,
676- ... archive=mark.archive,
677- ... distroseries=warty)
678- ... # Add the maintained packages.
679- ... discard = test_pub.getPubSource(
680- ... sourcename=source_name,
681- ... status=PackagePublishingStatus.PUBLISHED,
682- ... distroseries=warty)
683- ... # Add the uploaded packages.
684- ... discard = test_pub.getPubSource(
685- ... maintainer=mark,
686- ... sourcename=source_name,
687- ... status=PackagePublishingStatus.PUBLISHED,
688- ... distroseries=warty)
689- >>> import transaction
690- >>> transaction.commit()
691-
692-There are many more new packages to be displayed on the page now:
693-
694- >>> name16.getLatestUploadedPPAPackages().count() > max_results
695- True
696-
697- >>> name16.getLatestMaintainedPackages().count() > max_results
698- True
699-
700- >>> (name16.getLatestUploadedButNotMaintainedPackages().count() >
701- ... max_results)
702- True
703-
704-The view enforces the limit.
705-
706- >>> len(view.get_latest_uploaded_ppa_packages_with_stats) == max_results
707- True
708-
709- >>> len(view.get_latest_maintained_packages_with_stats) == max_results
710- True
711-
712- >>> (len(view.get_latest_uploaded_but_not_maintained_packages_with_stats)
713- ... == max_results)
714- True
715-
716-The view has a helper method that returns a message that can be used
717-at the head of each table.
718-
719- >>> view._tableHeaderMessage(100)
720- 'Displaying first ... packages out of 100 total'
721-
722- >>> view._tableHeaderMessage(max_results)
723- '... packages'
724-
725- >>> view._tableHeaderMessage(1)
726- '1 package'
727-
728-
729-== Person contacting another person ==
730+Person contacting another person
731+--------------------------------
732
733 The PersonView provides information to make the link to contact a user.
734 No Privileges Person can send a message to Sample Person, even though
735@@ -621,75 +577,88 @@
736 >>> print view.contact_link_title
737 Send an email to this user through Launchpad
738
739-The EmailToPersonView provides many properties to the page template
740-to explain exactly who is being contacted.
741+The EmailToPersonView provides many properties to the page template to
742+explain exactly who is being contacted.
743
744 >>> view = create_initialized_view(sample_person, '+contactuser')
745 >>> print view.label
746- Contact Sample Person
747+ Contact user
748+
749 >>> print view.specific_contact_title_text
750 Contact this user
751+
752 >>> print view.recipients.description
753 You are contacting Sample Person (name12).
754+
755 >>> [recipient.name for recipient in view.recipients]
756 [u'name12']
757
758
759-== Person contacting himself ==
760+Person contacting himself
761+-------------------------
762
763-For consistency and testing purposes, the "+contactuser" page is available
764-even when someone is looking at his own profile page. The wording on the
765-tooltip is different though. No Privileges Person can send a message to
766-himself.
767+For consistency and testing purposes, the "+contactuser" page is
768+available even when someone is looking at his own profile page. The
769+wording on the tooltip is different though. No Privileges Person can
770+send a message to himself.
771
772 >>> no_priv = person_set.getByEmail('no-priv@canonical.com')
773 >>> view = create_initialized_view(no_priv, '+index')
774 >>> print view.contact_link_title
775 Send an email to yourself through Launchpad
776
777-The EmailToPersonView provides the explanation about who is being contacted.
778+The EmailToPersonView provides the explanation about who is being
779+contacted.
780
781 >>> view = create_initialized_view(no_priv, '+contactuser')
782 >>> print view.label
783- Contact No Privileges Person
784+ Contact user
785+
786 >>> print view.specific_contact_title_text
787 Contact yourself
788+
789 >>> print view.recipients.description
790 You are contacting No Privileges Person (no-priv).
791+
792 >>> [recipient.name for recipient in view.recipients]
793 [u'no-priv']
794
795
796-== Non-member contacting a Team ==
797+Non-member contacting a Team
798+----------------------------
799
800-Users can contact teams, but the behaviour depends upon whether the
801-user is a member of the team. No Privileges Person is not a member of
802-the Landscape Developers team.
803+Users can contact teams, but the behaviour depends upon whether the user
804+is a member of the team. No Privileges Person is not a member of the
805+Landscape Developers team.
806
807 >>> view = create_initialized_view(landscape_developers, '+index')
808 >>> print view.contact_link_title
809 Send an email to this team's owner through Launchpad
810
811-The EmailToPersonView can be used by non-members to contact the
812-team owner.
813+The EmailToPersonView can be used by non-members to contact the team
814+owner.
815
816 >>> view = create_initialized_view(landscape_developers, '+contactuser')
817 >>> print view.label
818- Contact Landscape Developers
819+ Contact user
820+
821 >>> print view.specific_contact_title_text
822 Contact this team
823+
824 >>> print view.recipients.description
825 You are contacting the Landscape Developers (landscape-developers) team
826 owner, Sample Person (name12).
827+
828 >>> [recipient.name for recipient in view.recipients]
829 [u'name12']
830
831
832-== Member contacting a Team ==
833+Member contacting a Team
834+------------------------
835
836 Members can contact their team. How they are contacted depends upon
837-whether the team's contact address is set. Sample Person can contact
838-his team, Landscape developers, even though they do not have a contact
839+whether the team's contact address is set. Sample Person can contact his
840+team, Landscape developers, even though they do not have a contact
841 address.
842
843 >>> login('test@canonical.com')
844@@ -701,12 +670,15 @@
845
846 >>> view = create_initialized_view(landscape_developers, '+contactuser')
847 >>> print view.label
848- Contact Landscape Developers
849+ Contact user
850+
851 >>> print view.specific_contact_title_text
852 Contact your team
853+
854 >>> print view.recipients.description
855 You are contacting 2 members of the Landscape Developers
856 (landscape-developers) team directly.
857+
858 >>> [recipient.name for recipient in view.recipients]
859 [u'salgado', u'name12']
860
861@@ -715,22 +687,26 @@
862 >>> recipients = view.recipients
863 >>> len(recipients)
864 2
865+
866 >>> bool(recipients)
867 True
868
869 If there is only one member of the team, who must therefore be the user
870-sending the email, and also be the team owner, The view provides a special
871-message just for him.
872+sending the email, and also be the team owner, The view provides a
873+special message just for him.
874
875 >>> vanity_team = factory.makeTeam(
876 ... sample_person, displayname='Vanity', name='vanity')
877 >>> view = create_initialized_view(vanity_team, '+contactuser')
878 >>> print view.label
879- Contact Vanity
880+ Contact user
881+
882 >>> print view.specific_contact_title_text
883 Contact your team
884+
885 >>> print view.recipients.description
886 You are contacting 1 member of the Vanity (vanity) team directly.
887+
888 >>> [recipient.name for recipient in view.recipients]
889 [u'name12']
890
891@@ -741,39 +717,39 @@
892 >>> landscape_developers.setContactAddress(email_address)
893
894 >>> view = create_initialized_view(landscape_developers, '+contactuser')
895- >>> print view.label
896- Contact Landscape Developers
897- >>> print view.specific_contact_title_text
898- Contact your team
899 >>> print view.recipients.description
900 You are contacting the Landscape Developers (landscape-developers) team.
901+
902 >>> [recipient.name for recipient in view.recipients]
903 [u'landscape-developers']
904
905
906-== Contact this user/team valid addresses and quotas ==
907+Contact this user/team valid addresses and quotas
908+-------------------------------------------------
909
910-The EmailToPersonView has_valid_email_address property is normally
911-True. The is_possible property is True when contact_is_allowed and
912+The EmailToPersonView has_valid_email_address property is normally True.
913+The is_possible property is True when contact_is_allowed and
914 has_valid_email_address are both True.
915
916 >>> view = create_initialized_view(landscape_developers, '+contactuser')
917 >>> view.has_valid_email_address
918 True
919+
920 >>> view.contact_is_possible
921 True
922
923 The EmailToPersonView provides two properties that check that the user
924-is_allowed to send emails because he has not exceeded the daily quota. The
925-next_try property is the date when the user will be allowed to send emails
926-again. The is_possible property is True when both contact_is_allowed and
927-as_valid_email_address are True.
928-
929-The daily quota is set to 3 emails per day. See the "Message quota"
930-in `doc/user-to-user.txt` to see how these two attributes are used.
931-
932-
933-== Invalid users and anonymous contacters ==
934+is_allowed to send emails because he has not exceeded the daily quota.
935+The next_try property is the date when the user will be allowed to send
936+emails again. The is_possible property is True when both
937+contact_is_allowed and as_valid_email_address are True.
938+
939+The daily quota is set to 3 emails per day. See the "Message quota" in
940+`doc/user-to-user.txt` to see how these two attributes are used.
941+
942+
943+Invalid users and anonymous contacters
944+--------------------------------------
945
946 Inactive users and users without a preferred email address are invalid
947 and cannot be contacted.
948@@ -782,11 +758,14 @@
949 >>> view = create_initialized_view(former_user, '+contactuser')
950 >>> view.request.response.getStatus()
951 302
952+
953 >>> print view.request.response.getHeader('Location')
954 http://launchpad.dev/~former-user-deactivatedaccount
955+
956 >>> recipients = view.recipients
957 >>> len(recipients)
958 0
959+
960 >>> bool(recipients)
961 False
962
963@@ -798,14 +777,16 @@
964 >>> view = create_initialized_view(landscape_developers, '+contactuser')
965 >>> view.request.response.getStatus()
966 302
967+
968 >>> print view.request.response.getHeader('Location')
969 http://launchpad.dev/~landscape-developers
970
971
972-== Messages and subjects cannot be empty ==
973+Messages and subjects cannot be empty
974+-------------------------------------
975
976-Messages or subjects that contain only whitespace are treated as an error
977-that the user must fix.
978+Messages or subjects that contain only whitespace are treated as an
979+error that the user must fix.
980
981 >>> login('test@canonical.com')
982 >>> view = create_initialized_view(
983@@ -819,13 +800,15 @@
984 [u'You must provide a subject and a message.']
985
986
987-== Person +index "Personal package archives" section ==
988+Person +index "Personal package archives" section
989+-------------------------------------------------
990
991-The person:+index page has a section titled "Personal package
992-archives", which is conditionally displayed depending on the value of the
993-view property `should_show_ppa_section`.
994+The person:+index page has a section titled "Personal package archives",
995+which is conditionally displayed depending on the value of the view
996+property `should_show_ppa_section`.
997
998 The property checks two things to decide whether to return True or not:
999+
1000 * Return True if the current user has launchpad.Edit permission
1001 * Return True if the person has PPAs and at least one of them is viewable
1002 by the current user.
1003@@ -879,9 +862,8 @@
1004 >>> view.should_show_ppa_section
1005 False
1006
1007-For a user with no PPAs, nobody will see the section apart from
1008-himself. This aspect allows him to access the 'Create a new PPA'
1009-link.
1010+For a user with no PPAs, nobody will see the section apart from himself.
1011+This aspect allows him to access the 'Create a new PPA' link.
1012
1013 >>> print sample_person.archive
1014 None
1015@@ -896,15 +878,22 @@
1016 >>> view.should_show_ppa_section
1017 False
1018
1019-If the person is a member of teams with PPAs but doesn't own any himself, the
1020-section will still not appear for anyone but people with lp.edit.
1021+If the person is a member of teams with PPAs but doesn't own any
1022+himself, the section will still not appear for anyone but people with
1023+lp.edit.
1024+
1025+ >>> from canonical.launchpad.interfaces.launchpad import (
1026+ ... ILaunchpadCelebrities)
1027
1028 >>> login("admin@canonical.com")
1029 >>> team = factory.makeTeam()
1030 >>> ignored = team.addMember(sample_person, sample_person)
1031+ >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
1032 >>> ppa = factory.makeArchive(distribution=ubuntu, owner=team)
1033
1034 >>> login(ANONYMOUS)
1035 >>> view = create_initialized_view(sample_person, "+index")
1036 >>> view.should_show_ppa_section
1037 False
1038+
1039+
1040
1041=== added file 'lib/lp/registry/browser/tests/test_branding.py'
1042--- lib/lp/registry/browser/tests/test_branding.py 1970-01-01 00:00:00 +0000
1043+++ lib/lp/registry/browser/tests/test_branding.py 2010-07-14 21:22:45 +0000
1044@@ -0,0 +1,33 @@
1045+# Copyright 2010 Canonical Ltd. This software is licensed under the
1046+# GNU Affero General Public License version 3 (see the file LICENSE).
1047+
1048+"""Tests for Branding."""
1049+
1050+__metaclass__ = type
1051+
1052+import unittest
1053+
1054+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
1055+from canonical.testing.layers import DatabaseFunctionalLayer
1056+from lp.registry.browser.branding import BrandingChangeView
1057+from lp.testing import TestCaseWithFactory
1058+
1059+
1060+class TestBrandingChangeView(TestCaseWithFactory):
1061+
1062+ layer = DatabaseFunctionalLayer
1063+
1064+ def setUp(self):
1065+ super(TestBrandingChangeView, self).setUp()
1066+ self.context = self.factory.makePerson(name='cow')
1067+ self.view = BrandingChangeView(self.context, LaunchpadTestRequest())
1068+
1069+ def test_common_attributes(self):
1070+ # The canonical URL of a GPG key is ssh-keys
1071+ label = 'Change the images used to represent Cow in Launchpad'
1072+ self.assertEqual(label, self.view.label)
1073+ self.assertEqual('Change branding', self.view.page_title)
1074+
1075+
1076+def test_suite():
1077+ return unittest.TestLoader().loadTestsFromName(__name__)
1078
1079=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
1080--- lib/lp/registry/browser/tests/test_person_view.py 2010-06-18 15:06:32 +0000
1081+++ lib/lp/registry/browser/tests/test_person_view.py 2010-07-14 21:22:45 +0000
1082@@ -8,7 +8,9 @@
1083 import transaction
1084 from zope.component import getUtility
1085
1086+from canonical.config import config
1087 from canonical.launchpad.ftests import ANONYMOUS, login
1088+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
1089 from canonical.launchpad.webapp.interfaces import NotFoundError
1090 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
1091 from canonical.testing import (
1092@@ -21,8 +23,9 @@
1093 from lp.registry.model.karma import KarmaCategory
1094 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
1095 from lp.soyuz.interfaces.archive import ArchiveStatus
1096+from lp.soyuz.interfaces.publishing import PackagePublishingStatus
1097 from lp.testing import TestCaseWithFactory, login_person
1098-from lp.testing.views import create_view
1099+from lp.testing.views import create_initialized_view, create_view
1100
1101
1102 class TestPersonViewKarma(TestCaseWithFactory):
1103@@ -357,6 +360,149 @@
1104 self.assertEqual(True, self.view.has_participations)
1105
1106
1107+class TestPersonRelatedSoftwareView(TestCaseWithFactory):
1108+ """Test the related software view."""
1109+
1110+ layer = LaunchpadFunctionalLayer
1111+
1112+ def setUp(self):
1113+ super(TestPersonRelatedSoftwareView, self).setUp()
1114+ self.user = self.factory.makePerson()
1115+ self.factory.makeGPGKey(self.user)
1116+ self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
1117+ self.warty = self.ubuntu.getSeries('warty')
1118+ self.view = create_initialized_view(self.user, '+related-software')
1119+
1120+ def publishSource(self, archive, maintainer):
1121+ publisher = SoyuzTestPublisher()
1122+ publisher.person = self.user
1123+ login('foo.bar@canonical.com')
1124+ for count in range(0, self.view.max_results_to_display + 3):
1125+ source_name = "foo" + str(count)
1126+ publisher.getPubSource(
1127+ sourcename=source_name,
1128+ status=PackagePublishingStatus.PUBLISHED,
1129+ archive=archive,
1130+ maintainer = maintainer,
1131+ creator = self.user,
1132+ distroseries=self.warty)
1133+ login(ANONYMOUS)
1134+
1135+ def test_view_helper_attributes(self):
1136+ # Verify view helper attributes.
1137+ self.assertEqual('Related software', self.view.page_title)
1138+ self.assertEqual('summary_list_size', self.view._max_results_key)
1139+ self.assertEqual(
1140+ config.launchpad.summary_list_size,
1141+ self.view.max_results_to_display)
1142+
1143+ def test_tableHeaderMessage(self):
1144+ limit = self.view.max_results_to_display
1145+ expected = 'Displaying first %s packages out of 100 total' % limit
1146+ self.assertEqual(expected, self.view._tableHeaderMessage(100))
1147+ expected = '%s packages' % limit
1148+ self.assertEqual(expected, self.view._tableHeaderMessage(limit))
1149+ expected = '1 package'
1150+ self.assertEqual(expected, self.view._tableHeaderMessage(1))
1151+
1152+ def test_latest_uploaded_ppa_packages_with_stats(self):
1153+ # Verify number of PPA packages to display.
1154+ ppa = self.factory.makeArchive(owner=self.user)
1155+ self.publishSource(ppa, self.user)
1156+ count = len(self.view.latest_uploaded_ppa_packages_with_stats)
1157+ self.assertEqual(self.view.max_results_to_display, count)
1158+
1159+ def test_latest_maintained_packages_with_stats(self):
1160+ # Verify number of maintained packages to display.
1161+ self.publishSource(self.warty.main_archive, self.user)
1162+ count = len(self.view.latest_maintained_packages_with_stats)
1163+ self.assertEqual(self.view.max_results_to_display, count)
1164+
1165+ def test_latest_uploaded_nonmaintained_packages_with_stats(self):
1166+ # Verify number of non maintained packages to display.
1167+ maintainer = self.factory.makePerson()
1168+ self.publishSource(self.warty.main_archive, maintainer)
1169+ count = len(
1170+ self.view.latest_uploaded_but_not_maintained_packages_with_stats)
1171+ self.assertEqual(self.view.max_results_to_display, count)
1172+
1173+
1174+class TestPersonMaintainedPackagesView(TestCaseWithFactory):
1175+ """Test the maintained packages view."""
1176+
1177+ layer = DatabaseFunctionalLayer
1178+
1179+ def setUp(self):
1180+ super(TestPersonMaintainedPackagesView, self).setUp()
1181+ self.user = self.factory.makePerson()
1182+ self.view = create_initialized_view(self.user, '+maintained-packages')
1183+
1184+ def test_view_helper_attributes(self):
1185+ # Verify view helper attributes.
1186+ self.assertEqual('Maintained Packages', self.view.page_title)
1187+ self.assertEqual('default_batch_size', self.view._max_results_key)
1188+ self.assertEqual(
1189+ config.launchpad.default_batch_size,
1190+ self.view.max_results_to_display)
1191+
1192+
1193+class TestPersonUploadedPackagesView(TestCaseWithFactory):
1194+ """Test the maintained packages view."""
1195+
1196+ layer = DatabaseFunctionalLayer
1197+
1198+ def setUp(self):
1199+ super(TestPersonUploadedPackagesView, self).setUp()
1200+ self.user = self.factory.makePerson()
1201+ self.view = create_initialized_view(self.user, '+uploaded-packages')
1202+
1203+ def test_view_helper_attributes(self):
1204+ # Verify view helper attributes.
1205+ self.assertEqual('Uploaded packages', self.view.page_title)
1206+ self.assertEqual('default_batch_size', self.view._max_results_key)
1207+ self.assertEqual(
1208+ config.launchpad.default_batch_size,
1209+ self.view.max_results_to_display)
1210+
1211+
1212+class TestPersonPPAPackagesView(TestCaseWithFactory):
1213+ """Test the maintained packages view."""
1214+
1215+ layer = DatabaseFunctionalLayer
1216+
1217+ def setUp(self):
1218+ super(TestPersonPPAPackagesView, self).setUp()
1219+ self.user = self.factory.makePerson()
1220+ self.view = create_initialized_view(self.user, '+ppa-packages')
1221+
1222+ def test_view_helper_attributes(self):
1223+ # Verify view helper attributes.
1224+ self.assertEqual('PPA packages', self.view.page_title)
1225+ self.assertEqual('default_batch_size', self.view._max_results_key)
1226+ self.assertEqual(
1227+ config.launchpad.default_batch_size,
1228+ self.view.max_results_to_display)
1229+
1230+
1231+class TestPersonRelatedProjectsView(TestCaseWithFactory):
1232+ """Test the maintained packages view."""
1233+
1234+ layer = DatabaseFunctionalLayer
1235+
1236+ def setUp(self):
1237+ super(TestPersonRelatedProjectsView, self).setUp()
1238+ self.user = self.factory.makePerson()
1239+ self.view = create_initialized_view(self.user, '+related-projects')
1240+
1241+ def test_view_helper_attributes(self):
1242+ # Verify view helper attributes.
1243+ self.assertEqual('Related projects', self.view.page_title)
1244+ self.assertEqual('default_batch_size', self.view._max_results_key)
1245+ self.assertEqual(
1246+ config.launchpad.default_batch_size,
1247+ self.view.max_results_to_display)
1248+
1249+
1250 class TestPersonRelatedSoftwareFailedBuild(TestCaseWithFactory):
1251 """The related software views display links to failed builds."""
1252
1253
1254=== modified file 'lib/lp/registry/browser/tests/user-to-user-views.txt'
1255--- lib/lp/registry/browser/tests/user-to-user-views.txt 2009-08-22 16:51:26 +0000
1256+++ lib/lp/registry/browser/tests/user-to-user-views.txt 2010-07-14 21:22:45 +0000
1257@@ -1,7 +1,8 @@
1258-= User-to-user direct email contact =
1259+User-to-user direct email contact
1260+=================================
1261
1262-A Launchpad user can contact another Launchpad user directly, even if the
1263-recipient is hiding their email addresses.
1264+A Launchpad user can contact another Launchpad user directly, even if
1265+the recipient is hiding their email addresses.
1266
1267 >>> def create_view(sender, recipient, form=None):
1268 ... return create_initialized_view(
1269@@ -29,7 +30,8 @@
1270 This contact is allowed.
1271
1272 >>> print view.label
1273- Contact Guilherme Salgado
1274+ Contact user
1275+
1276 >>> view.contact_is_allowed
1277 True
1278
1279@@ -51,6 +53,7 @@
1280 Message sent to Guilherme Salgado
1281
1282 # Capture the date of the last contact for later.
1283+
1284 >>> from canonical.config import config
1285 >>> from canonical.launchpad.database.message import UserToUserEmail
1286 >>> from lazr.config import as_timedelta
1287@@ -61,30 +64,31 @@
1288 >>> expires = first_contact.date_sent + as_timedelta(
1289 ... config.launchpad.user_to_user_throttle_interval)
1290
1291-No Priv sends two more messages to Salgado. Each of these are allowed too.
1292-
1293- >>> view = create_view(
1294- ... no_priv, salgado, {
1295- ... 'field.field.from_': 'no-priv@canonical.com',
1296- ... 'field.subject': 'Hello Salgado',
1297- ... 'field.message': 'Can you tell me about your project?',
1298- ... 'field.actions.send': 'Send',
1299- ... })
1300- >>> print_notifications(view)
1301- Message sent to Guilherme Salgado
1302-
1303- >>> view = create_view(
1304- ... no_priv, salgado, {
1305- ... 'field.field.from_': 'no-priv@canonical.com',
1306- ... 'field.subject': 'Hello Salgado',
1307- ... 'field.message': 'Can you tell me about your project?',
1308- ... 'field.actions.send': 'Send',
1309- ... })
1310- >>> print_notifications(view)
1311- Message sent to Guilherme Salgado
1312-
1313-Now however, No Priv had reached her quota for direct user-to-user contact and
1314-is not allowed to send a fourth message today.
1315+No Priv sends two more messages to Salgado. Each of these are allowed
1316+too.
1317+
1318+ >>> view = create_view(
1319+ ... no_priv, salgado, {
1320+ ... 'field.field.from_': 'no-priv@canonical.com',
1321+ ... 'field.subject': 'Hello Salgado',
1322+ ... 'field.message': 'Can you tell me about your project?',
1323+ ... 'field.actions.send': 'Send',
1324+ ... })
1325+ >>> print_notifications(view)
1326+ Message sent to Guilherme Salgado
1327+
1328+ >>> view = create_view(
1329+ ... no_priv, salgado, {
1330+ ... 'field.field.from_': 'no-priv@canonical.com',
1331+ ... 'field.subject': 'Hello Salgado',
1332+ ... 'field.message': 'Can you tell me about your project?',
1333+ ... 'field.actions.send': 'Send',
1334+ ... })
1335+ >>> print_notifications(view)
1336+ Message sent to Guilherme Salgado
1337+
1338+Now however, No Priv had reached her quota for direct user-to-user
1339+contact and is not allowed to send a fourth message today.
1340
1341 >>> view = create_view(no_priv, salgado)
1342 >>> view.contact_is_allowed
1343@@ -95,13 +99,13 @@
1344 >>> view.next_try == expires
1345 True
1346
1347-As a corner case, let's say the number of notifications allowed was greater
1348-yesterday than it was today.
1349+As a corner case, let's say the number of notifications allowed was
1350+greater yesterday than it was today.
1351
1352 >>> config.push('seven_allowed', """\
1353- ... [launchpad]
1354- ... user_to_user_max_messages: 7
1355- ... """)
1356+ ... [launchpad]
1357+ ... user_to_user_max_messages: 7
1358+ ... """)
1359
1360 No Priv can actually try again right now.
1361
1362@@ -133,6 +137,7 @@
1363 >>> view = create_view(no_priv, salgado)
1364 >>> view.contact_is_allowed
1365 False
1366+
1367 >>> view.next_try == expires
1368 True
1369
1370@@ -140,6 +145,7 @@
1371
1372 >>> config.pop('seven_allowed')
1373 (...)
1374+
1375 >>> contacts = Store.of(no_priv).find(
1376 ... UserToUserEmail,
1377 ... UserToUserEmail.sender == no_priv)
1378@@ -148,10 +154,11 @@
1379 ... config.launchpad.user_to_user_throttle_interval)
1380
1381
1382-== Non-ASCII names ==
1383+Non-ASCII names
1384+---------------
1385
1386-Carlos has non-ASCII characters in his name. When he sends a message to a
1387-user, his real name will be properly RFC 2047 encoded.
1388+Carlos has non-ASCII characters in his name. When he sends a message to
1389+a user, his real name will be properly RFC 2047 encoded.
1390
1391 >>> transaction.abort()
1392 >>> from lp.services.mail import stub
1393@@ -172,6 +179,7 @@
1394
1395 >>> len(stub.test_emails)
1396 1
1397+
1398 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
1399 >>> print raw_msg
1400 Content-Type: text/plain; charset="us-ascii"
1401@@ -180,8 +188,8 @@
1402 To: No Privileges Person <no-priv@canonical.com>
1403 ...
1404
1405-Similarly, if Carlos is the recipient of a message, his real name will be
1406-properly RFC 2047 encoded as well.
1407+Similarly, if Carlos is the recipient of a message, his real name will
1408+be properly RFC 2047 encoded as well.
1409
1410 >>> del stub.test_emails[:]
1411
1412@@ -197,6 +205,7 @@
1413
1414 >>> len(stub.test_emails)
1415 1
1416+
1417 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
1418 >>> print raw_msg
1419 Content-Type: text/plain; charset="us-ascii"
1420@@ -206,7 +215,8 @@
1421 ...
1422
1423
1424-== Hidden addresses ==
1425+Hidden addresses
1426+----------------
1427
1428 Salgado decides to hide his email addresses.
1429
1430@@ -230,13 +240,15 @@
1431 Message sent to Guilherme Salgado
1432
1433
1434-== Contacting teams ==
1435+Contacting teams
1436+----------------
1437
1438 Teams can also be contacted directly, regardless of whether they have no
1439-official contact address, use a Launchpad mailing list, or have the contact
1440-address set to an explicit address.
1441+official contact address, use a Launchpad mailing list, or have the
1442+contact address set to an explicit address.
1443
1444 # Clear out left over crud.
1445+
1446 >>> transaction.commit()
1447 >>> del stub.test_emails[:]
1448
1449@@ -266,7 +278,8 @@
1450 ... print ' ', recipient
1451
1452
1453-=== Non-member to team ===
1454+Non-member to team
1455+..................
1456
1457 Non-members may only contact the team owner.
1458
1459@@ -301,7 +314,8 @@
1460 Foo Bar <foo.bar@canonical.com>
1461
1462
1463-=== Member to team ===
1464+Member to team
1465+..............
1466
1467 Foo Bar is a member of Guadamen team, he is not restricted to contacting
1468 the team owner. The Guadamen team has no contact address, so contacting
1469@@ -319,9 +333,9 @@
1470 >>> print_notifications(view)
1471 Message sent to GuadaMen
1472
1473-There are 10 members of the team, so exactly 10 unique copies of the message
1474-are sent, one to each team member. Everyone gets a message with the same
1475-subject and body from the same sender.
1476+There are 10 members of the team, so exactly 10 unique copies of the
1477+message are sent, one to each team member. Everyone gets a message with
1478+the same subject and body from the same sender.
1479
1480 >>> transaction.commit()
1481 >>> print_messages()
1482@@ -356,12 +370,13 @@
1483 ... guadamen.name, guadamen.teamowner.name)
1484
1485 # Ignore the 'new mailing list message'
1486+
1487 >>> transaction.commit()
1488 >>> del stub.test_emails[:]
1489
1490-Foo Bar now contacts them again, which he can do because his quota is still
1491-not met. This message includes a "%s" combination; it is not a interpolation
1492-instruction.
1493+Foo Bar now contacts them again, which he can do because his quota is
1494+still not met. This message includes a "%s" combination; it is not a
1495+interpolation instruction.
1496
1497 >>> view = create_view(
1498 ... foo_bar, guadamen, {
1499@@ -410,9 +425,9 @@
1500 >>> address = email_set.new('guadamen@example.com', guadamen)
1501 >>> guadamen.setContactAddress(address)
1502
1503-Foo Bar contacts the Guadamen team again, which is allowed because his quota
1504-was not met by his first message. This time only one message is sent, and
1505-that to the new contact address.
1506+Foo Bar contacts the Guadamen team again, which is allowed because his
1507+quota was not met by his first message. This time only one message is
1508+sent, and that to the new contact address.
1509
1510 >>> view = create_view(
1511 ... foo_bar, guadamen, {
1512@@ -427,9 +442,11 @@
1513 >>> transaction.commit()
1514 >>> len(stub.test_emails)
1515 1
1516+
1517 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
1518 >>> print from_addr, to_addrs
1519 bounces@canonical.com [u'GuadaMen <guadamen@example.com>']
1520+
1521 >>> print raw_msg
1522 Content-Type: text/plain; charset="us-ascii"
1523 ...
1524@@ -449,14 +466,17 @@
1525 https://help.launchpad.net/YourAccount/ContactingPeople
1526
1527
1528-== Message quota ==
1529+Message quota
1530+-------------
1531
1532 The EmailToPersonView provides two properties that check that the user
1533-is_allowed to send emails because he has not exceeded the daily quota. The
1534-next_try property is the date when the user will be allowed to send emails
1535-again. The is_possible property will be False if is_allowed is False.
1536+is_allowed to send emails because he has not exceeded the daily quota.
1537+The next_try property is the date when the user will be allowed to send
1538+emails again. The is_possible property will be False if is_allowed is
1539+False.
1540
1541-Foo Bar has now reached his quota and can send no more contact messages today.
1542+Foo Bar has now reached his quota and can send no more contact messages
1543+today.
1544
1545 >>> view = create_view(
1546 ... foo_bar, guadamen, {
1547@@ -467,10 +487,13 @@
1548 ... })
1549 >>> view.contact_is_allowed
1550 False
1551+
1552 >>> view.next_try
1553 datetime.datetime...
1554+
1555 >>> view.contact_is_possible
1556 False
1557+
1558 >>> print_notifications(view)
1559 Your message was not sent because you have exceeded your daily quota of
1560 3 messages to contact users. Try again in ...
1561@@ -481,15 +504,17 @@
1562 >>> view = create_view(bart, guadamen)
1563 >>> view.contact_is_allowed
1564 True
1565+
1566 >>> view.contact_is_possible
1567 True
1568
1569
1570-== Identifying information ==
1571+Identifying information
1572+-----------------------
1573
1574-Every contact message has a special Launchpad header so that people can tell
1575-that the message came to them through Launchpad. It has a footer that
1576-contains an explanation as well.
1577+Every contact message has a special Launchpad header so that people can
1578+tell that the message came to them through Launchpad. It has a footer
1579+that contains an explanation as well.
1580
1581 >>> cris = factory.makePerson(email='cris@example.com', name='cris')
1582 >>> dave = factory.makePerson(email='dave@example.com', name='dave')
1583@@ -523,11 +548,12 @@
1584 https://help.launchpad.net/YourAccount/ContactingPeople
1585
1586
1587-== Message wrapping ==
1588+Message wrapping
1589+----------------
1590
1591-The message body is wrapped at 72 characters. The footer is not wrapped, but
1592-a new line is started after the names of the sender and the recient to
1593-minimise long lines.
1594+The message body is wrapped at 72 characters. The footer is not wrapped,
1595+but a new line is started after the names of the sender and the recient
1596+to minimise long lines.
1597
1598 >>> login('test@canonical.com')
1599 >>> sample_person = person_set.getByEmail('test@canonical.com')
1600@@ -554,3 +580,4 @@
1601 ^For more information see$
1602 ^https://help.launchpad.net/YourAccount/ContactingPeople$
1603
1604+
1605
1606=== modified file 'lib/lp/registry/doc/person-account.txt'
1607--- lib/lp/registry/doc/person-account.txt 2010-04-07 12:50:17 +0000
1608+++ lib/lp/registry/doc/person-account.txt 2010-07-14 21:22:45 +0000
1609@@ -1,10 +1,12 @@
1610-= Person and Account =
1611+Person and Account
1612+==================
1613
1614 The Person object is responsible for updating the status of its
1615 Account object.
1616
1617
1618-== Activating user accounts ==
1619+Activating user accounts
1620+------------------------
1621
1622 A user may activate their account that was created by an automated
1623 process. Matsubara's account was created during a code import.
1624@@ -54,7 +56,8 @@
1625 u'matsubara@async.com.br'
1626
1627
1628-== Deactivating user accounts ==
1629+Deactivating user accounts
1630+--------------------------
1631
1632 Any user can deactivate his own account, in case they don't want it
1633 anymore or they don't want to be shown as Launchpad users.
1634@@ -182,7 +185,7 @@
1635
1636 ...no owned or driven pillars...
1637
1638- >>> foobar.getOwnedOrDrivenPillars().count()
1639+ >>> len(foobar.getOwnedOrDrivenPillars())
1640 0
1641
1642 ...and, finally, to not be considered a valid person in Launchpad.
1643@@ -206,7 +209,8 @@
1644 True
1645
1646
1647-== Reactivating user accounts ==
1648+Reactivating user accounts
1649+--------------------------
1650
1651 Accounts can be reactivated. A comment and a non-None preferred email address
1652 are required to reactivate() an account, though.
1653
1654=== modified file 'lib/lp/registry/doc/person.txt'
1655--- lib/lp/registry/doc/person.txt 2010-04-23 16:54:21 +0000
1656+++ lib/lp/registry/doc/person.txt 2010-07-14 21:22:45 +0000
1657@@ -1132,25 +1132,22 @@
1658 ... elif IProduct.providedBy(pillar):
1659 ... pillar_type = 'project'
1660 ... print "%s: %s (%s)" % (
1661- ... pillar_type, pillar.title, pillar.name)
1662+ ... pillar_type, pillar.displayname, pillar.name)
1663
1664 >>> for pillarname in mark.getOwnedOrDrivenPillars():
1665 ... print_pillar(pillarname)
1666- distribution: Ubuntu Linux (ubuntu)
1667- distribution: Redhat Advanced Server (redhat)
1668- distribution: Debian GNU/Linux (debian)
1669- distribution: The Gentoo Linux (gentoo)
1670- distribution: Kubuntu - Free KDE-based Linux (kubuntu)
1671- distribution: Ubuntu Test (ubuntutest)
1672+ distribution: Debian (debian)
1673+ distribution: Gentoo (gentoo)
1674+ distribution: Kubuntu (kubuntu)
1675+ distribution: Red Hat (redhat)
1676 project group: Apache (apache)
1677- project: Tomcat (tomcat)
1678- project: ALSA utilities (alsa-utils)
1679- project: Derby - Java Database (derby)
1680+ project: Derby (derby)
1681+ project: alsa-utils (alsa-utils)
1682
1683 >>> for pillarname in ubuntu_team.getOwnedOrDrivenPillars():
1684 ... print_pillar(pillarname)
1685- distribution: Ubuntu Linux (ubuntu)
1686- distribution: Ubuntu Test (ubuntutest)
1687+ distribution: Ubuntu (ubuntu)
1688+ distribution: ubuntutest (ubuntutest)
1689 project: Tomcat (tomcat)
1690
1691
1692
1693=== modified file 'lib/lp/registry/interfaces/person.py'
1694--- lib/lp/registry/interfaces/person.py 2010-07-13 15:29:08 +0000
1695+++ lib/lp/registry/interfaces/person.py 2010-07-14 21:22:45 +0000
1696@@ -999,9 +999,7 @@
1697 """
1698
1699 def getOwnedOrDrivenPillars():
1700- """Return Distribution, Project Groups and Projects that this person
1701- owns or drives.
1702- """
1703+ """Return the pillars that this person directly owns or drives."""
1704
1705 def getOwnedProjects(match_name=None):
1706 """Projects owned by this person or teams to which she belongs.
1707@@ -2130,6 +2128,7 @@
1708 requiring a Launchpad account.
1709 """
1710
1711+
1712 class ISoftwareCenterAgentApplication(ILaunchpadApplication):
1713 """XMLRPC application root for ISoftwareCenterAgentAPI."""
1714
1715
1716=== modified file 'lib/lp/registry/model/person.py'
1717--- lib/lp/registry/model/person.py 2010-07-14 19:33:16 +0000
1718+++ lib/lp/registry/model/person.py 2010-07-14 21:22:45 +0000
1719@@ -186,7 +186,6 @@
1720
1721 This is readonly, as this is a view in the database.
1722 """
1723- # Look Ma, no columns! (apart from id)
1724
1725
1726 def validate_person_visibility(person, attr, value):
1727@@ -725,7 +724,7 @@
1728
1729 # filter based on completion. see the implementation of
1730 # Specification.is_complete() for more details
1731- completeness = Specification.completeness_clause
1732+ completeness = Specification.completeness_clause
1733
1734 if SpecificationFilter.COMPLETE in filter:
1735 query += ' AND ( %s ) ' % completeness
1736@@ -901,40 +900,33 @@
1737
1738 def getOwnedOrDrivenPillars(self):
1739 """See `IPerson`."""
1740- query = """
1741- SELECT name
1742- FROM product, teamparticipation
1743- WHERE teamparticipation.person = %(person)s
1744- AND (driver = teamparticipation.team
1745- OR owner = teamparticipation.team)
1746-
1747- UNION
1748-
1749- SELECT name
1750- FROM project, teamparticipation
1751- WHERE teamparticipation.person = %(person)s
1752- AND (driver = teamparticipation.team
1753- OR owner = teamparticipation.team)
1754-
1755- UNION
1756-
1757- SELECT name
1758- FROM distribution, teamparticipation
1759- WHERE teamparticipation.person = %(person)s
1760- AND (driver = teamparticipation.team
1761- OR owner = teamparticipation.team)
1762- """ % sqlvalues(person=self)
1763- cur = cursor()
1764- cur.execute(query)
1765- names = [sqlvalues(str(name)) for [name] in cur.fetchall()]
1766- if not names:
1767- return PillarName.select("1=2")
1768- quoted_names = ','.join([name for [name] in names])
1769- return PillarName.select(
1770- "PillarName.name IN (%s) AND PillarName.active IS TRUE" %
1771- quoted_names, prejoins=['distribution', 'project', 'product'],
1772- orderBy=['PillarName.distribution', 'PillarName.project',
1773- 'PillarName.product'])
1774+ find_spec = (PillarName, SQL('kind'), SQL('displayname'))
1775+ origin = SQL("""
1776+ PillarName
1777+ JOIN (
1778+ SELECT name, 3 as kind, displayname
1779+ FROM product
1780+ WHERE
1781+ driver = %(person)s
1782+ OR owner = %(person)s
1783+ UNION
1784+ SELECT name, 2 as kind, displayname
1785+ FROM project
1786+ WHERE
1787+ driver = %(person)s
1788+ OR owner = %(person)s
1789+ UNION
1790+ SELECT name, 1 as kind, displayname
1791+ FROM distribution
1792+ WHERE
1793+ driver = %(person)s
1794+ OR owner = %(person)s
1795+ ) _pillar
1796+ ON PillarName.name = _pillar.name
1797+ """ % sqlvalues(person=self))
1798+ results = IStore(self).using(origin).find(find_spec)
1799+ results = results.order_by('kind', 'displayname')
1800+ return [pillar_name for pillar_name, kind, displayname in results]
1801
1802 def getOwnedProjects(self, match_name=None):
1803 """See `IPerson`."""
1804@@ -1447,7 +1439,7 @@
1805 @property
1806 def wiki_names(self):
1807 """See `IPerson`."""
1808- result = Store.of(self).find(WikiName, WikiName.person == self.id)
1809+ result = Store.of(self).find(WikiName, WikiName.person == self.id)
1810 return result.order_by(WikiName.wiki, WikiName.wikiname)
1811
1812 @property
1813@@ -1611,7 +1603,7 @@
1814 Person.teamowner IS NULL
1815 """ % sqlvalues(self.id),
1816 clauseTables=['TeamParticipation', 'Person'],
1817- prejoins=['person',], limit=limit)
1818+ prejoins=['person', ], limit=limit)
1819
1820 def getMappedParticipants(self, limit=None):
1821 """See `IPersonViewRestricted`."""
1822@@ -1644,7 +1636,7 @@
1823 min_lng = 180.0
1824 locations = self._getMappedParticipantsLocations(limit)
1825 if self.mapped_participants_count == 0:
1826- raise AssertionError, (
1827+ raise AssertionError(
1828 'This method cannot be called when '
1829 'mapped_participants_count == 0.')
1830 latitudes = sorted(location.latitude for location in locations)
1831@@ -1824,7 +1816,7 @@
1832 ('teamparticipation', 'team'),
1833 # Skip mailing lists because if the mailing list is purged, it's
1834 # not a problem. Do this check separately below.
1835- ('mailinglist', 'team')
1836+ ('mailinglist', 'team'),
1837 ])
1838
1839 # Private teams may participate in more areas of Launchpad than
1840@@ -2037,7 +2029,7 @@
1841 email = IMasterObject(email)
1842 assert not self.is_team, "This method must not be used for teams."
1843 if not IEmailAddress.providedBy(email):
1844- raise TypeError, (
1845+ raise TypeError(
1846 "Any person's email address must provide the IEmailAddress "
1847 "interface. %s doesn't." % email)
1848 # XXX Steve Alexander 2005-07-05:
1849@@ -2085,7 +2077,7 @@
1850 mailing_list_email = None
1851 all_addresses = IMasterStore(self).find(
1852 EmailAddress, EmailAddress.personID == self.id)
1853- for address in all_addresses :
1854+ for address in all_addresses:
1855 if address not in (email, mailing_list_email):
1856 address.destroySelf()
1857
1858@@ -2117,7 +2109,7 @@
1859 this person.
1860 """
1861 if not IEmailAddress.providedBy(email):
1862- raise TypeError, (
1863+ raise TypeError(
1864 "Any person's email address must provide the IEmailAddress "
1865 "interface. %s doesn't." % email)
1866 assert email.personID == self.id
1867@@ -2223,12 +2215,12 @@
1868 by this person.
1869
1870 :param ppa_only: controls if we are interested only in source
1871- package releases targeted to any PPAs or, if False, sources targeted
1872- to primary archives.
1873+ package releases targeted to any PPAs or, if False, sources
1874+ targeted to primary archives.
1875
1876- Active 'ppa_only' flag is usually associated with active 'uploader_only'
1877- because there shouldn't be any sense of maintainership for packages
1878- uploaded to PPAs by someone else than the user himself.
1879+ Active 'ppa_only' flag is usually associated with active
1880+ 'uploader_only' because there shouldn't be any sense of maintainership
1881+ for packages uploaded to PPAs by someone else than the user himself.
1882 """
1883 clauses = ['sourcepackagerelease.upload_archive = archive.id']
1884
1885@@ -2712,7 +2704,7 @@
1886 private_query = None
1887
1888 base_query = SQL("Person.visibility = ?",
1889- (PersonVisibility.PUBLIC.value,),
1890+ (PersonVisibility.PUBLIC.value, ),
1891 tables=['Person'])
1892
1893 if private_query is None:
1894@@ -2733,8 +2725,7 @@
1895 Not(Person.teamowner == None),
1896 Person.merged == None,
1897 EmailAddress.person == Person.id,
1898- StartsWith(Lower(EmailAddress.email), text)
1899- )
1900+ StartsWith(Lower(EmailAddress.email), text))
1901 return team_email_query
1902
1903 def _teamNameQuery(self, text):
1904@@ -2747,8 +2738,7 @@
1905 TeamParticipation.team == Person.id,
1906 Not(Person.teamowner == None),
1907 Person.merged == None,
1908- SQL("Person.fti @@ ftq(?)", (text,))
1909- )
1910+ SQL("Person.fti @@ ftq(?)", (text, )))
1911 return team_name_query
1912
1913 def find(self, text=""):
1914@@ -2770,8 +2760,7 @@
1915 EmailAddress.person == Person.id,
1916 Person.account == Account.id,
1917 Not(In(Account.status, inactive_statuses)),
1918- StartsWith(Lower(EmailAddress.email), text)
1919- )
1920+ StartsWith(Lower(EmailAddress.email), text))
1921
1922 store = IStore(Person)
1923
1924@@ -2814,8 +2803,7 @@
1925 status.value for status in INACTIVE_ACCOUNT_STATUSES)
1926 base_query = And(
1927 Person.teamowner == None,
1928- Person.merged == None
1929- )
1930+ Person.merged == None)
1931
1932 clause_tables = []
1933
1934@@ -2824,25 +2812,21 @@
1935 base_query = And(
1936 base_query,
1937 Person.account == Account.id,
1938- Not(In(Account.status, inactive_statuses))
1939- )
1940+ Not(In(Account.status, inactive_statuses)))
1941 email_clause_tables = clause_tables + ['EmailAddress']
1942 if must_have_email:
1943 clause_tables = email_clause_tables
1944 base_query = And(
1945 base_query,
1946- EmailAddress.person == Person.id
1947- )
1948+ EmailAddress.person == Person.id)
1949 if created_after is not None:
1950 base_query = And(
1951 base_query,
1952- Person.datecreated > created_after
1953- )
1954+ Person.datecreated > created_after)
1955 if created_before is not None:
1956 base_query = And(
1957 base_query,
1958- Person.datecreated < created_before
1959- )
1960+ Person.datecreated < created_before)
1961
1962 # Short circuit for returning all users in order
1963 if not text:
1964@@ -2855,13 +2839,11 @@
1965 email_query = And(
1966 base_query,
1967 EmailAddress.person == Person.id,
1968- StartsWith(Lower(EmailAddress.email), text)
1969- )
1970+ StartsWith(Lower(EmailAddress.email), text))
1971
1972 name_query = And(
1973 base_query,
1974- SQL("Person.fti @@ ftq(?)", (text,))
1975- )
1976+ SQL("Person.fti @@ ftq(?)", (text, )))
1977 email_results = store.find(Person, email_query).order_by()
1978 name_results = store.find(Person, name_query).order_by()
1979 combined_results = email_results.union(name_results)
1980@@ -3456,8 +3438,7 @@
1981 if updact != 'c':
1982 raise RuntimeError(
1983 '%s.%s reference to %s.%s must be ON UPDATE CASCADE'
1984- % (src_tab, src_col, ref_tab, ref_col)
1985- )
1986+ % (src_tab, src_col, ref_tab, ref_col))
1987
1988 # These rows are in a UNIQUE index, and we can only move them
1989 # to the new Person if there is not already an entry. eg. if
1990@@ -3478,16 +3459,16 @@
1991 cur.execute(
1992 'UPDATE GPGKey SET owner=%(to_id)d WHERE owner=%(from_id)d'
1993 % vars())
1994- skip.append(('gpgkey','owner'))
1995+ skip.append(('gpgkey', 'owner'))
1996
1997 # Update the Branches that will not conflict, and fudge the names of
1998 # ones that *do* conflict.
1999 self._mergeBranches(cur, from_id, to_id)
2000- skip.append(('branch','owner'))
2001+ skip.append(('branch', 'owner'))
2002
2003 # XXX MichaelHudson 2010-01-13: Write _mergeSourcePackageRecipes!
2004 #self._mergeSourcePackageRecipes(cur, from_id, to_id))
2005- skip.append(('sourcepackagerecipe','owner'))
2006+ skip.append(('sourcepackagerecipe', 'owner'))
2007
2008 self._mergeMailingListSubscriptions(cur, from_id, to_id)
2009 skip.append(('mailinglistsubscription', 'person'))
2010@@ -3568,17 +3549,14 @@
2011 raise NotImplementedError(
2012 '%s.%s reference to %s.%s is in a UNIQUE index '
2013 'but has not been handled' % (
2014- src_tab, src_col, ref_tab, ref_col
2015- )
2016- )
2017+ src_tab, src_col, ref_tab, ref_col))
2018
2019 # Handle all simple cases
2020 for src_tab, src_col, ref_tab, ref_col, updact, delact in references:
2021 if (src_tab, src_col) in skip:
2022 continue
2023 cur.execute('UPDATE %s SET %s=%d WHERE %s=%d' % (
2024- src_tab, src_col, to_person.id, src_col, from_person.id
2025- ))
2026+ src_tab, src_col, to_person.id, src_col, from_person.id))
2027
2028 self._mergeTeamMembership(cur, from_id, to_id)
2029
2030@@ -3924,6 +3902,7 @@
2031 domain_parts = domain.split(".")
2032
2033 person_set = PersonSet()
2034+
2035 def _valid_nick(nick):
2036 if not valid_name(nick):
2037 return False
2038@@ -3984,8 +3963,7 @@
2039 raise NicknameGenerationError(
2040 "No nickname could be generated. "
2041 "This should be impossible to trigger unless some twonk has "
2042- "registered a match everything regexp in the black list."
2043- )
2044+ "registered a match everything regexp in the black list.")
2045
2046 finally:
2047 random.setstate(random_state)
2048
2049=== removed file 'lib/lp/registry/stories/person/xx-person-packages.txt'
2050--- lib/lp/registry/stories/person/xx-person-packages.txt 2009-09-18 15:24:30 +0000
2051+++ lib/lp/registry/stories/person/xx-person-packages.txt 1970-01-01 00:00:00 +0000
2052@@ -1,16 +0,0 @@
2053-==========================
2054-Package Maintenance Report
2055-==========================
2056-
2057-From the main person page, the user's Package Maintenance Report can be
2058-accessed by clicking on the 'Related Software' menu item.
2059-
2060- >>> anon_browser.open('http://launchpad.dev/~mark')
2061- >>> anon_browser.getLink('Related software').click()
2062-
2063- >>> print anon_browser.title
2064- Software related to Mark Shuttleworth...
2065- >>> print anon_browser.url
2066- http://launchpad.dev/~mark/+related-software
2067-
2068-Please see pagetests/soyuz/xx-person-packages.txt for details.
2069
2070=== modified file 'lib/lp/registry/stories/person/xx-person-projects.txt'
2071--- lib/lp/registry/stories/person/xx-person-projects.txt 2009-09-18 15:24:30 +0000
2072+++ lib/lp/registry/stories/person/xx-person-projects.txt 2010-07-14 21:22:45 +0000
2073@@ -1,4 +1,5 @@
2074-== List of owned or driven projects ==
2075+List of owned or driven projects
2076+================================
2077
2078 A Team home page displays a list of projects owned or driven by that
2079 team.
2080@@ -16,8 +17,8 @@
2081 unimplemented specs and open questions.
2082
2083 >>> anon_browser.getLink('Show related projects').click()
2084- >>> anon_browser.title
2085- 'Software related to Ubuntu Team...
2086+ >>> print anon_browser.title
2087+ Related software : ...Ubuntu Team... team
2088
2089 >>> related_projects = find_tag_by_id(
2090 ... anon_browser.contents, 'related-projects')
2091@@ -44,7 +45,7 @@
2092 >>> print anon_browser.url
2093 http://launchpad.dev/~mark/+related-software
2094 >>> print anon_browser.title
2095- Software related to Mark Shuttleworth...
2096+ Related software : Mark Shuttleworth
2097
2098 In the case of a person that owns/drives more than
2099 config.launchpad.default_batch_size, a message is displayed and the
2100@@ -55,14 +56,14 @@
2101
2102 >>> print extract_text(
2103 ... find_tag_by_id(anon_browser.contents, 'limit-encountered'))
2104- Displaying first 5 projects out of 10 total
2105+ Displaying first 5 projects out of 7 total
2106
2107 >>> related_projects = find_tag_by_id(
2108 ... anon_browser.contents, 'related-projects')
2109 >>> print extract_text(related_projects)
2110 Name Bugs Blueprints Questions
2111- Ubuntu Linux 4 1 8
2112- Redhat Advanced Server 0 0 0
2113 Debian GNU/Linux 3 0 0
2114 The Gentoo Linux 0 0 0
2115 Kubuntu - Free KDE-based Linux 0 4 0
2116+ Redhat Advanced Server 0 0 0
2117+ Apache 1 0 0
2118
2119=== modified file 'lib/lp/registry/stories/person/xx-user-to-user.txt'
2120--- lib/lp/registry/stories/person/xx-user-to-user.txt 2009-12-03 20:54:00 +0000
2121+++ lib/lp/registry/stories/person/xx-user-to-user.txt 2010-07-14 21:22:45 +0000
2122@@ -12,7 +12,7 @@
2123 >>> user_browser.open('http://launchpad.dev/~salgado')
2124 >>> user_browser.getLink('Contact this user').click()
2125 >>> print user_browser.title
2126- Contact Guilherme Salgado...
2127+ Contact user : Guilherme Salgado
2128
2129 >>> user_browser.getControl('Subject').value = 'Hi Salgado'
2130 >>> user_browser.getControl('Message').value = 'Just saying hello'
2131@@ -163,81 +163,3 @@
2132 >>> print_errors(browser_4.contents)
2133 Your message was not sent because you have exceeded your daily quota of 3
2134 messages to contact users. Try again in ... hours.
2135-
2136-
2137-Your own profile page
2138-=====================
2139-
2140-For consistency and testing purposes, the "contact" page is available even
2141-when someone is looking at their own profile page. The wording on the profile
2142-page is different though.
2143-
2144- >>> user_browser.open('http://launchpad.dev/~no-priv')
2145- >>> user_browser.getLink('Contact this user').click()
2146- >>> print user_browser.title
2147- Contact No Privileges Person...
2148-
2149-This holds true even for Sample Person, who is hiding her email addresses.
2150-
2151- >>> user_browser.open('http://launchpad.dev/~name12')
2152- >>> user_browser.getLink('Contact this user').click()
2153- >>> print user_browser.title
2154- Contact Sample Person...
2155-
2156- >>> name12_browser = setupBrowser('Basic test@canonical.com:test')
2157- >>> name12_browser.open('http://launchpad.dev/~name12')
2158- >>> name12_browser.getLink('Contact this user').click()
2159- >>> print name12_browser.title
2160- Contact Sample Person...
2161-
2162-
2163-Teams
2164-=====
2165-
2166-Teams can also be contacted directly by team members, regardless of whether
2167-the team has set a contact address or uses a Launchpad mailing list.
2168-
2169-Guadamen have no contact address, so contacting them contacts all users
2170-directly.
2171-
2172- >>> admin_browser.open('http://launchpad.dev/~guadamen')
2173- >>> admin_browser.getLink('Contact this team').click()
2174- >>> admin_browser.title
2175- 'Contact GuadaMen...
2176-
2177-Foo Bar registers an explicit contact address for Guadamen...
2178-
2179- >>> admin_browser.open('http://launchpad.dev/~guadamen/+contactaddress')
2180- >>> admin_browser.getControl('Another e-mail address').selected = True
2181- >>> admin_browser.getControl(
2182- ... name='field.contact_address').value = 'foo@example.com'
2183- >>> admin_browser.getControl('Change').click()
2184-
2185- >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
2186-
2187- # Extract the link (from the email we just sent) the user will have to
2188- # use to finish the registration process.
2189- >>> from canonical.launchpad.ftests.logintoken import (
2190- ... get_token_url_from_email)
2191- >>> token_url = get_token_url_from_email(raw_msg)
2192- >>> admin_browser.open(token_url)
2193- >>> admin_browser.getControl('Continue').click()
2194-
2195-...which can also be contacted directly.
2196-
2197- >>> admin_browser.open('http://launchpad.dev/~guadamen')
2198- >>> admin_browser.getLink('Contact this team').click()
2199- >>> admin_browser.title
2200- 'Contact GuadaMen...
2201-
2202-Foo Bar later registers a Launchpad mailing list for Guadamen...
2203-
2204- >>> admin_browser.open('http://launchpad.dev/~guadamen/+mailinglist')
2205- >>> admin_browser.getControl('Apply for Mailing List').click()
2206-
2207-...which too can be contacted directly.
2208-
2209- >>> admin_browser.open('http://launchpad.dev/~guadamen')
2210- >>> admin_browser.getLink('Contact this team').click()
2211- >>> admin_browser.title
2212- 'Contact GuadaMen...
2213
2214=== modified file 'lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt'
2215--- lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt 2010-07-08 12:32:59 +0000
2216+++ lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt 2010-07-14 21:22:45 +0000
2217@@ -1,4 +1,5 @@
2218-= Voucher Redemption =
2219+Voucher Redemption
2220+==================
2221
2222 For a project to use Launchpad it must either be released under an
2223 approved open source license or the project administrators must buy a
2224@@ -11,7 +12,8 @@
2225 is related to a person.
2226
2227
2228-== Accessing the voucher redemption page ==
2229+Accessing the voucher redemption page
2230+-------------------------------------
2231
2232 Mark is an administrator for at least one project that does not have a
2233 valid open source license so he is displayed the voucher redemption
2234@@ -32,8 +34,8 @@
2235 >>> main = find_main_content(browser.contents)
2236 >>> print extract_text(main)
2237 Redeem Vouchers for Commercial Subscriptions...
2238- Marilize Coetzee does not own any commercial projects. Only project owners can redeem
2239- vouchers for a project.
2240+ Marilize Coetzee does not own any commercial projects. Only project
2241+ owners can redeem vouchers for a project.
2242
2243 A user can access her voucher page but not someone else's. Here
2244 Marilize tries to access '+vouchers' on another user and is not
2245@@ -65,7 +67,8 @@
2246 Here are the steps to obtain a commercial subscription:...
2247
2248
2249-== Redeeming a voucher ==
2250+Redeeming a voucher
2251+------------------
2252
2253 Selecting a project the user owns and a valid voucher result in a
2254 successful voucher redemption.
2255@@ -125,7 +128,7 @@
2256 ... 'http://launchpad.dev/~commercial-member/+related-projects')
2257 >>> main = find_main_content(browser.contents)
2258 >>> print extract_text(main)
2259- Projects related to Commercial Member
2260+ Related projects
2261 ...
2262 Commercial Member doesn't own or drive any projects.
2263
2264@@ -142,7 +145,8 @@
2265 Voucher redeemed successfully
2266
2267
2268-== OOPS handling ==
2269+OOPS handling
2270+-------------
2271
2272 If an error occurs in the proxy while trying to redeem a voucher an
2273 OOPS is recorded but an error is not raised. The user is shown an
2274@@ -189,7 +193,9 @@
2275
2276 >>> SalesforceXMLRPCTestTransport.forced_fault = None
2277
2278-== Canceling the request ==
2279+
2280+Canceling the request
2281+---------------------
2282
2283 If the 'Cancel' button is selected the person's overview page is shown.
2284
2285
2286=== modified file 'lib/lp/registry/templates/person-related-software.pt'
2287--- lib/lp/registry/templates/person-related-software.pt 2010-06-24 20:07:30 +0000
2288+++ lib/lp/registry/templates/person-related-software.pt 2010-07-14 21:22:45 +0000
2289@@ -22,7 +22,7 @@
2290 <div id="packages">
2291
2292 <tal:maintained-packages
2293- define="sourcepackagereleases view/get_latest_maintained_packages_with_stats"
2294+ define="sourcepackagereleases view/latest_maintained_packages_with_stats"
2295 condition="sourcepackagereleases">
2296
2297 <div class="top-portlet">
2298@@ -49,7 +49,7 @@
2299 </tal:maintained-packages>
2300
2301 <tal:uploaded-packages
2302- define="sourcepackagereleases view/get_latest_uploaded_but_not_maintained_packages_with_stats"
2303+ define="sourcepackagereleases view/latest_uploaded_but_not_maintained_packages_with_stats"
2304 condition="sourcepackagereleases">
2305
2306 <div class="top-portlet">
2307@@ -75,7 +75,7 @@
2308 </tal:uploaded-packages>
2309
2310 <tal:ppa-packages
2311- define="sourcepackagereleases view/get_latest_uploaded_ppa_packages_with_stats"
2312+ define="sourcepackagereleases view/latest_uploaded_ppa_packages_with_stats"
2313 condition="sourcepackagereleases">
2314
2315 <div class="top-portlet">
2316
2317=== modified file 'lib/lp/soyuz/stories/soyuz/xx-person-packages.txt'
2318--- lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2010-06-24 20:07:30 +0000
2319+++ lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2010-07-14 21:22:45 +0000
2320@@ -1,4 +1,5 @@
2321-= Person Packages =
2322+Person Packages
2323+===============
2324
2325 All packages maintained or uploaded by a given person can be seen on
2326 that person's +related-software page, which is linked to from the
2327@@ -6,7 +7,7 @@
2328
2329 >>> browser.open("http://launchpad.dev/~mark/+related-software")
2330 >>> print browser.title
2331- Software related to Mark Shuttleworth...
2332+ Related software : Mark Shuttleworth
2333
2334 This page is just a summary of the user's packages and will only
2335 display up to the most recent 30 items in each category. However, it
2336@@ -67,7 +68,7 @@
2337 >>> browser.open("http://launchpad.dev/~name16/+related-software")
2338 >>> link = browser.getLink(url="/ubuntu/hoary/+source/cnews")
2339 >>> print link
2340- <Link text='Ubuntu Hoary' url='http://launchpad.dev/ubuntu/hoary/+source/cnews'>
2341+ <Link text='Ubuntu Hoary' ...>
2342 >>> link.click()
2343 >>> browser.title
2344 '...cnews... package : Hoary (5.04) : Ubuntu'
2345@@ -78,13 +79,14 @@
2346 >>> browser.open("http://launchpad.dev/~name16/+related-software")
2347 >>> link = browser.getLink(url="/ubuntu/+source/cnews/cr.g7-37")
2348 >>> print link
2349- <Link text='cr.g7-37' url='http://launchpad.dev/ubuntu/+source/cnews/cr.g7-37'>
2350+ <Link ... url='http://launchpad.dev/ubuntu/+source/cnews/cr.g7-37'>
2351 >>> link.click()
2352 >>> browser.title
2353 'cr.g7-37 : \xe2\x80\x9ccnews\xe2\x80\x9d package : Ubuntu'
2354
2355
2356-== Batched listing pages ==
2357+Batched listing pages
2358+---------------------
2359
2360 Following the navigation link to "Maintained packages" takes the user
2361 to the page that lists maintained packages in batches.
2362@@ -120,8 +122,8 @@
2363 >>> print extract_text(find_tag_by_id(browser.contents, 'packages'))
2364 1...5 of 6 results
2365 ...
2366- Name Uploaded to Version When Failures Bugs Questions
2367- foobar Ubuntu Breezy-autotest 1.0 2006-12-01 i386 0 0
2368+ Name Uploaded to Version When Failures Bugs Questions
2369+ foobar Ubuntu Breezy-autotest 1.0 2006-12-01 i386 0 0
2370 ...
2371
2372 The navigation link to "PPA packages" takes the user to the
2373@@ -134,19 +136,9 @@
2374 Name Uploaded to Version When Failures
2375 iceweasel PPA for Mark...Warty 1.0 2006-04-11 None
2376
2377-And finally the Related projects navigation link takes the user to the
2378-page that lists related projects in batches.
2379-
2380- >>> browser.getLink("Related projects").click()
2381- >>> print extract_text(find_tag_by_id(browser.contents, 'projects'))
2382- 1...5 of 5 results
2383- ...
2384- Name Bugs Blueprints Questions
2385- Ubuntu Linux 4 1 8
2386- ...
2387-
2388-
2389-== Private PPA packages ==
2390+
2391+Private PPA packages
2392+--------------------
2393
2394 Packages listed in the PPA section of this page are filtered so that
2395 if the user is not allowed to see a private package they are not present
2396@@ -215,7 +207,9 @@
2397
2398 >>> user_browser = setupBrowser(auth='Basic test@canonical.com:test')
2399
2400-=== Cprov's +related-software page ===
2401+
2402+Cprov's +related-software page
2403+------------------------------
2404
2405 For unprivileged users, cprov's displayed PPA packages only display
2406 the one in his own public PPA because source2 is only published
2407@@ -351,7 +345,8 @@
2408 ...ago None - -
2409
2410
2411-== Packages deleted from a PPA ==
2412+Packages deleted from a PPA
2413+---------------------------
2414
2415 When a package is deleted from a PPA, in contrast to the archive index
2416 it will continue to appear in the related-software packages list. This