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