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
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2010-07-01 06:31:55 +0000
+++ configs/development/launchpad-lazr.conf 2010-07-14 21:22:45 +0000
@@ -139,6 +139,7 @@
139mugshot_batch_size: 8139mugshot_batch_size: 8
140announcement_batch_size: 4140announcement_batch_size: 4
141download_batch_size: 4141download_batch_size: 4
142summary_list_size: 5
142openid_preauthorization_acl:143openid_preauthorization_acl:
143 localhost http://launchpad.dev/144 localhost http://launchpad.dev/
144max_bug_feed_cache_minutes: 30145max_bug_feed_cache_minutes: 30
145146
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2010-07-08 16:52:35 +0000
+++ lib/canonical/config/schema-lazr.conf 2010-07-14 21:22:45 +0000
@@ -994,6 +994,9 @@
994# files. The releases are batched, not the individual files.994# files. The releases are batched, not the individual files.
995download_batch_size: 10995download_batch_size: 10
996996
997# The default size of a list that summarizes and introduces a larger list.
998summary_list_size: 10
999
997# If restrict_to_team is set (such as on the beta1000# If restrict_to_team is set (such as on the beta
998# website), then this indicates the hostname suffix for1001# website), then this indicates the hostname suffix for
999# the non-restricted version of Launchpad. Replacing1002# the non-restricted version of Launchpad. Replacing
10001003
=== modified file 'lib/lp/registry/browser/branding.py'
--- lib/lp/registry/browser/branding.py 2009-09-03 13:25:04 +0000
+++ lib/lp/registry/browser/branding.py 2010-07-14 21:22:45 +0000
@@ -28,7 +28,7 @@
28 return ('Change the images used to represent %s in Launchpad'28 return ('Change the images used to represent %s in Launchpad'
29 % self.context.displayname)29 % self.context.displayname)
3030
31 page_title = label31 page_title = "Change branding"
3232
33 custom_widget('icon', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)33 custom_widget('icon', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)
34 custom_widget('logo', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)34 custom_widget('logo', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)
3535
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-07-13 14:50:47 +0000
+++ lib/lp/registry/browser/person.py 2010-07-14 21:22:45 +0000
@@ -2511,7 +2511,7 @@
2511 if self.user == self.context:2511 if self.user == self.context:
2512 return 'Your Launchpad Karma'2512 return 'Your Launchpad Karma'
2513 else:2513 else:
2514 return 'Launchpad Karma for %s' % self.context.displayname2514 return 'Launchpad Karma'
25152515
2516 @cachedproperty2516 @cachedproperty
2517 def has_karma(self):2517 def has_karma(self):
@@ -5084,12 +5084,15 @@
5084class PersonRelatedSoftwareView(LaunchpadView):5084class PersonRelatedSoftwareView(LaunchpadView):
5085 """View for +related-software."""5085 """View for +related-software."""
5086 implements(IPersonRelatedSoftwareMenu)5086 implements(IPersonRelatedSoftwareMenu)
5087 _max_results_key = 'summary_list_size'
50875088
5088 max_results_to_display = config.launchpad.default_batch_size5089 @property
5090 def max_results_to_display(self):
5091 return config.launchpad[self._max_results_key]
50895092
5090 @property5093 @property
5091 def page_title(self):5094 def page_title(self):
5092 return "Software related to " + self.context.displayname5095 return 'Related software'
50935096
5094 @cachedproperty5097 @cachedproperty
5095 def related_projects(self):5098 def related_projects(self):
@@ -5126,12 +5129,12 @@
5126 @cachedproperty5129 @cachedproperty
5127 def first_five_related_projects(self):5130 def first_five_related_projects(self):
5128 """Return first five projects owned or driven by this person."""5131 """Return first five projects owned or driven by this person."""
5129 return list(self._related_projects()[:5])5132 return self._related_projects()[:5]
51305133
5131 @cachedproperty5134 @cachedproperty
5132 def related_projects_count(self):5135 def related_projects_count(self):
5133 """The number of project owned or driven by this person."""5136 """The number of project owned or driven by this person."""
5134 return self._related_projects().count()5137 return len(self._related_projects())
51355138
5136 @cachedproperty5139 @cachedproperty
5137 def has_more_related_projects(self):5140 def has_more_related_projects(self):
@@ -5204,7 +5207,7 @@
5204 return results, header_message5207 return results, header_message
52055208
5206 @property5209 @property
5207 def get_latest_uploaded_ppa_packages_with_stats(self):5210 def latest_uploaded_ppa_packages_with_stats(self):
5208 """Return the sourcepackagereleases uploaded to PPAs by this person.5211 """Return the sourcepackagereleases uploaded to PPAs by this person.
52095212
5210 Results are filtered according to the permission of the requesting5213 Results are filtered according to the permission of the requesting
@@ -5216,7 +5219,7 @@
5216 return self.filterPPAPackageList(results)5219 return self.filterPPAPackageList(results)
52175220
5218 @property5221 @property
5219 def get_latest_maintained_packages_with_stats(self):5222 def latest_maintained_packages_with_stats(self):
5220 """Return the latest maintained packages, including stats."""5223 """Return the latest maintained packages, including stats."""
5221 packages = self.context.getLatestMaintainedPackages()5224 packages = self.context.getLatestMaintainedPackages()
5222 results, header_message = self._getDecoratedPackagesSummary(packages)5225 results, header_message = self._getDecoratedPackagesSummary(packages)
@@ -5224,7 +5227,7 @@
5224 return results5227 return results
52255228
5226 @property5229 @property
5227 def get_latest_uploaded_but_not_maintained_packages_with_stats(self):5230 def latest_uploaded_but_not_maintained_packages_with_stats(self):
5228 """Return the latest uploaded packages, including stats.5231 """Return the latest uploaded packages, including stats.
52295232
5230 Don't include packages that are maintained by the user.5233 Don't include packages that are maintained by the user.
@@ -5308,6 +5311,7 @@
53085311
5309class PersonMaintainedPackagesView(PersonRelatedSoftwareView):5312class PersonMaintainedPackagesView(PersonRelatedSoftwareView):
5310 """View for +maintained-packages."""5313 """View for +maintained-packages."""
5314 _max_results_key = 'default_batch_size'
53115315
5312 def initialize(self):5316 def initialize(self):
5313 """Set up the batch navigation."""5317 """Set up the batch navigation."""
@@ -5316,11 +5320,12 @@
53165320
5317 @property5321 @property
5318 def page_title(self):5322 def page_title(self):
5319 return "Software maintained by " + self.context.displayname5323 return "Maintained Packages"
53205324
53215325
5322class PersonUploadedPackagesView(PersonRelatedSoftwareView):5326class PersonUploadedPackagesView(PersonRelatedSoftwareView):
5323 """View for +uploaded-packages."""5327 """View for +uploaded-packages."""
5328 _max_results_key = 'default_batch_size'
53245329
5325 def initialize(self):5330 def initialize(self):
5326 """Set up the batch navigation."""5331 """Set up the batch navigation."""
@@ -5329,11 +5334,12 @@
53295334
5330 @property5335 @property
5331 def page_title(self):5336 def page_title(self):
5332 return "Software uploaded by " + self.context.displayname5337 return "Uploaded packages"
53335338
53345339
5335class PersonPPAPackagesView(PersonRelatedSoftwareView):5340class PersonPPAPackagesView(PersonRelatedSoftwareView):
5336 """View for +ppa-packages."""5341 """View for +ppa-packages."""
5342 _max_results_key = 'default_batch_size'
53375343
5338 def initialize(self):5344 def initialize(self):
5339 """Set up the batch navigation."""5345 """Set up the batch navigation."""
@@ -5349,11 +5355,12 @@
53495355
5350 @property5356 @property
5351 def page_title(self):5357 def page_title(self):
5352 return "PPA packages related to " + self.context.displayname5358 return "PPA packages"
53535359
53545360
5355class PersonRelatedProjectsView(PersonRelatedSoftwareView):5361class PersonRelatedProjectsView(PersonRelatedSoftwareView):
5356 """View for +related-projects."""5362 """View for +related-projects."""
5363 _max_results_key = 'default_batch_size'
53575364
5358 def initialize(self):5365 def initialize(self):
5359 """Set up the batch navigation."""5366 """Set up the batch navigation."""
@@ -5363,7 +5370,7 @@
53635370
5364 @property5371 @property
5365 def page_title(self):5372 def page_title(self):
5366 return "Projects related to " + self.context.displayname5373 return "Related projects"
53675374
53685375
5369class PersonOAuthTokensView(LaunchpadView):5376class PersonOAuthTokensView(LaunchpadView):
@@ -5778,10 +5785,7 @@
5778 # Subject and then Message fields.5785 # Subject and then Message fields.
5779 self.form_fields = FormFields(*chain((field, ), self.form_fields))5786 self.form_fields = FormFields(*chain((field, ), self.form_fields))
57805787
5781 @property5788 label = 'Contact user'
5782 def label(self):
5783 """The form label."""
5784 return 'Contact ' + self.context.displayname
57855789
5786 @cachedproperty5790 @cachedproperty
5787 def recipients(self):5791 def recipients(self):
57885792
=== modified file 'lib/lp/registry/browser/tests/person-karma-views.txt'
--- lib/lp/registry/browser/tests/person-karma-views.txt 2010-01-21 11:55:56 +0000
+++ lib/lp/registry/browser/tests/person-karma-views.txt 2010-07-14 21:22:45 +0000
@@ -18,4 +18,4 @@
18 >>> neil = factory.makePerson(name='neil', displayname='Neil Peart')18 >>> neil = factory.makePerson(name='neil', displayname='Neil Peart')
19 >>> view = create_initialized_view(neil, '+karma')19 >>> view = create_initialized_view(neil, '+karma')
20 >>> print view.label20 >>> print view.label
21 Launchpad Karma for Neil Peart21 Launchpad Karma
2222
=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
--- lib/lp/registry/browser/tests/person-views.txt 2010-04-19 21:16:12 +0000
+++ lib/lp/registry/browser/tests/person-views.txt 2010-07-14 21:22:45 +0000
@@ -1,16 +1,17 @@
1= Person Pages =1Person Pages
2============
23
3There are many views that wrap the Person object to display the4There are many views that wrap the Person object to display the person's
4person's information.5information.
56
67
7Probationary and invalid users8Probationary and invalid users
8------------------------------9------------------------------
910
10The person +index view provides the is_probationary_or_invalid_user so that11The person +index view provides the is_probationary_or_invalid_user so
11page features can be disabled because the user may abuse them. Active12that page features can be disabled because the user may abuse them.
12users with karma are not on probation; the user's homepage_content is13Active users with karma are not on probation; the user's
13formatted as HTML.14homepage_content is formatted as HTML.
1415
15 >>> from lp.registry.interfaces.person import IPersonSet16 >>> from lp.registry.interfaces.person import IPersonSet
1617
@@ -29,8 +30,8 @@
29 <BLANKLINE>30 <BLANKLINE>
30 <p><a rel="nofollow" href="http://aa.aa/">http://<wbr></wbr>aa.aa/</a></p>31 <p><a rel="nofollow" href="http://aa.aa/">http://<wbr></wbr>aa.aa/</a></p>
3132
32Teams are always valid and do not have probation rules; the homepage content33Teams are always valid and do not have probation rules; the homepage
33is formatted HTML.34content is formatted HTML.
3435
35 >>> team = factory.makeTeam()36 >>> team = factory.makeTeam()
36 >>> login_person(team.teamowner)37 >>> login_person(team.teamowner)
@@ -59,13 +60,14 @@
59 <BLANKLINE>60 <BLANKLINE>
60 http://aa.aa/61 http://aa.aa/
6162
62Inactive and suspended users are invalid; the homepage content is escaped63Inactive and suspended users are invalid; the homepage content is
63HTML.64escaped HTML.
6465
65 >>> from canonical.launchpad.interfaces.account import AccountStatus66 >>> from canonical.launchpad.interfaces.account import AccountStatus
66 >>> from canonical.launchpad.interfaces import IMasterObject67 >>> from canonical.launchpad.interfaces import IMasterObject
6768
68 # Only admins can change an account.69 # Only admins can change an account.
70
69 >>> admin_user = person_set.getByName('name16')71 >>> admin_user = person_set.getByName('name16')
70 >>> login_person(admin_user)72 >>> login_person(admin_user)
71 >>> invalid_user = factory.makePerson(name="ugh")73 >>> invalid_user = factory.makePerson(name="ugh")
@@ -109,6 +111,7 @@
109 >>> mark = person_set.getByEmail('mark@example.com')111 >>> mark = person_set.getByEmail('mark@example.com')
110 >>> mark.preferredemail.email112 >>> mark.preferredemail.email
111 u'mark@example.com'113 u'mark@example.com'
114
112 >>> mark.hide_email_addresses115 >>> mark.hide_email_addresses
113 False116 False
114117
@@ -119,21 +122,25 @@
119 >>> view = create_initialized_view(mark, '+index')122 >>> view = create_initialized_view(mark, '+index')
120 >>> view.email_address_visibility.is_login_required123 >>> view.email_address_visibility.is_login_required
121 True124 True
125
122 >>> print view.visible_email_address_description126 >>> print view.visible_email_address_description
123 None127 None
128
124 >>> view.visible_email_addresses129 >>> view.visible_email_addresses
125 []130 []
126131
127Logged in user can see Mark's email addresses. The email addresses132Logged in user can see Mark's email addresses. The email addresses state
128state is PUBLIC. There is a description of who can see the list of133is PUBLIC. There is a description of who can see the list of email
129email addresses.134addresses.
130135
131 >>> login('test@canonical.com')136 >>> login('test@canonical.com')
132 >>> view = create_initialized_view(mark, '+index')137 >>> view = create_initialized_view(mark, '+index')
133 >>> view.email_address_visibility.are_public138 >>> view.email_address_visibility.are_public
134 True139 True
140
135 >>> view.visible_email_address_description141 >>> view.visible_email_address_description
136 'This email address is only visible to Launchpad users.'142 'This email address is only visible to Launchpad users.'
143
137 >>> view.visible_email_addresses144 >>> view.visible_email_addresses
138 [u'mark@example.com']145 [u'mark@example.com']
139146
@@ -149,33 +156,37 @@
149 >>> view = create_initialized_view(sample_person, '+index')156 >>> view = create_initialized_view(sample_person, '+index')
150 >>> view.email_address_visibility.is_login_required157 >>> view.email_address_visibility.is_login_required
151 True158 True
159
152 >>> view.visible_email_addresses160 >>> view.visible_email_addresses
153 []161 []
154162
155No Privileges Person cannot see them either because the state is163No Privileges Person cannot see them either because the state is HIDDEN.
156HIDDEN. There is no description for the email addresses because164There is no description for the email addresses because he cannot view
157he cannot view them.165them.
158166
159 >>> login('no-priv@canonical.com')167 >>> login('no-priv@canonical.com')
160 >>> view = create_initialized_view(sample_person, '+index')168 >>> view = create_initialized_view(sample_person, '+index')
161 >>> view.email_address_visibility.are_hidden169 >>> view.email_address_visibility.are_hidden
162 True170 True
171
163 >>> print view.visible_email_address_description172 >>> print view.visible_email_address_description
164 None173 None
174
165 >>> view.visible_email_addresses175 >>> view.visible_email_addresses
166 []176 []
167177
168Admins and commercial admins, like Foo Bar and Commercial Member, can178Admins and commercial admins, like Foo Bar and Commercial Member, can
169see Sample Person's email addresses because the state is ALLOWED.179see Sample Person's email addresses because the state is ALLOWED. The
170The description states that the email addresses are not disclosed to180description states that the email addresses are not disclosed to others.
171others.
172181
173 >>> login('foo.bar@canonical.com')182 >>> login('foo.bar@canonical.com')
174 >>> view = create_initialized_view(sample_person, '+index')183 >>> view = create_initialized_view(sample_person, '+index')
175 >>> view.email_address_visibility.are_allowed184 >>> view.email_address_visibility.are_allowed
176 True185 True
186
177 >>> view.visible_email_address_description187 >>> view.visible_email_address_description
178 'This email address is not disclosed to others.'188 'This email address is not disclosed to others.'
189
179 >>> view.visible_email_addresses190 >>> view.visible_email_addresses
180 [u'test@canonical.com', u'testing@canonical.com']191 [u'test@canonical.com', u'testing@canonical.com']
181192
@@ -183,6 +194,7 @@
183 >>> view = create_initialized_view(sample_person, '+index')194 >>> view = create_initialized_view(sample_person, '+index')
184 >>> view.email_address_visibility.are_allowed195 >>> view.email_address_visibility.are_allowed
185 True196 True
197
186 >>> view.visible_email_addresses198 >>> view.visible_email_addresses
187 [u'test@canonical.com', u'testing@canonical.com']199 [u'test@canonical.com', u'testing@canonical.com']
188200
@@ -194,22 +206,24 @@
194 >>> view = create_initialized_view(ubuntu_team, '+index')206 >>> view = create_initialized_view(ubuntu_team, '+index')
195 >>> view.email_address_visibility.is_login_required207 >>> view.email_address_visibility.is_login_required
196 True208 True
209
197 >>> view.visible_email_addresses210 >>> view.visible_email_addresses
198 []211 []
199212
200A logged in user can see the team's contact address because it cannot213A logged in user can see the team's contact address because it cannot be
201be hidden.214hidden.
202215
203 >>> login('no-priv@canonical.com')216 >>> login('no-priv@canonical.com')
204 >>> view = create_initialized_view(ubuntu_team, '+index')217 >>> view = create_initialized_view(ubuntu_team, '+index')
205 >>> view.email_address_visibility.are_public218 >>> view.email_address_visibility.are_public
206 True219 True
220
207 >>> view.visible_email_addresses221 >>> view.visible_email_addresses
208 [u'support@ubuntu.com']222 [u'support@ubuntu.com']
209223
210It is possible for a team to have more than two addresses (from a mailing224It is possible for a team to have more than two addresses (from a
211list), but only the preferred address is listed in the visible_email_addresses225mailing list), but only the preferred address is listed in the
212property.226visible_email_addresses property.
213227
214 >>> email_address = factory.makeEmail(228 >>> email_address = factory.makeEmail(
215 ... 'ubuntu_team@canonical.com', ubuntu_team)229 ... 'ubuntu_team@canonical.com', ubuntu_team)
@@ -227,16 +241,19 @@
227 >>> view = create_initialized_view(landscape_developers, '+index')241 >>> view = create_initialized_view(landscape_developers, '+index')
228 >>> view.email_address_visibility.are_none_available242 >>> view.email_address_visibility.are_none_available
229 True243 True
244
230 >>> print view.visible_email_address_description245 >>> print view.visible_email_address_description
231 None246 None
247
232 >>> view.visible_email_addresses248 >>> view.visible_email_addresses
233 []249 []
234250
235251
236== Languages ==252Languages
253---------
237254
238The PersonView provides a comma separated list of languages that a person255The PersonView provides a comma separated list of languages that a
239speaks. The contact details portlet displays the user languages.256person speaks. The contact details portlet displays the user languages.
240257
241English is the default language in Launchpad. If the user has not set258English is the default language in Launchpad. If the user has not set
242his preferred languages, English is used.259his preferred languages, English is used.
@@ -249,9 +266,9 @@
249 >>> print view.languages266 >>> print view.languages
250 English267 English
251268
252This assumption is visible to the user when he views his own profile page,269This assumption is visible to the user when he views his own profile
253and he can set his preferred languages if he wants to make a correction.270page, and he can set his preferred languages if he wants to make a
254The list of languages is alphabetized.271correction. The list of languages is alphabetized.
255272
256 >>> from lp.services.worlddata.interfaces.language import ILanguageSet273 >>> from lp.services.worlddata.interfaces.language import ILanguageSet
257274
@@ -274,8 +291,8 @@
274 English291 English
275292
276Teams most often set just one language that is used for the Answers293Teams most often set just one language that is used for the Answers
277application. If the language is a variant, the variation is shown294application. If the language is a variant, the variation is shown in
278in parenthesis.295parenthesis.
279296
280 >>> landscape_developers.addLanguage(297 >>> landscape_developers.addLanguage(
281 ... languageset.getLanguageByCode('pt_BR'))298 ... languageset.getLanguageByCode('pt_BR'))
@@ -284,13 +301,14 @@
284 Portuguese (Brazil)301 Portuguese (Brazil)
285302
286303
287== Location ==304Location
305--------
288306
289The Person profile page contains the location portlet that shows a map.307The Person profile page contains the location portlet that shows a map.
290The map requires the google GMap JavaScript to display, so the views set308The map requires the google GMap JavaScript to display, so the views set
291the state of the request's needs_gmap2 attribute to True only when the309the state of the request's needs_gmap2 attribute to True only when the
292user has set his latitude, it is visible, and the viewing user wishes310user has set his latitude, it is visible, and the viewing user wishes to
293to see it. The map is not rendered if the user has not set his location.311see it. The map is not rendered if the user has not set his location.
294312
295 >>> sample_person.latitude is None313 >>> sample_person.latitude is None
296 True314 True
@@ -319,6 +337,7 @@
319 >>> person_view = create_initialized_view(sample_person, '+index')337 >>> person_view = create_initialized_view(sample_person, '+index')
320 >>> person_view.request.needs_gmap2338 >>> person_view.request.needs_gmap2
321 False339 False
340
322 >>> print person_view.map_portlet_html341 >>> print person_view.map_portlet_html
323 Traceback (most recent call last):342 Traceback (most recent call last):
324 ...343 ...
@@ -334,20 +353,22 @@
334 >>> person_view = create_initialized_view(sample_person, '+index')353 >>> person_view = create_initialized_view(sample_person, '+index')
335 >>> person_view.request.needs_gmap2354 >>> person_view.request.needs_gmap2
336 True355 True
356
337 >>> print person_view.map_portlet_html357 >>> print person_view.map_portlet_html
338 <script type="text/javascript">358 <script type="text/javascript">
339 YUI().use('node', 'lp.mapping', function(Y) { ...359 YUI().use('node', 'lp.mapping', function(Y) { ...
340360
341The small_maps key in the launchpad_views cookie can be set of the viewing361The small_maps key in the launchpad_views cookie can be set of the
342user to 'false' to indicate that small maps are not wanted. While needs_gmap2362viewing user to 'false' to indicate that small maps are not wanted.
343is False, the map_portlet_html property's markup is still needed to render363While needs_gmap2 is False, the map_portlet_html property's markup is
344the 'Show maps' checkbox.364still needed to render the 'Show maps' checkbox.
345365
346 >>> cookie = 'launchpad_views=small_maps=false'366 >>> cookie = 'launchpad_views=small_maps=false'
347 >>> person_view = create_initialized_view(367 >>> person_view = create_initialized_view(
348 ... sample_person, '+index', cookie=cookie)368 ... sample_person, '+index', cookie=cookie)
349 >>> person_view.request.needs_gmap2369 >>> person_view.request.needs_gmap2
350 False370 False
371
351 >>> print person_view.map_portlet_html372 >>> print person_view.map_portlet_html
352 <script type="text/javascript">373 <script type="text/javascript">
353 YUI().use('node', 'lp.mapping', function(Y) { ...374 YUI().use('node', 'lp.mapping', function(Y) { ...
@@ -358,6 +379,7 @@
358 >>> user = factory.makePerson()379 >>> user = factory.makePerson()
359 >>> user.latitude is None380 >>> user.latitude is None
360 True381 True
382
361 >>> login_person(user)383 >>> login_person(user)
362 >>> person_view = create_initialized_view(384 >>> person_view = create_initialized_view(
363 ... user, '+index')385 ... user, '+index')
@@ -372,7 +394,8 @@
372 >>> person_view.should_show_map_portlet394 >>> person_view.should_show_map_portlet
373 False395 False
374396
375If a user has a location set and it is visibible then the portlet is shown.397If a user has a location set and it is visibible then the portlet is
398shown.
376399
377 >>> person_view = create_initialized_view(400 >>> person_view = create_initialized_view(
378 ... sample_person, '+index')401 ... sample_person, '+index')
@@ -380,33 +403,37 @@
380 True403 True
381404
382405
383== Things a person is working on ==406Things a person is working on
407-----------------------------
384408
385PersonView is the base for many views for Person objects. It provides409PersonView is the base for many views for Person objects. It provides
386several properties to help display things the user is working on.410several properties to help display things the user is working on.
387411
388The +portlet-currentfocus view is responsible for rendering the412The +portlet-currentfocus view is responsible for rendering the "Working
389"Working on..." section in the Person profile page (+index). Nothing413on..." section in the Person profile page (+index). Nothing is rendered
390is rendered when the user does not have any assigned bug or specs414when the user does not have any assigned bug or specs that are not in
391that are not in progress.415progress.
392416
393 >>> user = factory.makePerson(name='ken', password='test')417 >>> user = factory.makePerson(name='ken', password='test')
394 >>> view = create_initialized_view(user, name='+portlet-currentfocus')418 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
395 >>> view.has_assigned_bugs_or_specs_in_progress419 >>> view.has_assigned_bugs_or_specs_in_progress
396 False420 False
421
397 >>> len(view.assigned_bugs_in_progress)422 >>> len(view.assigned_bugs_in_progress)
398 0423 0
424
399 >>> len(view.assigned_specs_in_progress)425 >>> len(view.assigned_specs_in_progress)
400 0426 0
427
401 >>> from canonical.launchpad.testing.pages import extract_text428 >>> from canonical.launchpad.testing.pages import extract_text
402 >>> len(extract_text(view.render()))429 >>> len(extract_text(view.render()))
403 0430 0
404431
405Assigned specifications that do not display when they are not in an432Assigned specifications that do not display when they are not in an in
406in progress state.433progress state.
407434
408 >>> from canonical.launchpad.interfaces import (435 >>> from canonical.launchpad.interfaces import (
409 ... SpecificationImplementationStatus)436 ... SpecificationImplementationStatus)
410437
411 >>> login(user.preferredemail.email)438 >>> login(user.preferredemail.email)
412 >>> product = factory.makeProduct(name="tool", owner=user)439 >>> product = factory.makeProduct(name="tool", owner=user)
@@ -415,18 +442,20 @@
415 >>> spec.assignee = user442 >>> spec.assignee = user
416 >>> view.has_assigned_bugs_or_specs_in_progress443 >>> view.has_assigned_bugs_or_specs_in_progress
417 False444 False
445
418 >>> len(view.assigned_bugs_in_progress)446 >>> len(view.assigned_bugs_in_progress)
419 0447 0
448
420 >>> len(view.assigned_specs_in_progress)449 >>> len(view.assigned_specs_in_progress)
421 0450 0
422451
423The specification is displayed only when it is in a in progress state452The specification is displayed only when it is in a in progress state
424(The state may be any from STARTED though DEPLOYMENT). Below the453(The state may be any from STARTED though DEPLOYMENT). Below the list of
425list of specifications is a link to show all the specifications that454specifications is a link to show all the specifications that the user is
426the user is working on.455working on.
427456
428 >>> from canonical.launchpad.interfaces import (457 >>> from canonical.launchpad.interfaces import (
429 ... SpecificationDefinitionStatus)458 ... SpecificationDefinitionStatus)
430459
431 >>> spec.definition_status = SpecificationDefinitionStatus.APPROVED460 >>> spec.definition_status = SpecificationDefinitionStatus.APPROVED
432 >>> newstate = spec.updateLifecycleStatus(user)461 >>> newstate = spec.updateLifecycleStatus(user)
@@ -435,10 +464,13 @@
435 >>> view = create_initialized_view(user, name='+portlet-currentfocus')464 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
436 >>> view.has_assigned_bugs_or_specs_in_progress465 >>> view.has_assigned_bugs_or_specs_in_progress
437 True466 True
467
438 >>> len(view.assigned_bugs_in_progress)468 >>> len(view.assigned_bugs_in_progress)
439 0469 0
470
440 >>> len(view.assigned_specs_in_progress)471 >>> len(view.assigned_specs_in_progress)
441 1472 1
473
442 >>> print view.render()474 >>> print view.render()
443 <div id="working-on"...475 <div id="working-on"...
444 <a href="/~ken/+specs?role=assignee"> All assigned blueprints </a>...476 <a href="/~ken/+specs?role=assignee"> All assigned blueprints </a>...
@@ -453,26 +485,32 @@
453 >>> bug.bugtasks[0].transitionToAssignee(user)485 >>> bug.bugtasks[0].transitionToAssignee(user)
454 >>> view.has_assigned_bugs_or_specs_in_progress486 >>> view.has_assigned_bugs_or_specs_in_progress
455 True487 True
488
456 >>> len(view.assigned_bugs_in_progress)489 >>> len(view.assigned_bugs_in_progress)
457 0490 0
491
458 >>> len(view.assigned_specs_in_progress)492 >>> len(view.assigned_specs_in_progress)
459 1493 1
460494
461The assigned bug is displayed in the "Working on..." section when495The assigned bug is displayed in the "Working on..." section when its
462its status is in INPROGRESS.496status is in INPROGRESS.
463497
464 >>> from canonical.launchpad.interfaces import BugTaskStatus498 >>> from canonical.launchpad.interfaces import BugTaskStatus
465 >>> bug.bugtasks[0].transitionToStatus(BugTaskStatus.INPROGRESS, user)499 >>> bug.bugtasks[0].transitionToStatus(BugTaskStatus.INPROGRESS, user)
466500
467 # Create a new view because we're testing some cached properties.501 # Create a new view because we're testing some cached properties.
502
468 >>> view = create_initialized_view(user, name='+portlet-currentfocus')503 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
469504
470 >>> view.has_assigned_bugs_or_specs_in_progress505 >>> view.has_assigned_bugs_or_specs_in_progress
471 True506 True
507
472 >>> len(view.assigned_bugs_in_progress)508 >>> len(view.assigned_bugs_in_progress)
473 1509 1
510
474 >>> len(view.assigned_specs_in_progress)511 >>> len(view.assigned_specs_in_progress)
475 1512 1
513
476 >>> print view.render()514 >>> print view.render()
477 <div id="working-on"...515 <div id="working-on"...
478 <a href="http://launchpad.dev/~ken/+assignedbugs?...">516 <a href="http://launchpad.dev/~ken/+assignedbugs?...">
@@ -494,12 +532,15 @@
494 ... BugTaskStatus.INPROGRESS, user)532 ... BugTaskStatus.INPROGRESS, user)
495533
496 # Create a new view because we're testing some cached properties.534 # Create a new view because we're testing some cached properties.
535
497 >>> view = create_initialized_view(user, name='+portlet-currentfocus')536 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
498537
499 >>> view.has_assigned_bugs_or_specs_in_progress538 >>> view.has_assigned_bugs_or_specs_in_progress
500 True539 True
540
501 >>> len(view.assigned_bugs_in_progress)541 >>> len(view.assigned_bugs_in_progress)
502 2542 2
543
503 >>> len(view.assigned_specs_in_progress)544 >>> len(view.assigned_specs_in_progress)
504 1545 1
505546
@@ -508,106 +549,21 @@
508 >>> another_bug.duplicateof = bug549 >>> another_bug.duplicateof = bug
509550
510 # Create a new view because we're testing some cached properties.551 # Create a new view because we're testing some cached properties.
552
511 >>> view = create_initialized_view(user, name='+portlet-currentfocus')553 >>> view = create_initialized_view(user, name='+portlet-currentfocus')
512554
513 >>> view.has_assigned_bugs_or_specs_in_progress555 >>> view.has_assigned_bugs_or_specs_in_progress
514 True556 True
557
515 >>> len(view.assigned_bugs_in_progress)558 >>> len(view.assigned_bugs_in_progress)
516 1559 1
560
517 >>> len(view.assigned_specs_in_progress)561 >>> len(view.assigned_specs_in_progress)
518 1562 1
519563
520564
521== Person Packages ==565Person contacting another person
522566--------------------------------
523The page at ~user/+related-software contains 4 sections,
524"Maintained Packages", "Uploaded Packages", "PPA Packages" and "Related
525projects".
526
527Each section is limited to displaying at most N packages, where N is the value
528of config.launchpad.default_batch_size, so that the page does not time out
529before Zope can render it.
530
531Before continuing, create lots of packages that will appear in each
532section of Foo Bar's +related-software page, such that there's more available
533than we're willing to display.
534
535 >>> login("admin@canonical.com")
536 >>> from lp.registry.interfaces.distribution import (
537 ... IDistributionSet)
538 >>> from lp.soyuz.interfaces.publishing import (
539 ... PackagePublishingStatus)
540 >>> name16 = person_set.getByName('name16')
541 >>> mark = person_set.getByName('mark')
542 >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
543 >>> warty = ubuntu['warty']
544 >>> from lp.soyuz.tests.test_publishing import (
545 ... SoyuzTestPublisher)
546 >>> test_pub = SoyuzTestPublisher()
547 >>> test_pub.person = name16
548
549 >>> view = create_initialized_view(name16, '+related-software')
550 >>> max_results = view.max_results_to_display
551 >>> for count in range(0, max_results + 3):
552 ... source_name = "foo" + str(count)
553 ... # Add the PPA packages.
554 ... discard = test_pub.getPubSource(
555 ... sourcename=source_name,
556 ... status=PackagePublishingStatus.PUBLISHED,
557 ... archive=mark.archive,
558 ... distroseries=warty)
559 ... # Add the maintained packages.
560 ... discard = test_pub.getPubSource(
561 ... sourcename=source_name,
562 ... status=PackagePublishingStatus.PUBLISHED,
563 ... distroseries=warty)
564 ... # Add the uploaded packages.
565 ... discard = test_pub.getPubSource(
566 ... maintainer=mark,
567 ... sourcename=source_name,
568 ... status=PackagePublishingStatus.PUBLISHED,
569 ... distroseries=warty)
570 >>> import transaction
571 >>> transaction.commit()
572
573There are many more new packages to be displayed on the page now:
574
575 >>> name16.getLatestUploadedPPAPackages().count() > max_results
576 True
577
578 >>> name16.getLatestMaintainedPackages().count() > max_results
579 True
580
581 >>> (name16.getLatestUploadedButNotMaintainedPackages().count() >
582 ... max_results)
583 True
584
585The view enforces the limit.
586
587 >>> len(view.get_latest_uploaded_ppa_packages_with_stats) == max_results
588 True
589
590 >>> len(view.get_latest_maintained_packages_with_stats) == max_results
591 True
592
593 >>> (len(view.get_latest_uploaded_but_not_maintained_packages_with_stats)
594 ... == max_results)
595 True
596
597The view has a helper method that returns a message that can be used
598at the head of each table.
599
600 >>> view._tableHeaderMessage(100)
601 'Displaying first ... packages out of 100 total'
602
603 >>> view._tableHeaderMessage(max_results)
604 '... packages'
605
606 >>> view._tableHeaderMessage(1)
607 '1 package'
608
609
610== Person contacting another person ==
611567
612The PersonView provides information to make the link to contact a user.568The PersonView provides information to make the link to contact a user.
613No Privileges Person can send a message to Sample Person, even though569No Privileges Person can send a message to Sample Person, even though
@@ -621,75 +577,88 @@
621 >>> print view.contact_link_title577 >>> print view.contact_link_title
622 Send an email to this user through Launchpad578 Send an email to this user through Launchpad
623579
624The EmailToPersonView provides many properties to the page template580The EmailToPersonView provides many properties to the page template to
625to explain exactly who is being contacted.581explain exactly who is being contacted.
626582
627 >>> view = create_initialized_view(sample_person, '+contactuser')583 >>> view = create_initialized_view(sample_person, '+contactuser')
628 >>> print view.label584 >>> print view.label
629 Contact Sample Person585 Contact user
586
630 >>> print view.specific_contact_title_text587 >>> print view.specific_contact_title_text
631 Contact this user588 Contact this user
589
632 >>> print view.recipients.description590 >>> print view.recipients.description
633 You are contacting Sample Person (name12).591 You are contacting Sample Person (name12).
592
634 >>> [recipient.name for recipient in view.recipients]593 >>> [recipient.name for recipient in view.recipients]
635 [u'name12']594 [u'name12']
636595
637596
638== Person contacting himself ==597Person contacting himself
598-------------------------
639599
640For consistency and testing purposes, the "+contactuser" page is available600For consistency and testing purposes, the "+contactuser" page is
641even when someone is looking at his own profile page. The wording on the601available even when someone is looking at his own profile page. The
642tooltip is different though. No Privileges Person can send a message to602wording on the tooltip is different though. No Privileges Person can
643himself.603send a message to himself.
644604
645 >>> no_priv = person_set.getByEmail('no-priv@canonical.com')605 >>> no_priv = person_set.getByEmail('no-priv@canonical.com')
646 >>> view = create_initialized_view(no_priv, '+index')606 >>> view = create_initialized_view(no_priv, '+index')
647 >>> print view.contact_link_title607 >>> print view.contact_link_title
648 Send an email to yourself through Launchpad608 Send an email to yourself through Launchpad
649609
650The EmailToPersonView provides the explanation about who is being contacted.610The EmailToPersonView provides the explanation about who is being
611contacted.
651612
652 >>> view = create_initialized_view(no_priv, '+contactuser')613 >>> view = create_initialized_view(no_priv, '+contactuser')
653 >>> print view.label614 >>> print view.label
654 Contact No Privileges Person615 Contact user
616
655 >>> print view.specific_contact_title_text617 >>> print view.specific_contact_title_text
656 Contact yourself618 Contact yourself
619
657 >>> print view.recipients.description620 >>> print view.recipients.description
658 You are contacting No Privileges Person (no-priv).621 You are contacting No Privileges Person (no-priv).
622
659 >>> [recipient.name for recipient in view.recipients]623 >>> [recipient.name for recipient in view.recipients]
660 [u'no-priv']624 [u'no-priv']
661625
662626
663== Non-member contacting a Team ==627Non-member contacting a Team
628----------------------------
664629
665Users can contact teams, but the behaviour depends upon whether the630Users can contact teams, but the behaviour depends upon whether the user
666user is a member of the team. No Privileges Person is not a member of631is a member of the team. No Privileges Person is not a member of the
667the Landscape Developers team.632Landscape Developers team.
668633
669 >>> view = create_initialized_view(landscape_developers, '+index')634 >>> view = create_initialized_view(landscape_developers, '+index')
670 >>> print view.contact_link_title635 >>> print view.contact_link_title
671 Send an email to this team's owner through Launchpad636 Send an email to this team's owner through Launchpad
672637
673The EmailToPersonView can be used by non-members to contact the638The EmailToPersonView can be used by non-members to contact the team
674team owner.639owner.
675640
676 >>> view = create_initialized_view(landscape_developers, '+contactuser')641 >>> view = create_initialized_view(landscape_developers, '+contactuser')
677 >>> print view.label642 >>> print view.label
678 Contact Landscape Developers643 Contact user
644
679 >>> print view.specific_contact_title_text645 >>> print view.specific_contact_title_text
680 Contact this team646 Contact this team
647
681 >>> print view.recipients.description648 >>> print view.recipients.description
682 You are contacting the Landscape Developers (landscape-developers) team649 You are contacting the Landscape Developers (landscape-developers) team
683 owner, Sample Person (name12).650 owner, Sample Person (name12).
651
684 >>> [recipient.name for recipient in view.recipients]652 >>> [recipient.name for recipient in view.recipients]
685 [u'name12']653 [u'name12']
686654
687655
688== Member contacting a Team ==656Member contacting a Team
657------------------------
689658
690Members can contact their team. How they are contacted depends upon659Members can contact their team. How they are contacted depends upon
691whether the team's contact address is set. Sample Person can contact660whether the team's contact address is set. Sample Person can contact his
692his team, Landscape developers, even though they do not have a contact661team, Landscape developers, even though they do not have a contact
693address.662address.
694663
695 >>> login('test@canonical.com')664 >>> login('test@canonical.com')
@@ -701,12 +670,15 @@
701670
702 >>> view = create_initialized_view(landscape_developers, '+contactuser')671 >>> view = create_initialized_view(landscape_developers, '+contactuser')
703 >>> print view.label672 >>> print view.label
704 Contact Landscape Developers673 Contact user
674
705 >>> print view.specific_contact_title_text675 >>> print view.specific_contact_title_text
706 Contact your team676 Contact your team
677
707 >>> print view.recipients.description678 >>> print view.recipients.description
708 You are contacting 2 members of the Landscape Developers679 You are contacting 2 members of the Landscape Developers
709 (landscape-developers) team directly.680 (landscape-developers) team directly.
681
710 >>> [recipient.name for recipient in view.recipients]682 >>> [recipient.name for recipient in view.recipients]
711 [u'salgado', u'name12']683 [u'salgado', u'name12']
712684
@@ -715,22 +687,26 @@
715 >>> recipients = view.recipients687 >>> recipients = view.recipients
716 >>> len(recipients)688 >>> len(recipients)
717 2689 2
690
718 >>> bool(recipients)691 >>> bool(recipients)
719 True692 True
720693
721If there is only one member of the team, who must therefore be the user694If there is only one member of the team, who must therefore be the user
722sending the email, and also be the team owner, The view provides a special695sending the email, and also be the team owner, The view provides a
723message just for him.696special message just for him.
724697
725 >>> vanity_team = factory.makeTeam(698 >>> vanity_team = factory.makeTeam(
726 ... sample_person, displayname='Vanity', name='vanity')699 ... sample_person, displayname='Vanity', name='vanity')
727 >>> view = create_initialized_view(vanity_team, '+contactuser')700 >>> view = create_initialized_view(vanity_team, '+contactuser')
728 >>> print view.label701 >>> print view.label
729 Contact Vanity702 Contact user
703
730 >>> print view.specific_contact_title_text704 >>> print view.specific_contact_title_text
731 Contact your team705 Contact your team
706
732 >>> print view.recipients.description707 >>> print view.recipients.description
733 You are contacting 1 member of the Vanity (vanity) team directly.708 You are contacting 1 member of the Vanity (vanity) team directly.
709
734 >>> [recipient.name for recipient in view.recipients]710 >>> [recipient.name for recipient in view.recipients]
735 [u'name12']711 [u'name12']
736712
@@ -741,39 +717,39 @@
741 >>> landscape_developers.setContactAddress(email_address)717 >>> landscape_developers.setContactAddress(email_address)
742718
743 >>> view = create_initialized_view(landscape_developers, '+contactuser')719 >>> view = create_initialized_view(landscape_developers, '+contactuser')
744 >>> print view.label
745 Contact Landscape Developers
746 >>> print view.specific_contact_title_text
747 Contact your team
748 >>> print view.recipients.description720 >>> print view.recipients.description
749 You are contacting the Landscape Developers (landscape-developers) team.721 You are contacting the Landscape Developers (landscape-developers) team.
722
750 >>> [recipient.name for recipient in view.recipients]723 >>> [recipient.name for recipient in view.recipients]
751 [u'landscape-developers']724 [u'landscape-developers']
752725
753726
754== Contact this user/team valid addresses and quotas ==727Contact this user/team valid addresses and quotas
728-------------------------------------------------
755729
756The EmailToPersonView has_valid_email_address property is normally730The EmailToPersonView has_valid_email_address property is normally True.
757True. The is_possible property is True when contact_is_allowed and731The is_possible property is True when contact_is_allowed and
758has_valid_email_address are both True.732has_valid_email_address are both True.
759733
760 >>> view = create_initialized_view(landscape_developers, '+contactuser')734 >>> view = create_initialized_view(landscape_developers, '+contactuser')
761 >>> view.has_valid_email_address735 >>> view.has_valid_email_address
762 True736 True
737
763 >>> view.contact_is_possible738 >>> view.contact_is_possible
764 True739 True
765740
766The EmailToPersonView provides two properties that check that the user741The EmailToPersonView provides two properties that check that the user
767is_allowed to send emails because he has not exceeded the daily quota. The742is_allowed to send emails because he has not exceeded the daily quota.
768next_try property is the date when the user will be allowed to send emails743The next_try property is the date when the user will be allowed to send
769again. The is_possible property is True when both contact_is_allowed and744emails again. The is_possible property is True when both
770as_valid_email_address are True.745contact_is_allowed and as_valid_email_address are True.
771746
772The daily quota is set to 3 emails per day. See the "Message quota"747The daily quota is set to 3 emails per day. See the "Message quota" in
773in `doc/user-to-user.txt` to see how these two attributes are used.748`doc/user-to-user.txt` to see how these two attributes are used.
774749
775750
776== Invalid users and anonymous contacters ==751Invalid users and anonymous contacters
752--------------------------------------
777753
778Inactive users and users without a preferred email address are invalid754Inactive users and users without a preferred email address are invalid
779and cannot be contacted.755and cannot be contacted.
@@ -782,11 +758,14 @@
782 >>> view = create_initialized_view(former_user, '+contactuser')758 >>> view = create_initialized_view(former_user, '+contactuser')
783 >>> view.request.response.getStatus()759 >>> view.request.response.getStatus()
784 302760 302
761
785 >>> print view.request.response.getHeader('Location')762 >>> print view.request.response.getHeader('Location')
786 http://launchpad.dev/~former-user-deactivatedaccount763 http://launchpad.dev/~former-user-deactivatedaccount
764
787 >>> recipients = view.recipients765 >>> recipients = view.recipients
788 >>> len(recipients)766 >>> len(recipients)
789 0767 0
768
790 >>> bool(recipients)769 >>> bool(recipients)
791 False770 False
792771
@@ -798,14 +777,16 @@
798 >>> view = create_initialized_view(landscape_developers, '+contactuser')777 >>> view = create_initialized_view(landscape_developers, '+contactuser')
799 >>> view.request.response.getStatus()778 >>> view.request.response.getStatus()
800 302779 302
780
801 >>> print view.request.response.getHeader('Location')781 >>> print view.request.response.getHeader('Location')
802 http://launchpad.dev/~landscape-developers782 http://launchpad.dev/~landscape-developers
803783
804784
805== Messages and subjects cannot be empty ==785Messages and subjects cannot be empty
786-------------------------------------
806787
807Messages or subjects that contain only whitespace are treated as an error788Messages or subjects that contain only whitespace are treated as an
808that the user must fix.789error that the user must fix.
809790
810 >>> login('test@canonical.com')791 >>> login('test@canonical.com')
811 >>> view = create_initialized_view(792 >>> view = create_initialized_view(
@@ -819,13 +800,15 @@
819 [u'You must provide a subject and a message.']800 [u'You must provide a subject and a message.']
820801
821802
822== Person +index "Personal package archives" section ==803Person +index "Personal package archives" section
804-------------------------------------------------
823805
824The person:+index page has a section titled "Personal package806The person:+index page has a section titled "Personal package archives",
825archives", which is conditionally displayed depending on the value of the807which is conditionally displayed depending on the value of the view
826view property `should_show_ppa_section`.808property `should_show_ppa_section`.
827809
828The property checks two things to decide whether to return True or not:810The property checks two things to decide whether to return True or not:
811
829 * Return True if the current user has launchpad.Edit permission812 * Return True if the current user has launchpad.Edit permission
830 * Return True if the person has PPAs and at least one of them is viewable813 * Return True if the person has PPAs and at least one of them is viewable
831 by the current user.814 by the current user.
@@ -879,9 +862,8 @@
879 >>> view.should_show_ppa_section862 >>> view.should_show_ppa_section
880 False863 False
881864
882For a user with no PPAs, nobody will see the section apart from865For a user with no PPAs, nobody will see the section apart from himself.
883himself. This aspect allows him to access the 'Create a new PPA'866This aspect allows him to access the 'Create a new PPA' link.
884link.
885867
886 >>> print sample_person.archive868 >>> print sample_person.archive
887 None869 None
@@ -896,15 +878,22 @@
896 >>> view.should_show_ppa_section878 >>> view.should_show_ppa_section
897 False879 False
898880
899If the person is a member of teams with PPAs but doesn't own any himself, the881If the person is a member of teams with PPAs but doesn't own any
900section will still not appear for anyone but people with lp.edit.882himself, the section will still not appear for anyone but people with
883lp.edit.
884
885 >>> from canonical.launchpad.interfaces.launchpad import (
886 ... ILaunchpadCelebrities)
901887
902 >>> login("admin@canonical.com")888 >>> login("admin@canonical.com")
903 >>> team = factory.makeTeam()889 >>> team = factory.makeTeam()
904 >>> ignored = team.addMember(sample_person, sample_person)890 >>> ignored = team.addMember(sample_person, sample_person)
891 >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
905 >>> ppa = factory.makeArchive(distribution=ubuntu, owner=team)892 >>> ppa = factory.makeArchive(distribution=ubuntu, owner=team)
906893
907 >>> login(ANONYMOUS)894 >>> login(ANONYMOUS)
908 >>> view = create_initialized_view(sample_person, "+index")895 >>> view = create_initialized_view(sample_person, "+index")
909 >>> view.should_show_ppa_section896 >>> view.should_show_ppa_section
910 False897 False
898
899
911900
=== added file 'lib/lp/registry/browser/tests/test_branding.py'
--- lib/lp/registry/browser/tests/test_branding.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_branding.py 2010-07-14 21:22:45 +0000
@@ -0,0 +1,33 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for Branding."""
5
6__metaclass__ = type
7
8import unittest
9
10from canonical.launchpad.webapp.servers import LaunchpadTestRequest
11from canonical.testing.layers import DatabaseFunctionalLayer
12from lp.registry.browser.branding import BrandingChangeView
13from lp.testing import TestCaseWithFactory
14
15
16class TestBrandingChangeView(TestCaseWithFactory):
17
18 layer = DatabaseFunctionalLayer
19
20 def setUp(self):
21 super(TestBrandingChangeView, self).setUp()
22 self.context = self.factory.makePerson(name='cow')
23 self.view = BrandingChangeView(self.context, LaunchpadTestRequest())
24
25 def test_common_attributes(self):
26 # The canonical URL of a GPG key is ssh-keys
27 label = 'Change the images used to represent Cow in Launchpad'
28 self.assertEqual(label, self.view.label)
29 self.assertEqual('Change branding', self.view.page_title)
30
31
32def test_suite():
33 return unittest.TestLoader().loadTestsFromName(__name__)
034
=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py 2010-06-18 15:06:32 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py 2010-07-14 21:22:45 +0000
@@ -8,7 +8,9 @@
8import transaction8import transaction
9from zope.component import getUtility9from zope.component import getUtility
1010
11from canonical.config import config
11from canonical.launchpad.ftests import ANONYMOUS, login12from canonical.launchpad.ftests import ANONYMOUS, login
13from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
12from canonical.launchpad.webapp.interfaces import NotFoundError14from canonical.launchpad.webapp.interfaces import NotFoundError
13from canonical.launchpad.webapp.servers import LaunchpadTestRequest15from canonical.launchpad.webapp.servers import LaunchpadTestRequest
14from canonical.testing import (16from canonical.testing import (
@@ -21,8 +23,9 @@
21from lp.registry.model.karma import KarmaCategory23from lp.registry.model.karma import KarmaCategory
22from lp.soyuz.tests.test_publishing import SoyuzTestPublisher24from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
23from lp.soyuz.interfaces.archive import ArchiveStatus25from lp.soyuz.interfaces.archive import ArchiveStatus
26from lp.soyuz.interfaces.publishing import PackagePublishingStatus
24from lp.testing import TestCaseWithFactory, login_person27from lp.testing import TestCaseWithFactory, login_person
25from lp.testing.views import create_view28from lp.testing.views import create_initialized_view, create_view
2629
2730
28class TestPersonViewKarma(TestCaseWithFactory):31class TestPersonViewKarma(TestCaseWithFactory):
@@ -357,6 +360,149 @@
357 self.assertEqual(True, self.view.has_participations)360 self.assertEqual(True, self.view.has_participations)
358361
359362
363class TestPersonRelatedSoftwareView(TestCaseWithFactory):
364 """Test the related software view."""
365
366 layer = LaunchpadFunctionalLayer
367
368 def setUp(self):
369 super(TestPersonRelatedSoftwareView, self).setUp()
370 self.user = self.factory.makePerson()
371 self.factory.makeGPGKey(self.user)
372 self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
373 self.warty = self.ubuntu.getSeries('warty')
374 self.view = create_initialized_view(self.user, '+related-software')
375
376 def publishSource(self, archive, maintainer):
377 publisher = SoyuzTestPublisher()
378 publisher.person = self.user
379 login('foo.bar@canonical.com')
380 for count in range(0, self.view.max_results_to_display + 3):
381 source_name = "foo" + str(count)
382 publisher.getPubSource(
383 sourcename=source_name,
384 status=PackagePublishingStatus.PUBLISHED,
385 archive=archive,
386 maintainer = maintainer,
387 creator = self.user,
388 distroseries=self.warty)
389 login(ANONYMOUS)
390
391 def test_view_helper_attributes(self):
392 # Verify view helper attributes.
393 self.assertEqual('Related software', self.view.page_title)
394 self.assertEqual('summary_list_size', self.view._max_results_key)
395 self.assertEqual(
396 config.launchpad.summary_list_size,
397 self.view.max_results_to_display)
398
399 def test_tableHeaderMessage(self):
400 limit = self.view.max_results_to_display
401 expected = 'Displaying first %s packages out of 100 total' % limit
402 self.assertEqual(expected, self.view._tableHeaderMessage(100))
403 expected = '%s packages' % limit
404 self.assertEqual(expected, self.view._tableHeaderMessage(limit))
405 expected = '1 package'
406 self.assertEqual(expected, self.view._tableHeaderMessage(1))
407
408 def test_latest_uploaded_ppa_packages_with_stats(self):
409 # Verify number of PPA packages to display.
410 ppa = self.factory.makeArchive(owner=self.user)
411 self.publishSource(ppa, self.user)
412 count = len(self.view.latest_uploaded_ppa_packages_with_stats)
413 self.assertEqual(self.view.max_results_to_display, count)
414
415 def test_latest_maintained_packages_with_stats(self):
416 # Verify number of maintained packages to display.
417 self.publishSource(self.warty.main_archive, self.user)
418 count = len(self.view.latest_maintained_packages_with_stats)
419 self.assertEqual(self.view.max_results_to_display, count)
420
421 def test_latest_uploaded_nonmaintained_packages_with_stats(self):
422 # Verify number of non maintained packages to display.
423 maintainer = self.factory.makePerson()
424 self.publishSource(self.warty.main_archive, maintainer)
425 count = len(
426 self.view.latest_uploaded_but_not_maintained_packages_with_stats)
427 self.assertEqual(self.view.max_results_to_display, count)
428
429
430class TestPersonMaintainedPackagesView(TestCaseWithFactory):
431 """Test the maintained packages view."""
432
433 layer = DatabaseFunctionalLayer
434
435 def setUp(self):
436 super(TestPersonMaintainedPackagesView, self).setUp()
437 self.user = self.factory.makePerson()
438 self.view = create_initialized_view(self.user, '+maintained-packages')
439
440 def test_view_helper_attributes(self):
441 # Verify view helper attributes.
442 self.assertEqual('Maintained Packages', self.view.page_title)
443 self.assertEqual('default_batch_size', self.view._max_results_key)
444 self.assertEqual(
445 config.launchpad.default_batch_size,
446 self.view.max_results_to_display)
447
448
449class TestPersonUploadedPackagesView(TestCaseWithFactory):
450 """Test the maintained packages view."""
451
452 layer = DatabaseFunctionalLayer
453
454 def setUp(self):
455 super(TestPersonUploadedPackagesView, self).setUp()
456 self.user = self.factory.makePerson()
457 self.view = create_initialized_view(self.user, '+uploaded-packages')
458
459 def test_view_helper_attributes(self):
460 # Verify view helper attributes.
461 self.assertEqual('Uploaded packages', self.view.page_title)
462 self.assertEqual('default_batch_size', self.view._max_results_key)
463 self.assertEqual(
464 config.launchpad.default_batch_size,
465 self.view.max_results_to_display)
466
467
468class TestPersonPPAPackagesView(TestCaseWithFactory):
469 """Test the maintained packages view."""
470
471 layer = DatabaseFunctionalLayer
472
473 def setUp(self):
474 super(TestPersonPPAPackagesView, self).setUp()
475 self.user = self.factory.makePerson()
476 self.view = create_initialized_view(self.user, '+ppa-packages')
477
478 def test_view_helper_attributes(self):
479 # Verify view helper attributes.
480 self.assertEqual('PPA packages', self.view.page_title)
481 self.assertEqual('default_batch_size', self.view._max_results_key)
482 self.assertEqual(
483 config.launchpad.default_batch_size,
484 self.view.max_results_to_display)
485
486
487class TestPersonRelatedProjectsView(TestCaseWithFactory):
488 """Test the maintained packages view."""
489
490 layer = DatabaseFunctionalLayer
491
492 def setUp(self):
493 super(TestPersonRelatedProjectsView, self).setUp()
494 self.user = self.factory.makePerson()
495 self.view = create_initialized_view(self.user, '+related-projects')
496
497 def test_view_helper_attributes(self):
498 # Verify view helper attributes.
499 self.assertEqual('Related projects', self.view.page_title)
500 self.assertEqual('default_batch_size', self.view._max_results_key)
501 self.assertEqual(
502 config.launchpad.default_batch_size,
503 self.view.max_results_to_display)
504
505
360class TestPersonRelatedSoftwareFailedBuild(TestCaseWithFactory):506class TestPersonRelatedSoftwareFailedBuild(TestCaseWithFactory):
361 """The related software views display links to failed builds."""507 """The related software views display links to failed builds."""
362508
363509
=== modified file 'lib/lp/registry/browser/tests/user-to-user-views.txt'
--- lib/lp/registry/browser/tests/user-to-user-views.txt 2009-08-22 16:51:26 +0000
+++ lib/lp/registry/browser/tests/user-to-user-views.txt 2010-07-14 21:22:45 +0000
@@ -1,7 +1,8 @@
1= User-to-user direct email contact =1User-to-user direct email contact
2=================================
23
3A Launchpad user can contact another Launchpad user directly, even if the4A Launchpad user can contact another Launchpad user directly, even if
4recipient is hiding their email addresses.5the recipient is hiding their email addresses.
56
6 >>> def create_view(sender, recipient, form=None):7 >>> def create_view(sender, recipient, form=None):
7 ... return create_initialized_view(8 ... return create_initialized_view(
@@ -29,7 +30,8 @@
29This contact is allowed.30This contact is allowed.
3031
31 >>> print view.label32 >>> print view.label
32 Contact Guilherme Salgado33 Contact user
34
33 >>> view.contact_is_allowed35 >>> view.contact_is_allowed
34 True36 True
3537
@@ -51,6 +53,7 @@
51 Message sent to Guilherme Salgado53 Message sent to Guilherme Salgado
5254
53 # Capture the date of the last contact for later.55 # Capture the date of the last contact for later.
56
54 >>> from canonical.config import config57 >>> from canonical.config import config
55 >>> from canonical.launchpad.database.message import UserToUserEmail58 >>> from canonical.launchpad.database.message import UserToUserEmail
56 >>> from lazr.config import as_timedelta59 >>> from lazr.config import as_timedelta
@@ -61,30 +64,31 @@
61 >>> expires = first_contact.date_sent + as_timedelta(64 >>> expires = first_contact.date_sent + as_timedelta(
62 ... config.launchpad.user_to_user_throttle_interval)65 ... config.launchpad.user_to_user_throttle_interval)
6366
64No Priv sends two more messages to Salgado. Each of these are allowed too.67No Priv sends two more messages to Salgado. Each of these are allowed
6568too.
66 >>> view = create_view(69
67 ... no_priv, salgado, {70 >>> view = create_view(
68 ... 'field.field.from_': 'no-priv@canonical.com',71 ... no_priv, salgado, {
69 ... 'field.subject': 'Hello Salgado',72 ... 'field.field.from_': 'no-priv@canonical.com',
70 ... 'field.message': 'Can you tell me about your project?',73 ... 'field.subject': 'Hello Salgado',
71 ... 'field.actions.send': 'Send',74 ... 'field.message': 'Can you tell me about your project?',
72 ... })75 ... 'field.actions.send': 'Send',
73 >>> print_notifications(view)76 ... })
74 Message sent to Guilherme Salgado77 >>> print_notifications(view)
7578 Message sent to Guilherme Salgado
76 >>> view = create_view(79
77 ... no_priv, salgado, {80 >>> view = create_view(
78 ... 'field.field.from_': 'no-priv@canonical.com',81 ... no_priv, salgado, {
79 ... 'field.subject': 'Hello Salgado',82 ... 'field.field.from_': 'no-priv@canonical.com',
80 ... 'field.message': 'Can you tell me about your project?',83 ... 'field.subject': 'Hello Salgado',
81 ... 'field.actions.send': 'Send',84 ... 'field.message': 'Can you tell me about your project?',
82 ... })85 ... 'field.actions.send': 'Send',
83 >>> print_notifications(view)86 ... })
84 Message sent to Guilherme Salgado87 >>> print_notifications(view)
8588 Message sent to Guilherme Salgado
86Now however, No Priv had reached her quota for direct user-to-user contact and89
87is not allowed to send a fourth message today.90Now however, No Priv had reached her quota for direct user-to-user
91contact and is not allowed to send a fourth message today.
8892
89 >>> view = create_view(no_priv, salgado)93 >>> view = create_view(no_priv, salgado)
90 >>> view.contact_is_allowed94 >>> view.contact_is_allowed
@@ -95,13 +99,13 @@
95 >>> view.next_try == expires99 >>> view.next_try == expires
96 True100 True
97101
98As a corner case, let's say the number of notifications allowed was greater102As a corner case, let's say the number of notifications allowed was
99yesterday than it was today.103greater yesterday than it was today.
100104
101 >>> config.push('seven_allowed', """\105 >>> config.push('seven_allowed', """\
102 ... [launchpad]106 ... [launchpad]
103 ... user_to_user_max_messages: 7107 ... user_to_user_max_messages: 7
104 ... """)108 ... """)
105109
106No Priv can actually try again right now.110No Priv can actually try again right now.
107111
@@ -133,6 +137,7 @@
133 >>> view = create_view(no_priv, salgado)137 >>> view = create_view(no_priv, salgado)
134 >>> view.contact_is_allowed138 >>> view.contact_is_allowed
135 False139 False
140
136 >>> view.next_try == expires141 >>> view.next_try == expires
137 True142 True
138143
@@ -140,6 +145,7 @@
140145
141 >>> config.pop('seven_allowed')146 >>> config.pop('seven_allowed')
142 (...)147 (...)
148
143 >>> contacts = Store.of(no_priv).find(149 >>> contacts = Store.of(no_priv).find(
144 ... UserToUserEmail,150 ... UserToUserEmail,
145 ... UserToUserEmail.sender == no_priv)151 ... UserToUserEmail.sender == no_priv)
@@ -148,10 +154,11 @@
148 ... config.launchpad.user_to_user_throttle_interval)154 ... config.launchpad.user_to_user_throttle_interval)
149155
150156
151== Non-ASCII names ==157Non-ASCII names
158---------------
152159
153Carlos has non-ASCII characters in his name. When he sends a message to a160Carlos has non-ASCII characters in his name. When he sends a message to
154user, his real name will be properly RFC 2047 encoded.161a user, his real name will be properly RFC 2047 encoded.
155162
156 >>> transaction.abort()163 >>> transaction.abort()
157 >>> from lp.services.mail import stub164 >>> from lp.services.mail import stub
@@ -172,6 +179,7 @@
172179
173 >>> len(stub.test_emails)180 >>> len(stub.test_emails)
174 1181 1
182
175 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()183 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
176 >>> print raw_msg184 >>> print raw_msg
177 Content-Type: text/plain; charset="us-ascii"185 Content-Type: text/plain; charset="us-ascii"
@@ -180,8 +188,8 @@
180 To: No Privileges Person <no-priv@canonical.com>188 To: No Privileges Person <no-priv@canonical.com>
181 ...189 ...
182190
183Similarly, if Carlos is the recipient of a message, his real name will be191Similarly, if Carlos is the recipient of a message, his real name will
184properly RFC 2047 encoded as well.192be properly RFC 2047 encoded as well.
185193
186 >>> del stub.test_emails[:]194 >>> del stub.test_emails[:]
187195
@@ -197,6 +205,7 @@
197205
198 >>> len(stub.test_emails)206 >>> len(stub.test_emails)
199 1207 1
208
200 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()209 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
201 >>> print raw_msg210 >>> print raw_msg
202 Content-Type: text/plain; charset="us-ascii"211 Content-Type: text/plain; charset="us-ascii"
@@ -206,7 +215,8 @@
206 ...215 ...
207216
208217
209== Hidden addresses ==218Hidden addresses
219----------------
210220
211Salgado decides to hide his email addresses.221Salgado decides to hide his email addresses.
212222
@@ -230,13 +240,15 @@
230 Message sent to Guilherme Salgado240 Message sent to Guilherme Salgado
231241
232242
233== Contacting teams ==243Contacting teams
244----------------
234245
235Teams can also be contacted directly, regardless of whether they have no246Teams can also be contacted directly, regardless of whether they have no
236official contact address, use a Launchpad mailing list, or have the contact247official contact address, use a Launchpad mailing list, or have the
237address set to an explicit address.248contact address set to an explicit address.
238249
239 # Clear out left over crud.250 # Clear out left over crud.
251
240 >>> transaction.commit()252 >>> transaction.commit()
241 >>> del stub.test_emails[:]253 >>> del stub.test_emails[:]
242254
@@ -266,7 +278,8 @@
266 ... print ' ', recipient278 ... print ' ', recipient
267279
268280
269=== Non-member to team ===281Non-member to team
282..................
270283
271Non-members may only contact the team owner.284Non-members may only contact the team owner.
272285
@@ -301,7 +314,8 @@
301 Foo Bar <foo.bar@canonical.com>314 Foo Bar <foo.bar@canonical.com>
302315
303316
304=== Member to team ===317Member to team
318..............
305319
306Foo Bar is a member of Guadamen team, he is not restricted to contacting320Foo Bar is a member of Guadamen team, he is not restricted to contacting
307the team owner. The Guadamen team has no contact address, so contacting321the team owner. The Guadamen team has no contact address, so contacting
@@ -319,9 +333,9 @@
319 >>> print_notifications(view)333 >>> print_notifications(view)
320 Message sent to GuadaMen334 Message sent to GuadaMen
321335
322There are 10 members of the team, so exactly 10 unique copies of the message336There are 10 members of the team, so exactly 10 unique copies of the
323are sent, one to each team member. Everyone gets a message with the same337message are sent, one to each team member. Everyone gets a message with
324subject and body from the same sender.338the same subject and body from the same sender.
325339
326 >>> transaction.commit()340 >>> transaction.commit()
327 >>> print_messages()341 >>> print_messages()
@@ -356,12 +370,13 @@
356 ... guadamen.name, guadamen.teamowner.name)370 ... guadamen.name, guadamen.teamowner.name)
357371
358 # Ignore the 'new mailing list message'372 # Ignore the 'new mailing list message'
373
359 >>> transaction.commit()374 >>> transaction.commit()
360 >>> del stub.test_emails[:]375 >>> del stub.test_emails[:]
361376
362Foo Bar now contacts them again, which he can do because his quota is still377Foo Bar now contacts them again, which he can do because his quota is
363not met. This message includes a "%s" combination; it is not a interpolation378still not met. This message includes a "%s" combination; it is not a
364instruction.379interpolation instruction.
365380
366 >>> view = create_view(381 >>> view = create_view(
367 ... foo_bar, guadamen, {382 ... foo_bar, guadamen, {
@@ -410,9 +425,9 @@
410 >>> address = email_set.new('guadamen@example.com', guadamen)425 >>> address = email_set.new('guadamen@example.com', guadamen)
411 >>> guadamen.setContactAddress(address)426 >>> guadamen.setContactAddress(address)
412427
413Foo Bar contacts the Guadamen team again, which is allowed because his quota428Foo Bar contacts the Guadamen team again, which is allowed because his
414was not met by his first message. This time only one message is sent, and429quota was not met by his first message. This time only one message is
415that to the new contact address.430sent, and that to the new contact address.
416431
417 >>> view = create_view(432 >>> view = create_view(
418 ... foo_bar, guadamen, {433 ... foo_bar, guadamen, {
@@ -427,9 +442,11 @@
427 >>> transaction.commit()442 >>> transaction.commit()
428 >>> len(stub.test_emails)443 >>> len(stub.test_emails)
429 1444 1
445
430 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()446 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
431 >>> print from_addr, to_addrs447 >>> print from_addr, to_addrs
432 bounces@canonical.com [u'GuadaMen <guadamen@example.com>']448 bounces@canonical.com [u'GuadaMen <guadamen@example.com>']
449
433 >>> print raw_msg450 >>> print raw_msg
434 Content-Type: text/plain; charset="us-ascii"451 Content-Type: text/plain; charset="us-ascii"
435 ...452 ...
@@ -449,14 +466,17 @@
449 https://help.launchpad.net/YourAccount/ContactingPeople466 https://help.launchpad.net/YourAccount/ContactingPeople
450467
451468
452== Message quota ==469Message quota
470-------------
453471
454The EmailToPersonView provides two properties that check that the user472The EmailToPersonView provides two properties that check that the user
455is_allowed to send emails because he has not exceeded the daily quota. The473is_allowed to send emails because he has not exceeded the daily quota.
456next_try property is the date when the user will be allowed to send emails474The next_try property is the date when the user will be allowed to send
457again. The is_possible property will be False if is_allowed is False.475emails again. The is_possible property will be False if is_allowed is
476False.
458477
459Foo Bar has now reached his quota and can send no more contact messages today.478Foo Bar has now reached his quota and can send no more contact messages
479today.
460480
461 >>> view = create_view(481 >>> view = create_view(
462 ... foo_bar, guadamen, {482 ... foo_bar, guadamen, {
@@ -467,10 +487,13 @@
467 ... })487 ... })
468 >>> view.contact_is_allowed488 >>> view.contact_is_allowed
469 False489 False
490
470 >>> view.next_try491 >>> view.next_try
471 datetime.datetime...492 datetime.datetime...
493
472 >>> view.contact_is_possible494 >>> view.contact_is_possible
473 False495 False
496
474 >>> print_notifications(view)497 >>> print_notifications(view)
475 Your message was not sent because you have exceeded your daily quota of498 Your message was not sent because you have exceeded your daily quota of
476 3 messages to contact users. Try again in ...499 3 messages to contact users. Try again in ...
@@ -481,15 +504,17 @@
481 >>> view = create_view(bart, guadamen)504 >>> view = create_view(bart, guadamen)
482 >>> view.contact_is_allowed505 >>> view.contact_is_allowed
483 True506 True
507
484 >>> view.contact_is_possible508 >>> view.contact_is_possible
485 True509 True
486510
487511
488== Identifying information ==512Identifying information
513-----------------------
489514
490Every contact message has a special Launchpad header so that people can tell515Every contact message has a special Launchpad header so that people can
491that the message came to them through Launchpad. It has a footer that516tell that the message came to them through Launchpad. It has a footer
492contains an explanation as well.517that contains an explanation as well.
493518
494 >>> cris = factory.makePerson(email='cris@example.com', name='cris')519 >>> cris = factory.makePerson(email='cris@example.com', name='cris')
495 >>> dave = factory.makePerson(email='dave@example.com', name='dave')520 >>> dave = factory.makePerson(email='dave@example.com', name='dave')
@@ -523,11 +548,12 @@
523 https://help.launchpad.net/YourAccount/ContactingPeople548 https://help.launchpad.net/YourAccount/ContactingPeople
524549
525550
526== Message wrapping ==551Message wrapping
552----------------
527553
528The message body is wrapped at 72 characters. The footer is not wrapped, but554The message body is wrapped at 72 characters. The footer is not wrapped,
529a new line is started after the names of the sender and the recient to555but a new line is started after the names of the sender and the recient
530minimise long lines.556to minimise long lines.
531557
532 >>> login('test@canonical.com')558 >>> login('test@canonical.com')
533 >>> sample_person = person_set.getByEmail('test@canonical.com')559 >>> sample_person = person_set.getByEmail('test@canonical.com')
@@ -554,3 +580,4 @@
554 ^For more information see$580 ^For more information see$
555 ^https://help.launchpad.net/YourAccount/ContactingPeople$581 ^https://help.launchpad.net/YourAccount/ContactingPeople$
556582
583
557584
=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt 2010-04-07 12:50:17 +0000
+++ lib/lp/registry/doc/person-account.txt 2010-07-14 21:22:45 +0000
@@ -1,10 +1,12 @@
1= Person and Account =1Person and Account
2==================
23
3The Person object is responsible for updating the status of its4The Person object is responsible for updating the status of its
4Account object.5Account object.
56
67
7== Activating user accounts ==8Activating user accounts
9------------------------
810
9A user may activate their account that was created by an automated11A user may activate their account that was created by an automated
10process. Matsubara's account was created during a code import.12process. Matsubara's account was created during a code import.
@@ -54,7 +56,8 @@
54 u'matsubara@async.com.br'56 u'matsubara@async.com.br'
5557
5658
57== Deactivating user accounts ==59Deactivating user accounts
60--------------------------
5861
59Any user can deactivate his own account, in case they don't want it62Any user can deactivate his own account, in case they don't want it
60anymore or they don't want to be shown as Launchpad users.63anymore or they don't want to be shown as Launchpad users.
@@ -182,7 +185,7 @@
182185
183...no owned or driven pillars...186...no owned or driven pillars...
184187
185 >>> foobar.getOwnedOrDrivenPillars().count()188 >>> len(foobar.getOwnedOrDrivenPillars())
186 0189 0
187190
188...and, finally, to not be considered a valid person in Launchpad.191...and, finally, to not be considered a valid person in Launchpad.
@@ -206,7 +209,8 @@
206 True209 True
207210
208211
209== Reactivating user accounts ==212Reactivating user accounts
213--------------------------
210214
211Accounts can be reactivated. A comment and a non-None preferred email address215Accounts can be reactivated. A comment and a non-None preferred email address
212are required to reactivate() an account, though.216are required to reactivate() an account, though.
213217
=== modified file 'lib/lp/registry/doc/person.txt'
--- lib/lp/registry/doc/person.txt 2010-04-23 16:54:21 +0000
+++ lib/lp/registry/doc/person.txt 2010-07-14 21:22:45 +0000
@@ -1132,25 +1132,22 @@
1132 ... elif IProduct.providedBy(pillar):1132 ... elif IProduct.providedBy(pillar):
1133 ... pillar_type = 'project'1133 ... pillar_type = 'project'
1134 ... print "%s: %s (%s)" % (1134 ... print "%s: %s (%s)" % (
1135 ... pillar_type, pillar.title, pillar.name)1135 ... pillar_type, pillar.displayname, pillar.name)
11361136
1137 >>> for pillarname in mark.getOwnedOrDrivenPillars():1137 >>> for pillarname in mark.getOwnedOrDrivenPillars():
1138 ... print_pillar(pillarname)1138 ... print_pillar(pillarname)
1139 distribution: Ubuntu Linux (ubuntu)1139 distribution: Debian (debian)
1140 distribution: Redhat Advanced Server (redhat)1140 distribution: Gentoo (gentoo)
1141 distribution: Debian GNU/Linux (debian)1141 distribution: Kubuntu (kubuntu)
1142 distribution: The Gentoo Linux (gentoo)1142 distribution: Red Hat (redhat)
1143 distribution: Kubuntu - Free KDE-based Linux (kubuntu)
1144 distribution: Ubuntu Test (ubuntutest)
1145 project group: Apache (apache)1143 project group: Apache (apache)
1146 project: Tomcat (tomcat)1144 project: Derby (derby)
1147 project: ALSA utilities (alsa-utils)1145 project: alsa-utils (alsa-utils)
1148 project: Derby - Java Database (derby)
11491146
1150 >>> for pillarname in ubuntu_team.getOwnedOrDrivenPillars():1147 >>> for pillarname in ubuntu_team.getOwnedOrDrivenPillars():
1151 ... print_pillar(pillarname)1148 ... print_pillar(pillarname)
1152 distribution: Ubuntu Linux (ubuntu)1149 distribution: Ubuntu (ubuntu)
1153 distribution: Ubuntu Test (ubuntutest)1150 distribution: ubuntutest (ubuntutest)
1154 project: Tomcat (tomcat)1151 project: Tomcat (tomcat)
11551152
11561153
11571154
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2010-07-13 15:29:08 +0000
+++ lib/lp/registry/interfaces/person.py 2010-07-14 21:22:45 +0000
@@ -999,9 +999,7 @@
999 """999 """
10001000
1001 def getOwnedOrDrivenPillars():1001 def getOwnedOrDrivenPillars():
1002 """Return Distribution, Project Groups and Projects that this person1002 """Return the pillars that this person directly owns or drives."""
1003 owns or drives.
1004 """
10051003
1006 def getOwnedProjects(match_name=None):1004 def getOwnedProjects(match_name=None):
1007 """Projects owned by this person or teams to which she belongs.1005 """Projects owned by this person or teams to which she belongs.
@@ -2130,6 +2128,7 @@
2130 requiring a Launchpad account.2128 requiring a Launchpad account.
2131 """2129 """
21322130
2131
2133class ISoftwareCenterAgentApplication(ILaunchpadApplication):2132class ISoftwareCenterAgentApplication(ILaunchpadApplication):
2134 """XMLRPC application root for ISoftwareCenterAgentAPI."""2133 """XMLRPC application root for ISoftwareCenterAgentAPI."""
21352134
21362135
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-07-14 19:33:16 +0000
+++ lib/lp/registry/model/person.py 2010-07-14 21:22:45 +0000
@@ -186,7 +186,6 @@
186186
187 This is readonly, as this is a view in the database.187 This is readonly, as this is a view in the database.
188 """188 """
189 # Look Ma, no columns! (apart from id)
190189
191190
192def validate_person_visibility(person, attr, value):191def validate_person_visibility(person, attr, value):
@@ -725,7 +724,7 @@
725724
726 # filter based on completion. see the implementation of725 # filter based on completion. see the implementation of
727 # Specification.is_complete() for more details726 # Specification.is_complete() for more details
728 completeness = Specification.completeness_clause727 completeness = Specification.completeness_clause
729728
730 if SpecificationFilter.COMPLETE in filter:729 if SpecificationFilter.COMPLETE in filter:
731 query += ' AND ( %s ) ' % completeness730 query += ' AND ( %s ) ' % completeness
@@ -901,40 +900,33 @@
901900
902 def getOwnedOrDrivenPillars(self):901 def getOwnedOrDrivenPillars(self):
903 """See `IPerson`."""902 """See `IPerson`."""
904 query = """903 find_spec = (PillarName, SQL('kind'), SQL('displayname'))
905 SELECT name904 origin = SQL("""
906 FROM product, teamparticipation905 PillarName
907 WHERE teamparticipation.person = %(person)s906 JOIN (
908 AND (driver = teamparticipation.team907 SELECT name, 3 as kind, displayname
909 OR owner = teamparticipation.team)908 FROM product
910909 WHERE
911 UNION910 driver = %(person)s
912911 OR owner = %(person)s
913 SELECT name912 UNION
914 FROM project, teamparticipation913 SELECT name, 2 as kind, displayname
915 WHERE teamparticipation.person = %(person)s914 FROM project
916 AND (driver = teamparticipation.team915 WHERE
917 OR owner = teamparticipation.team)916 driver = %(person)s
918917 OR owner = %(person)s
919 UNION918 UNION
920919 SELECT name, 1 as kind, displayname
921 SELECT name920 FROM distribution
922 FROM distribution, teamparticipation921 WHERE
923 WHERE teamparticipation.person = %(person)s922 driver = %(person)s
924 AND (driver = teamparticipation.team923 OR owner = %(person)s
925 OR owner = teamparticipation.team)924 ) _pillar
926 """ % sqlvalues(person=self)925 ON PillarName.name = _pillar.name
927 cur = cursor()926 """ % sqlvalues(person=self))
928 cur.execute(query)927 results = IStore(self).using(origin).find(find_spec)
929 names = [sqlvalues(str(name)) for [name] in cur.fetchall()]928 results = results.order_by('kind', 'displayname')
930 if not names:929 return [pillar_name for pillar_name, kind, displayname in results]
931 return PillarName.select("1=2")
932 quoted_names = ','.join([name for [name] in names])
933 return PillarName.select(
934 "PillarName.name IN (%s) AND PillarName.active IS TRUE" %
935 quoted_names, prejoins=['distribution', 'project', 'product'],
936 orderBy=['PillarName.distribution', 'PillarName.project',
937 'PillarName.product'])
938930
939 def getOwnedProjects(self, match_name=None):931 def getOwnedProjects(self, match_name=None):
940 """See `IPerson`."""932 """See `IPerson`."""
@@ -1447,7 +1439,7 @@
1447 @property1439 @property
1448 def wiki_names(self):1440 def wiki_names(self):
1449 """See `IPerson`."""1441 """See `IPerson`."""
1450 result = Store.of(self).find(WikiName, WikiName.person == self.id)1442 result = Store.of(self).find(WikiName, WikiName.person == self.id)
1451 return result.order_by(WikiName.wiki, WikiName.wikiname)1443 return result.order_by(WikiName.wiki, WikiName.wikiname)
14521444
1453 @property1445 @property
@@ -1611,7 +1603,7 @@
1611 Person.teamowner IS NULL1603 Person.teamowner IS NULL
1612 """ % sqlvalues(self.id),1604 """ % sqlvalues(self.id),
1613 clauseTables=['TeamParticipation', 'Person'],1605 clauseTables=['TeamParticipation', 'Person'],
1614 prejoins=['person',], limit=limit)1606 prejoins=['person', ], limit=limit)
16151607
1616 def getMappedParticipants(self, limit=None):1608 def getMappedParticipants(self, limit=None):
1617 """See `IPersonViewRestricted`."""1609 """See `IPersonViewRestricted`."""
@@ -1644,7 +1636,7 @@
1644 min_lng = 180.01636 min_lng = 180.0
1645 locations = self._getMappedParticipantsLocations(limit)1637 locations = self._getMappedParticipantsLocations(limit)
1646 if self.mapped_participants_count == 0:1638 if self.mapped_participants_count == 0:
1647 raise AssertionError, (1639 raise AssertionError(
1648 'This method cannot be called when '1640 'This method cannot be called when '
1649 'mapped_participants_count == 0.')1641 'mapped_participants_count == 0.')
1650 latitudes = sorted(location.latitude for location in locations)1642 latitudes = sorted(location.latitude for location in locations)
@@ -1824,7 +1816,7 @@
1824 ('teamparticipation', 'team'),1816 ('teamparticipation', 'team'),
1825 # Skip mailing lists because if the mailing list is purged, it's1817 # Skip mailing lists because if the mailing list is purged, it's
1826 # not a problem. Do this check separately below.1818 # not a problem. Do this check separately below.
1827 ('mailinglist', 'team')1819 ('mailinglist', 'team'),
1828 ])1820 ])
18291821
1830 # Private teams may participate in more areas of Launchpad than1822 # Private teams may participate in more areas of Launchpad than
@@ -2037,7 +2029,7 @@
2037 email = IMasterObject(email)2029 email = IMasterObject(email)
2038 assert not self.is_team, "This method must not be used for teams."2030 assert not self.is_team, "This method must not be used for teams."
2039 if not IEmailAddress.providedBy(email):2031 if not IEmailAddress.providedBy(email):
2040 raise TypeError, (2032 raise TypeError(
2041 "Any person's email address must provide the IEmailAddress "2033 "Any person's email address must provide the IEmailAddress "
2042 "interface. %s doesn't." % email)2034 "interface. %s doesn't." % email)
2043 # XXX Steve Alexander 2005-07-05:2035 # XXX Steve Alexander 2005-07-05:
@@ -2085,7 +2077,7 @@
2085 mailing_list_email = None2077 mailing_list_email = None
2086 all_addresses = IMasterStore(self).find(2078 all_addresses = IMasterStore(self).find(
2087 EmailAddress, EmailAddress.personID == self.id)2079 EmailAddress, EmailAddress.personID == self.id)
2088 for address in all_addresses :2080 for address in all_addresses:
2089 if address not in (email, mailing_list_email):2081 if address not in (email, mailing_list_email):
2090 address.destroySelf()2082 address.destroySelf()
20912083
@@ -2117,7 +2109,7 @@
2117 this person.2109 this person.
2118 """2110 """
2119 if not IEmailAddress.providedBy(email):2111 if not IEmailAddress.providedBy(email):
2120 raise TypeError, (2112 raise TypeError(
2121 "Any person's email address must provide the IEmailAddress "2113 "Any person's email address must provide the IEmailAddress "
2122 "interface. %s doesn't." % email)2114 "interface. %s doesn't." % email)
2123 assert email.personID == self.id2115 assert email.personID == self.id
@@ -2223,12 +2215,12 @@
2223 by this person.2215 by this person.
22242216
2225 :param ppa_only: controls if we are interested only in source2217 :param ppa_only: controls if we are interested only in source
2226 package releases targeted to any PPAs or, if False, sources targeted2218 package releases targeted to any PPAs or, if False, sources
2227 to primary archives.2219 targeted to primary archives.
22282220
2229 Active 'ppa_only' flag is usually associated with active 'uploader_only'2221 Active 'ppa_only' flag is usually associated with active
2230 because there shouldn't be any sense of maintainership for packages2222 'uploader_only' because there shouldn't be any sense of maintainership
2231 uploaded to PPAs by someone else than the user himself.2223 for packages uploaded to PPAs by someone else than the user himself.
2232 """2224 """
2233 clauses = ['sourcepackagerelease.upload_archive = archive.id']2225 clauses = ['sourcepackagerelease.upload_archive = archive.id']
22342226
@@ -2712,7 +2704,7 @@
2712 private_query = None2704 private_query = None
27132705
2714 base_query = SQL("Person.visibility = ?",2706 base_query = SQL("Person.visibility = ?",
2715 (PersonVisibility.PUBLIC.value,),2707 (PersonVisibility.PUBLIC.value, ),
2716 tables=['Person'])2708 tables=['Person'])
27172709
2718 if private_query is None:2710 if private_query is None:
@@ -2733,8 +2725,7 @@
2733 Not(Person.teamowner == None),2725 Not(Person.teamowner == None),
2734 Person.merged == None,2726 Person.merged == None,
2735 EmailAddress.person == Person.id,2727 EmailAddress.person == Person.id,
2736 StartsWith(Lower(EmailAddress.email), text)2728 StartsWith(Lower(EmailAddress.email), text))
2737 )
2738 return team_email_query2729 return team_email_query
27392730
2740 def _teamNameQuery(self, text):2731 def _teamNameQuery(self, text):
@@ -2747,8 +2738,7 @@
2747 TeamParticipation.team == Person.id,2738 TeamParticipation.team == Person.id,
2748 Not(Person.teamowner == None),2739 Not(Person.teamowner == None),
2749 Person.merged == None,2740 Person.merged == None,
2750 SQL("Person.fti @@ ftq(?)", (text,))2741 SQL("Person.fti @@ ftq(?)", (text, )))
2751 )
2752 return team_name_query2742 return team_name_query
27532743
2754 def find(self, text=""):2744 def find(self, text=""):
@@ -2770,8 +2760,7 @@
2770 EmailAddress.person == Person.id,2760 EmailAddress.person == Person.id,
2771 Person.account == Account.id,2761 Person.account == Account.id,
2772 Not(In(Account.status, inactive_statuses)),2762 Not(In(Account.status, inactive_statuses)),
2773 StartsWith(Lower(EmailAddress.email), text)2763 StartsWith(Lower(EmailAddress.email), text))
2774 )
27752764
2776 store = IStore(Person)2765 store = IStore(Person)
27772766
@@ -2814,8 +2803,7 @@
2814 status.value for status in INACTIVE_ACCOUNT_STATUSES)2803 status.value for status in INACTIVE_ACCOUNT_STATUSES)
2815 base_query = And(2804 base_query = And(
2816 Person.teamowner == None,2805 Person.teamowner == None,
2817 Person.merged == None2806 Person.merged == None)
2818 )
28192807
2820 clause_tables = []2808 clause_tables = []
28212809
@@ -2824,25 +2812,21 @@
2824 base_query = And(2812 base_query = And(
2825 base_query,2813 base_query,
2826 Person.account == Account.id,2814 Person.account == Account.id,
2827 Not(In(Account.status, inactive_statuses))2815 Not(In(Account.status, inactive_statuses)))
2828 )
2829 email_clause_tables = clause_tables + ['EmailAddress']2816 email_clause_tables = clause_tables + ['EmailAddress']
2830 if must_have_email:2817 if must_have_email:
2831 clause_tables = email_clause_tables2818 clause_tables = email_clause_tables
2832 base_query = And(2819 base_query = And(
2833 base_query,2820 base_query,
2834 EmailAddress.person == Person.id2821 EmailAddress.person == Person.id)
2835 )
2836 if created_after is not None:2822 if created_after is not None:
2837 base_query = And(2823 base_query = And(
2838 base_query,2824 base_query,
2839 Person.datecreated > created_after2825 Person.datecreated > created_after)
2840 )
2841 if created_before is not None:2826 if created_before is not None:
2842 base_query = And(2827 base_query = And(
2843 base_query,2828 base_query,
2844 Person.datecreated < created_before2829 Person.datecreated < created_before)
2845 )
28462830
2847 # Short circuit for returning all users in order2831 # Short circuit for returning all users in order
2848 if not text:2832 if not text:
@@ -2855,13 +2839,11 @@
2855 email_query = And(2839 email_query = And(
2856 base_query,2840 base_query,
2857 EmailAddress.person == Person.id,2841 EmailAddress.person == Person.id,
2858 StartsWith(Lower(EmailAddress.email), text)2842 StartsWith(Lower(EmailAddress.email), text))
2859 )
28602843
2861 name_query = And(2844 name_query = And(
2862 base_query,2845 base_query,
2863 SQL("Person.fti @@ ftq(?)", (text,))2846 SQL("Person.fti @@ ftq(?)", (text, )))
2864 )
2865 email_results = store.find(Person, email_query).order_by()2847 email_results = store.find(Person, email_query).order_by()
2866 name_results = store.find(Person, name_query).order_by()2848 name_results = store.find(Person, name_query).order_by()
2867 combined_results = email_results.union(name_results)2849 combined_results = email_results.union(name_results)
@@ -3456,8 +3438,7 @@
3456 if updact != 'c':3438 if updact != 'c':
3457 raise RuntimeError(3439 raise RuntimeError(
3458 '%s.%s reference to %s.%s must be ON UPDATE CASCADE'3440 '%s.%s reference to %s.%s must be ON UPDATE CASCADE'
3459 % (src_tab, src_col, ref_tab, ref_col)3441 % (src_tab, src_col, ref_tab, ref_col))
3460 )
34613442
3462 # These rows are in a UNIQUE index, and we can only move them3443 # These rows are in a UNIQUE index, and we can only move them
3463 # to the new Person if there is not already an entry. eg. if3444 # to the new Person if there is not already an entry. eg. if
@@ -3478,16 +3459,16 @@
3478 cur.execute(3459 cur.execute(
3479 'UPDATE GPGKey SET owner=%(to_id)d WHERE owner=%(from_id)d'3460 'UPDATE GPGKey SET owner=%(to_id)d WHERE owner=%(from_id)d'
3480 % vars())3461 % vars())
3481 skip.append(('gpgkey','owner'))3462 skip.append(('gpgkey', 'owner'))
34823463
3483 # Update the Branches that will not conflict, and fudge the names of3464 # Update the Branches that will not conflict, and fudge the names of
3484 # ones that *do* conflict.3465 # ones that *do* conflict.
3485 self._mergeBranches(cur, from_id, to_id)3466 self._mergeBranches(cur, from_id, to_id)
3486 skip.append(('branch','owner'))3467 skip.append(('branch', 'owner'))
34873468
3488 # XXX MichaelHudson 2010-01-13: Write _mergeSourcePackageRecipes!3469 # XXX MichaelHudson 2010-01-13: Write _mergeSourcePackageRecipes!
3489 #self._mergeSourcePackageRecipes(cur, from_id, to_id))3470 #self._mergeSourcePackageRecipes(cur, from_id, to_id))
3490 skip.append(('sourcepackagerecipe','owner'))3471 skip.append(('sourcepackagerecipe', 'owner'))
34913472
3492 self._mergeMailingListSubscriptions(cur, from_id, to_id)3473 self._mergeMailingListSubscriptions(cur, from_id, to_id)
3493 skip.append(('mailinglistsubscription', 'person'))3474 skip.append(('mailinglistsubscription', 'person'))
@@ -3568,17 +3549,14 @@
3568 raise NotImplementedError(3549 raise NotImplementedError(
3569 '%s.%s reference to %s.%s is in a UNIQUE index '3550 '%s.%s reference to %s.%s is in a UNIQUE index '
3570 'but has not been handled' % (3551 'but has not been handled' % (
3571 src_tab, src_col, ref_tab, ref_col3552 src_tab, src_col, ref_tab, ref_col))
3572 )
3573 )
35743553
3575 # Handle all simple cases3554 # Handle all simple cases
3576 for src_tab, src_col, ref_tab, ref_col, updact, delact in references:3555 for src_tab, src_col, ref_tab, ref_col, updact, delact in references:
3577 if (src_tab, src_col) in skip:3556 if (src_tab, src_col) in skip:
3578 continue3557 continue
3579 cur.execute('UPDATE %s SET %s=%d WHERE %s=%d' % (3558 cur.execute('UPDATE %s SET %s=%d WHERE %s=%d' % (
3580 src_tab, src_col, to_person.id, src_col, from_person.id3559 src_tab, src_col, to_person.id, src_col, from_person.id))
3581 ))
35823560
3583 self._mergeTeamMembership(cur, from_id, to_id)3561 self._mergeTeamMembership(cur, from_id, to_id)
35843562
@@ -3924,6 +3902,7 @@
3924 domain_parts = domain.split(".")3902 domain_parts = domain.split(".")
39253903
3926 person_set = PersonSet()3904 person_set = PersonSet()
3905
3927 def _valid_nick(nick):3906 def _valid_nick(nick):
3928 if not valid_name(nick):3907 if not valid_name(nick):
3929 return False3908 return False
@@ -3984,8 +3963,7 @@
3984 raise NicknameGenerationError(3963 raise NicknameGenerationError(
3985 "No nickname could be generated. "3964 "No nickname could be generated. "
3986 "This should be impossible to trigger unless some twonk has "3965 "This should be impossible to trigger unless some twonk has "
3987 "registered a match everything regexp in the black list."3966 "registered a match everything regexp in the black list.")
3988 )
39893967
3990 finally:3968 finally:
3991 random.setstate(random_state)3969 random.setstate(random_state)
39923970
=== removed file 'lib/lp/registry/stories/person/xx-person-packages.txt'
--- lib/lp/registry/stories/person/xx-person-packages.txt 2009-09-18 15:24:30 +0000
+++ lib/lp/registry/stories/person/xx-person-packages.txt 1970-01-01 00:00:00 +0000
@@ -1,16 +0,0 @@
1==========================
2Package Maintenance Report
3==========================
4
5From the main person page, the user's Package Maintenance Report can be
6accessed by clicking on the 'Related Software' menu item.
7
8 >>> anon_browser.open('http://launchpad.dev/~mark')
9 >>> anon_browser.getLink('Related software').click()
10
11 >>> print anon_browser.title
12 Software related to Mark Shuttleworth...
13 >>> print anon_browser.url
14 http://launchpad.dev/~mark/+related-software
15
16Please see pagetests/soyuz/xx-person-packages.txt for details.
170
=== modified file 'lib/lp/registry/stories/person/xx-person-projects.txt'
--- lib/lp/registry/stories/person/xx-person-projects.txt 2009-09-18 15:24:30 +0000
+++ lib/lp/registry/stories/person/xx-person-projects.txt 2010-07-14 21:22:45 +0000
@@ -1,4 +1,5 @@
1== List of owned or driven projects ==1List of owned or driven projects
2================================
23
3A Team home page displays a list of projects owned or driven by that4A Team home page displays a list of projects owned or driven by that
4team.5team.
@@ -16,8 +17,8 @@
16unimplemented specs and open questions.17unimplemented specs and open questions.
1718
18 >>> anon_browser.getLink('Show related projects').click()19 >>> anon_browser.getLink('Show related projects').click()
19 >>> anon_browser.title20 >>> print anon_browser.title
20 'Software related to Ubuntu Team...21 Related software : ...Ubuntu Team... team
2122
22 >>> related_projects = find_tag_by_id(23 >>> related_projects = find_tag_by_id(
23 ... anon_browser.contents, 'related-projects')24 ... anon_browser.contents, 'related-projects')
@@ -44,7 +45,7 @@
44 >>> print anon_browser.url45 >>> print anon_browser.url
45 http://launchpad.dev/~mark/+related-software46 http://launchpad.dev/~mark/+related-software
46 >>> print anon_browser.title47 >>> print anon_browser.title
47 Software related to Mark Shuttleworth...48 Related software : Mark Shuttleworth
4849
49In the case of a person that owns/drives more than50In the case of a person that owns/drives more than
50config.launchpad.default_batch_size, a message is displayed and the51config.launchpad.default_batch_size, a message is displayed and the
@@ -55,14 +56,14 @@
5556
56 >>> print extract_text(57 >>> print extract_text(
57 ... find_tag_by_id(anon_browser.contents, 'limit-encountered'))58 ... find_tag_by_id(anon_browser.contents, 'limit-encountered'))
58 Displaying first 5 projects out of 10 total59 Displaying first 5 projects out of 7 total
5960
60 >>> related_projects = find_tag_by_id(61 >>> related_projects = find_tag_by_id(
61 ... anon_browser.contents, 'related-projects')62 ... anon_browser.contents, 'related-projects')
62 >>> print extract_text(related_projects)63 >>> print extract_text(related_projects)
63 Name Bugs Blueprints Questions64 Name Bugs Blueprints Questions
64 Ubuntu Linux 4 1 8
65 Redhat Advanced Server 0 0 0
66 Debian GNU/Linux 3 0 065 Debian GNU/Linux 3 0 0
67 The Gentoo Linux 0 0 066 The Gentoo Linux 0 0 0
68 Kubuntu - Free KDE-based Linux 0 4 067 Kubuntu - Free KDE-based Linux 0 4 0
68 Redhat Advanced Server 0 0 0
69 Apache 1 0 0
6970
=== modified file 'lib/lp/registry/stories/person/xx-user-to-user.txt'
--- lib/lp/registry/stories/person/xx-user-to-user.txt 2009-12-03 20:54:00 +0000
+++ lib/lp/registry/stories/person/xx-user-to-user.txt 2010-07-14 21:22:45 +0000
@@ -12,7 +12,7 @@
12 >>> user_browser.open('http://launchpad.dev/~salgado')12 >>> user_browser.open('http://launchpad.dev/~salgado')
13 >>> user_browser.getLink('Contact this user').click()13 >>> user_browser.getLink('Contact this user').click()
14 >>> print user_browser.title14 >>> print user_browser.title
15 Contact Guilherme Salgado...15 Contact user : Guilherme Salgado
1616
17 >>> user_browser.getControl('Subject').value = 'Hi Salgado'17 >>> user_browser.getControl('Subject').value = 'Hi Salgado'
18 >>> user_browser.getControl('Message').value = 'Just saying hello'18 >>> user_browser.getControl('Message').value = 'Just saying hello'
@@ -163,81 +163,3 @@
163 >>> print_errors(browser_4.contents)163 >>> print_errors(browser_4.contents)
164 Your message was not sent because you have exceeded your daily quota of 3164 Your message was not sent because you have exceeded your daily quota of 3
165 messages to contact users. Try again in ... hours.165 messages to contact users. Try again in ... hours.
166
167
168Your own profile page
169=====================
170
171For consistency and testing purposes, the "contact" page is available even
172when someone is looking at their own profile page. The wording on the profile
173page is different though.
174
175 >>> user_browser.open('http://launchpad.dev/~no-priv')
176 >>> user_browser.getLink('Contact this user').click()
177 >>> print user_browser.title
178 Contact No Privileges Person...
179
180This holds true even for Sample Person, who is hiding her email addresses.
181
182 >>> user_browser.open('http://launchpad.dev/~name12')
183 >>> user_browser.getLink('Contact this user').click()
184 >>> print user_browser.title
185 Contact Sample Person...
186
187 >>> name12_browser = setupBrowser('Basic test@canonical.com:test')
188 >>> name12_browser.open('http://launchpad.dev/~name12')
189 >>> name12_browser.getLink('Contact this user').click()
190 >>> print name12_browser.title
191 Contact Sample Person...
192
193
194Teams
195=====
196
197Teams can also be contacted directly by team members, regardless of whether
198the team has set a contact address or uses a Launchpad mailing list.
199
200Guadamen have no contact address, so contacting them contacts all users
201directly.
202
203 >>> admin_browser.open('http://launchpad.dev/~guadamen')
204 >>> admin_browser.getLink('Contact this team').click()
205 >>> admin_browser.title
206 'Contact GuadaMen...
207
208Foo Bar registers an explicit contact address for Guadamen...
209
210 >>> admin_browser.open('http://launchpad.dev/~guadamen/+contactaddress')
211 >>> admin_browser.getControl('Another e-mail address').selected = True
212 >>> admin_browser.getControl(
213 ... name='field.contact_address').value = 'foo@example.com'
214 >>> admin_browser.getControl('Change').click()
215
216 >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
217
218 # Extract the link (from the email we just sent) the user will have to
219 # use to finish the registration process.
220 >>> from canonical.launchpad.ftests.logintoken import (
221 ... get_token_url_from_email)
222 >>> token_url = get_token_url_from_email(raw_msg)
223 >>> admin_browser.open(token_url)
224 >>> admin_browser.getControl('Continue').click()
225
226...which can also be contacted directly.
227
228 >>> admin_browser.open('http://launchpad.dev/~guadamen')
229 >>> admin_browser.getLink('Contact this team').click()
230 >>> admin_browser.title
231 'Contact GuadaMen...
232
233Foo Bar later registers a Launchpad mailing list for Guadamen...
234
235 >>> admin_browser.open('http://launchpad.dev/~guadamen/+mailinglist')
236 >>> admin_browser.getControl('Apply for Mailing List').click()
237
238...which too can be contacted directly.
239
240 >>> admin_browser.open('http://launchpad.dev/~guadamen')
241 >>> admin_browser.getLink('Contact this team').click()
242 >>> admin_browser.title
243 'Contact GuadaMen...
244166
=== modified file 'lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt'
--- lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt 2010-07-08 12:32:59 +0000
+++ lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt 2010-07-14 21:22:45 +0000
@@ -1,4 +1,5 @@
1= Voucher Redemption =1Voucher Redemption
2==================
23
3For a project to use Launchpad it must either be released under an4For a project to use Launchpad it must either be released under an
4approved open source license or the project administrators must buy a5approved open source license or the project administrators must buy a
@@ -11,7 +12,8 @@
11is related to a person.12is related to a person.
1213
1314
14== Accessing the voucher redemption page ==15Accessing the voucher redemption page
16-------------------------------------
1517
16Mark is an administrator for at least one project that does not have a18Mark is an administrator for at least one project that does not have a
17valid open source license so he is displayed the voucher redemption19valid open source license so he is displayed the voucher redemption
@@ -32,8 +34,8 @@
32 >>> main = find_main_content(browser.contents)34 >>> main = find_main_content(browser.contents)
33 >>> print extract_text(main)35 >>> print extract_text(main)
34 Redeem Vouchers for Commercial Subscriptions...36 Redeem Vouchers for Commercial Subscriptions...
35 Marilize Coetzee does not own any commercial projects. Only project owners can redeem37 Marilize Coetzee does not own any commercial projects. Only project
36 vouchers for a project.38 owners can redeem vouchers for a project.
3739
38A user can access her voucher page but not someone else's. Here40A user can access her voucher page but not someone else's. Here
39Marilize tries to access '+vouchers' on another user and is not41Marilize tries to access '+vouchers' on another user and is not
@@ -65,7 +67,8 @@
65 Here are the steps to obtain a commercial subscription:...67 Here are the steps to obtain a commercial subscription:...
6668
6769
68== Redeeming a voucher ==70Redeeming a voucher
71------------------
6972
70Selecting a project the user owns and a valid voucher result in a73Selecting a project the user owns and a valid voucher result in a
71successful voucher redemption.74successful voucher redemption.
@@ -125,7 +128,7 @@
125 ... 'http://launchpad.dev/~commercial-member/+related-projects')128 ... 'http://launchpad.dev/~commercial-member/+related-projects')
126 >>> main = find_main_content(browser.contents)129 >>> main = find_main_content(browser.contents)
127 >>> print extract_text(main)130 >>> print extract_text(main)
128 Projects related to Commercial Member131 Related projects
129 ...132 ...
130 Commercial Member doesn't own or drive any projects.133 Commercial Member doesn't own or drive any projects.
131134
@@ -142,7 +145,8 @@
142 Voucher redeemed successfully145 Voucher redeemed successfully
143146
144147
145== OOPS handling ==148OOPS handling
149-------------
146150
147If an error occurs in the proxy while trying to redeem a voucher an151If an error occurs in the proxy while trying to redeem a voucher an
148OOPS is recorded but an error is not raised. The user is shown an152OOPS is recorded but an error is not raised. The user is shown an
@@ -189,7 +193,9 @@
189193
190 >>> SalesforceXMLRPCTestTransport.forced_fault = None194 >>> SalesforceXMLRPCTestTransport.forced_fault = None
191195
192== Canceling the request ==196
197Canceling the request
198---------------------
193199
194If the 'Cancel' button is selected the person's overview page is shown.200If the 'Cancel' button is selected the person's overview page is shown.
195201
196202
=== modified file 'lib/lp/registry/templates/person-related-software.pt'
--- lib/lp/registry/templates/person-related-software.pt 2010-06-24 20:07:30 +0000
+++ lib/lp/registry/templates/person-related-software.pt 2010-07-14 21:22:45 +0000
@@ -22,7 +22,7 @@
22 <div id="packages">22 <div id="packages">
2323
24 <tal:maintained-packages24 <tal:maintained-packages
25 define="sourcepackagereleases view/get_latest_maintained_packages_with_stats"25 define="sourcepackagereleases view/latest_maintained_packages_with_stats"
26 condition="sourcepackagereleases">26 condition="sourcepackagereleases">
2727
28 <div class="top-portlet">28 <div class="top-portlet">
@@ -49,7 +49,7 @@
49 </tal:maintained-packages>49 </tal:maintained-packages>
5050
51 <tal:uploaded-packages51 <tal:uploaded-packages
52 define="sourcepackagereleases view/get_latest_uploaded_but_not_maintained_packages_with_stats"52 define="sourcepackagereleases view/latest_uploaded_but_not_maintained_packages_with_stats"
53 condition="sourcepackagereleases">53 condition="sourcepackagereleases">
5454
55 <div class="top-portlet">55 <div class="top-portlet">
@@ -75,7 +75,7 @@
75 </tal:uploaded-packages>75 </tal:uploaded-packages>
7676
77 <tal:ppa-packages77 <tal:ppa-packages
78 define="sourcepackagereleases view/get_latest_uploaded_ppa_packages_with_stats"78 define="sourcepackagereleases view/latest_uploaded_ppa_packages_with_stats"
79 condition="sourcepackagereleases">79 condition="sourcepackagereleases">
8080
81 <div class="top-portlet">81 <div class="top-portlet">
8282
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-person-packages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2010-06-24 20:07:30 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2010-07-14 21:22:45 +0000
@@ -1,4 +1,5 @@
1= Person Packages =1Person Packages
2===============
23
3All packages maintained or uploaded by a given person can be seen on4All packages maintained or uploaded by a given person can be seen on
4that person's +related-software page, which is linked to from the5that person's +related-software page, which is linked to from the
@@ -6,7 +7,7 @@
67
7 >>> browser.open("http://launchpad.dev/~mark/+related-software")8 >>> browser.open("http://launchpad.dev/~mark/+related-software")
8 >>> print browser.title9 >>> print browser.title
9 Software related to Mark Shuttleworth...10 Related software : Mark Shuttleworth
1011
11This page is just a summary of the user's packages and will only12This page is just a summary of the user's packages and will only
12display up to the most recent 30 items in each category. However, it13display up to the most recent 30 items in each category. However, it
@@ -67,7 +68,7 @@
67 >>> browser.open("http://launchpad.dev/~name16/+related-software")68 >>> browser.open("http://launchpad.dev/~name16/+related-software")
68 >>> link = browser.getLink(url="/ubuntu/hoary/+source/cnews")69 >>> link = browser.getLink(url="/ubuntu/hoary/+source/cnews")
69 >>> print link70 >>> print link
70 <Link text='Ubuntu Hoary' url='http://launchpad.dev/ubuntu/hoary/+source/cnews'>71 <Link text='Ubuntu Hoary' ...>
71 >>> link.click()72 >>> link.click()
72 >>> browser.title73 >>> browser.title
73 '...cnews... package : Hoary (5.04) : Ubuntu'74 '...cnews... package : Hoary (5.04) : Ubuntu'
@@ -78,13 +79,14 @@
78 >>> browser.open("http://launchpad.dev/~name16/+related-software")79 >>> browser.open("http://launchpad.dev/~name16/+related-software")
79 >>> link = browser.getLink(url="/ubuntu/+source/cnews/cr.g7-37")80 >>> link = browser.getLink(url="/ubuntu/+source/cnews/cr.g7-37")
80 >>> print link81 >>> print link
81 <Link text='cr.g7-37' url='http://launchpad.dev/ubuntu/+source/cnews/cr.g7-37'>82 <Link ... url='http://launchpad.dev/ubuntu/+source/cnews/cr.g7-37'>
82 >>> link.click()83 >>> link.click()
83 >>> browser.title84 >>> browser.title
84 'cr.g7-37 : \xe2\x80\x9ccnews\xe2\x80\x9d package : Ubuntu'85 'cr.g7-37 : \xe2\x80\x9ccnews\xe2\x80\x9d package : Ubuntu'
8586
8687
87== Batched listing pages ==88Batched listing pages
89---------------------
8890
89Following the navigation link to "Maintained packages" takes the user91Following the navigation link to "Maintained packages" takes the user
90to the page that lists maintained packages in batches.92to the page that lists maintained packages in batches.
@@ -120,8 +122,8 @@
120 >>> print extract_text(find_tag_by_id(browser.contents, 'packages'))122 >>> print extract_text(find_tag_by_id(browser.contents, 'packages'))
121 1...5 of 6 results123 1...5 of 6 results
122 ...124 ...
123 Name Uploaded to Version When Failures Bugs Questions125 Name Uploaded to Version When Failures Bugs Questions
124 foobar Ubuntu Breezy-autotest 1.0 2006-12-01 i386 0 0126 foobar Ubuntu Breezy-autotest 1.0 2006-12-01 i386 0 0
125 ...127 ...
126128
127The navigation link to "PPA packages" takes the user to the129The navigation link to "PPA packages" takes the user to the
@@ -134,19 +136,9 @@
134 Name Uploaded to Version When Failures136 Name Uploaded to Version When Failures
135 iceweasel PPA for Mark...Warty 1.0 2006-04-11 None137 iceweasel PPA for Mark...Warty 1.0 2006-04-11 None
136138
137And finally the Related projects navigation link takes the user to the139
138page that lists related projects in batches.140Private PPA packages
139141--------------------
140 >>> browser.getLink("Related projects").click()
141 >>> print extract_text(find_tag_by_id(browser.contents, 'projects'))
142 1...5 of 5 results
143 ...
144 Name Bugs Blueprints Questions
145 Ubuntu Linux 4 1 8
146 ...
147
148
149== Private PPA packages ==
150142
151Packages listed in the PPA section of this page are filtered so that143Packages listed in the PPA section of this page are filtered so that
152if the user is not allowed to see a private package they are not present144if the user is not allowed to see a private package they are not present
@@ -215,7 +207,9 @@
215207
216 >>> user_browser = setupBrowser(auth='Basic test@canonical.com:test')208 >>> user_browser = setupBrowser(auth='Basic test@canonical.com:test')
217209
218=== Cprov's +related-software page ===210
211Cprov's +related-software page
212------------------------------
219213
220For unprivileged users, cprov's displayed PPA packages only display214For unprivileged users, cprov's displayed PPA packages only display
221the one in his own public PPA because source2 is only published215the one in his own public PPA because source2 is only published
@@ -351,7 +345,8 @@
351 ...ago None - -345 ...ago None - -
352346
353347
354== Packages deleted from a PPA ==348Packages deleted from a PPA
349---------------------------
355350
356When a package is deleted from a PPA, in contrast to the archive index351When a package is deleted from a PPA, in contrast to the archive index
357it will continue to appear in the related-software packages list. This352it will continue to appear in the related-software packages list. This