Merge lp:~jtv/launchpad/decruft-test-template into lp:launchpad/db-devel

Proposed by Jeroen T. Vermeulen
Status: Rejected
Rejected by: Jeroen T. Vermeulen
Proposed branch: lp:~jtv/launchpad/decruft-test-template
Merge into: lp:launchpad/db-devel
Diff against target: 3002 lines (+854/-598)
30 files modified
configs/development/launchpad-lazr.conf (+1/-0)
lib/canonical/config/schema-lazr.conf (+3/-0)
lib/canonical/launchpad/testing/codeimporthelpers.py (+0/-2)
lib/lp/codehosting/tests/servers.py (+7/-7)
lib/lp/poppy/tests/test_poppy.py (+1/-4)
lib/lp/registry/browser/branding.py (+1/-1)
lib/lp/registry/browser/person.py (+28/-46)
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/doc/sshkey.txt (+4/-4)
lib/lp/registry/interfaces/person.py (+2/-3)
lib/lp/registry/interfaces/ssh.py (+11/-1)
lib/lp/registry/model/person.py (+86/-83)
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/registry/tests/test_distroseries.py (+1/-1)
lib/lp/soyuz/browser/archive.py (+1/-5)
lib/lp/soyuz/model/publishing.py (+1/-1)
lib/lp/soyuz/stories/soyuz/xx-person-packages.txt (+17/-22)
lib/lp/soyuz/tests/test_publishing.py (+59/-2)
lib/lp/testing/factory.py (+112/-16)
standard_test_template.py (+14/-4)
To merge this branch: bzr merge lp:~jtv/launchpad/decruft-test-template
Reviewer Review Type Date Requested Status
Launchpad code reviewers code Pending
Review via email: mp+30080@code.launchpad.net

Commit message

Update sample unit test file.

Description of the change

= Decruft standard test template =

This is an update I'd like for the sample unit-test file we provide in the project's top-level directory.

The boilerplate at the bottom of the test is no longer needed, as far as I know. If this is correct, I'd also like to remove it from as many existing tests as I reasonably can.

I also documented some guidelines and typical steps, such as the layer. I don't know how it is for others but I'm generally too lazy to remember what layers there are and where to import them from, so I included that information.

Is this too much? I don't want engineers to have to spend more time deleting text from the sample than they would otherwise spend putting up scaffolding.

The new file is lint-free. This is why the "XXX: Sample test class" comment is inside the test class, not above it: otherwise "make lint" would expect a double blank line between the comment and the class definition.

The new sample test also actually passes. Perhaps it would be worthwhile to make it part of the test suite so that we can be sure it doesn't fall out of date.

Jeroen

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Whoops, accidentally proposed for the default db-devel instead of devel. Re-submitting: https://code.launchpad.net/~jtv/launchpad/decruft-test-template/+merge/30083

Preview Diff

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

Subscribers

People subscribed via source and target branches

to status/vote changes: