← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/decruft-test-template into lp:launchpad

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/decruft-test-template into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers): code


= 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
-- 
https://code.launchpad.net/~jtv/launchpad/decruft-test-template/+merge/30080
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/decruft-test-template into lp:launchpad.
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf	2010-07-01 06:31:55 +0000
+++ configs/development/launchpad-lazr.conf	2010-07-16 10:17:44 +0000
@@ -139,6 +139,7 @@
 mugshot_batch_size: 8
 announcement_batch_size: 4
 download_batch_size: 4
+summary_list_size: 5
 openid_preauthorization_acl:
     localhost http://launchpad.dev/
 max_bug_feed_cache_minutes: 30

=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf	2010-07-08 16:52:35 +0000
+++ lib/canonical/config/schema-lazr.conf	2010-07-16 10:17:44 +0000
@@ -994,6 +994,9 @@
 # files.  The releases are batched, not the individual files.
 download_batch_size: 10
 
+# The default size of a list that summarizes and introduces a larger list.
+summary_list_size: 10
+
 # If restrict_to_team is set (such as on the beta
 # website), then this indicates the hostname suffix for
 # the non-restricted version of Launchpad.  Replacing

=== modified file 'lib/canonical/launchpad/testing/codeimporthelpers.py'
--- lib/canonical/launchpad/testing/codeimporthelpers.py	2010-02-18 02:31:34 +0000
+++ lib/canonical/launchpad/testing/codeimporthelpers.py	2010-07-16 10:17:44 +0000
@@ -14,7 +14,6 @@
 from datetime import datetime, timedelta
 
 from pytz import UTC
-import transaction
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -56,7 +55,6 @@
         code_import = factory.makeCodeImport()
     if machine is None:
         machine = factory.makeCodeImportMachine(set_online=True)
-    transaction.commit() # Commit so factory created persons are valid
     # The code import must be in a reviewed state.
     if code_import.review_status != CodeImportReviewStatus.REVIEWED:
         code_import.updateFromData(

=== modified file 'lib/lp/codehosting/tests/servers.py'
--- lib/lp/codehosting/tests/servers.py	2010-04-01 01:01:21 +0000
+++ lib/lp/codehosting/tests/servers.py	2010-07-16 10:17:44 +0000
@@ -42,13 +42,13 @@
     testUser.join(testTeam)
     ssh_key_set = getUtility(ISSHKeySet)
     ssh_key_set.new(
-        testUser, SSHKeyType.DSA,
-        'AAAAB3NzaC1kc3MAAABBAL5VoWG5sy3CnLYeOw47L8m9A15hA/PzdX2u0B7c2Z1k'
-        'tFPcEaEuKbLqKVSkXpYm7YwKj9y88A9Qm61CdvI0c50AAAAVAKGY0YON9dEFH3Dz'
-        'eVYHVEBGFGfVAAAAQCoe0RhBcefm4YiyQVwMAxwTlgySTk7FSk6GZ95EZ5Q8/OTd'
-        'ViTaalvGXaRIsBdaQamHEBB+Vek/VpnF1UGGm8YAAABAaCXDl0r1k93JhnMdF0ap'
-        '4UJQ2/NnqCyoE8Xd5KdUWWwqwGdMzqB1NOeKN6ladIAXRggLc2E00UsnUXh3GE3R'
-        'gw==', 'testuser')
+        testUser, 
+        'ssh-dss AAAAB3NzaC1kc3MAAABBAL5VoWG5sy3CnLYeOw47L8m9A15hA/PzdX2u'
+        '0B7c2Z1ktFPcEaEuKbLqKVSkXpYm7YwKj9y88A9Qm61CdvI0c50AAAAVAKGY0YON'
+        '9dEFH3DzeVYHVEBGFGfVAAAAQCoe0RhBcefm4YiyQVwMAxwTlgySTk7FSk6GZ95E'
+        'Z5Q8/OTdViTaalvGXaRIsBdaQamHEBB+Vek/VpnF1UGGm8YAAABAaCXDl0r1k93J'
+        'hnMdF0ap4UJQ2/NnqCyoE8Xd5KdUWWwqwGdMzqB1NOeKN6ladIAXRggLc2E00Usn'
+        'UXh3GE3Rgw== testuser')
     commit()
 
 

=== modified file 'lib/lp/poppy/tests/test_poppy.py'
--- lib/lp/poppy/tests/test_poppy.py	2010-06-07 12:28:30 +0000
+++ lib/lp/poppy/tests/test_poppy.py	2010-07-16 10:17:44 +0000
@@ -92,11 +92,8 @@
             public_key = f.read()
         finally:
             f.close()
-        kind, key_text, comment = public_key.split(' ', 2)
         sshkeyset = getUtility(ISSHKeySet)
-        # Assume it's an RSA key for now, ignoring the actual value in the
-        # file.
-        key = sshkeyset.new(person, SSHKeyType.RSA, key_text, comment)
+        key = sshkeyset.new(person, public_key)
         transaction.commit()
         return key
 

=== modified file 'lib/lp/registry/browser/branding.py'
--- lib/lp/registry/browser/branding.py	2009-09-03 13:25:04 +0000
+++ lib/lp/registry/browser/branding.py	2010-07-16 10:17:44 +0000
@@ -28,7 +28,7 @@
         return ('Change the images used to represent %s in Launchpad'
                 % self.context.displayname)
 
-    page_title = label
+    page_title = "Change branding"
 
     custom_widget('icon', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)
     custom_widget('logo', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2010-07-08 21:14:06 +0000
+++ lib/lp/registry/browser/person.py	2010-07-16 10:17:44 +0000
@@ -84,7 +84,6 @@
 import copy
 import itertools
 import pytz
-import subprocess
 import urllib
 
 from datetime import datetime, timedelta
@@ -165,7 +164,8 @@
     IPerson, IPersonClaim, IPersonSet, ITeam, ITeamReassignment,
     PersonVisibility, TeamMembershipRenewalPolicy, TeamSubscriptionPolicy)
 from lp.registry.interfaces.poll import IPollSet, IPollSubset
-from lp.registry.interfaces.ssh import ISSHKeySet, SSHKeyType
+from lp.registry.interfaces.ssh import (
+    ISSHKeySet, SSHKeyAdditionError, SSHKeyCompromisedError, SSHKeyType)
 from lp.registry.interfaces.teammembership import (
     DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT, ITeamMembership,
     ITeamMembershipSet, TeamMembershipStatus)
@@ -2511,7 +2511,7 @@
         if self.user == self.context:
             return 'Your Launchpad Karma'
         else:
-            return 'Launchpad Karma for %s' % self.context.displayname
+            return 'Launchpad Karma'
 
     @cachedproperty
     def has_karma(self):
@@ -3598,24 +3598,12 @@
         return canonical_url(self.context, view_name="+edit")
 
     def add_ssh(self):
-        # XXX: JonathanLange 2010-05-13: This should hella not be in browser
-        # code. Move this to ISSHKeySet (bonus! tests become easier to write).
         sshkey = self.request.form.get('sshkey')
-        try:
-            kind, keytext, comment = sshkey.split(' ', 2)
-        except ValueError:
-            self.error_message = structured('Invalid public key')
-            return
-
-        if not (kind and keytext and comment):
-            self.error_message = structured('Invalid public key')
-            return
-
-        process = subprocess.Popen(
-            '/usr/bin/ssh-vulnkey -', shell=True, stdin=subprocess.PIPE,
-            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-        (out, err) = process.communicate(sshkey.encode('utf-8'))
-        if 'compromised' in out.lower():
+	try:
+      	    getUtility(ISSHKeySet).new(self.user, sshkey)
+        except SSHKeyAdditionError:
+            self.error_message = structured('Invalid public key')
+        except SSHKeyCompromisedError:
             self.error_message = structured(
                 'This key is known to be compromised due to a security flaw '
                 'in the software used to generate it, so it will not be '
@@ -3623,18 +3611,8 @@
                 '<a href="http://www.ubuntu.com/usn/usn-612-2";>Security '
                 'Notice</a> for further information and instructions on how '
                 'to generate another key.')
-            return
-
-        if kind == 'ssh-rsa':
-            keytype = SSHKeyType.RSA
-        elif kind == 'ssh-dss':
-            keytype = SSHKeyType.DSA
         else:
-            self.error_message = structured('Invalid public key')
-            return
-
-        getUtility(ISSHKeySet).new(self.user, keytype, keytext, comment)
-        self.info_message = structured('SSH public key added.')
+            self.info_message = structured('SSH public key added.')
 
     def remove_ssh(self):
         key_id = self.request.form.get('key')
@@ -5106,12 +5084,15 @@
 class PersonRelatedSoftwareView(LaunchpadView):
     """View for +related-software."""
     implements(IPersonRelatedSoftwareMenu)
+    _max_results_key = 'summary_list_size'
 
-    max_results_to_display = config.launchpad.default_batch_size
+    @property
+    def max_results_to_display(self):
+        return config.launchpad[self._max_results_key]
 
     @property
     def page_title(self):
-        return "Software related to " + self.context.displayname
+        return 'Related software'
 
     @cachedproperty
     def related_projects(self):
@@ -5148,12 +5129,12 @@
     @cachedproperty
     def first_five_related_projects(self):
         """Return first five projects owned or driven by this person."""
-        return list(self._related_projects()[:5])
+        return self._related_projects()[:5]
 
     @cachedproperty
     def related_projects_count(self):
         """The number of project owned or driven by this person."""
-        return self._related_projects().count()
+        return len(self._related_projects())
 
     @cachedproperty
     def has_more_related_projects(self):
@@ -5226,7 +5207,7 @@
         return results, header_message
 
     @property
-    def get_latest_uploaded_ppa_packages_with_stats(self):
+    def latest_uploaded_ppa_packages_with_stats(self):
         """Return the sourcepackagereleases uploaded to PPAs by this person.
 
         Results are filtered according to the permission of the requesting
@@ -5238,7 +5219,7 @@
         return self.filterPPAPackageList(results)
 
     @property
-    def get_latest_maintained_packages_with_stats(self):
+    def latest_maintained_packages_with_stats(self):
         """Return the latest maintained packages, including stats."""
         packages = self.context.getLatestMaintainedPackages()
         results, header_message = self._getDecoratedPackagesSummary(packages)
@@ -5246,7 +5227,7 @@
         return results
 
     @property
-    def get_latest_uploaded_but_not_maintained_packages_with_stats(self):
+    def latest_uploaded_but_not_maintained_packages_with_stats(self):
         """Return the latest uploaded packages, including stats.
 
         Don't include packages that are maintained by the user.
@@ -5330,6 +5311,7 @@
 
 class PersonMaintainedPackagesView(PersonRelatedSoftwareView):
     """View for +maintained-packages."""
+    _max_results_key = 'default_batch_size'
 
     def initialize(self):
         """Set up the batch navigation."""
@@ -5338,11 +5320,12 @@
 
     @property
     def page_title(self):
-        return "Software maintained by " + self.context.displayname
+        return "Maintained Packages"
 
 
 class PersonUploadedPackagesView(PersonRelatedSoftwareView):
     """View for +uploaded-packages."""
+    _max_results_key = 'default_batch_size'
 
     def initialize(self):
         """Set up the batch navigation."""
@@ -5351,11 +5334,12 @@
 
     @property
     def page_title(self):
-        return "Software uploaded by " + self.context.displayname
+        return "Uploaded packages"
 
 
 class PersonPPAPackagesView(PersonRelatedSoftwareView):
     """View for +ppa-packages."""
+    _max_results_key = 'default_batch_size'
 
     def initialize(self):
         """Set up the batch navigation."""
@@ -5371,11 +5355,12 @@
 
     @property
     def page_title(self):
-        return "PPA packages related to " + self.context.displayname
+        return "PPA packages"
 
 
 class PersonRelatedProjectsView(PersonRelatedSoftwareView):
     """View for +related-projects."""
+    _max_results_key = 'default_batch_size'
 
     def initialize(self):
         """Set up the batch navigation."""
@@ -5385,7 +5370,7 @@
 
     @property
     def page_title(self):
-        return "Projects related to " + self.context.displayname
+        return "Related projects"
 
 
 class PersonOAuthTokensView(LaunchpadView):
@@ -5800,10 +5785,7 @@
         # Subject and then Message fields.
         self.form_fields = FormFields(*chain((field, ), self.form_fields))
 
-    @property
-    def label(self):
-        """The form label."""
-        return 'Contact ' + self.context.displayname
+    label = 'Contact user'
 
     @cachedproperty
     def recipients(self):

=== modified file 'lib/lp/registry/browser/tests/person-karma-views.txt'
--- lib/lp/registry/browser/tests/person-karma-views.txt	2010-01-21 11:55:56 +0000
+++ lib/lp/registry/browser/tests/person-karma-views.txt	2010-07-16 10:17:44 +0000
@@ -18,4 +18,4 @@
     >>> neil = factory.makePerson(name='neil', displayname='Neil Peart')
     >>> view = create_initialized_view(neil, '+karma')
     >>> print view.label
-    Launchpad Karma for Neil Peart
+    Launchpad Karma

=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
--- lib/lp/registry/browser/tests/person-views.txt	2010-04-19 21:16:12 +0000
+++ lib/lp/registry/browser/tests/person-views.txt	2010-07-16 10:17:44 +0000
@@ -1,16 +1,17 @@
-= Person Pages =
+Person Pages
+============
 
-There are many views that wrap the Person object to display the
-person's information.
+There are many views that wrap the Person object to display the person's
+information.
 
 
 Probationary and invalid users
 ------------------------------
 
-The person +index view provides the is_probationary_or_invalid_user so that
-page features can be disabled because the user may abuse them. Active
-users with karma are not on probation; the user's homepage_content is
-formatted as HTML.
+The person +index view provides the is_probationary_or_invalid_user so
+that page features can be disabled because the user may abuse them.
+Active users with karma are not on probation; the user's
+homepage_content is formatted as HTML.
 
     >>> from lp.registry.interfaces.person import IPersonSet
 
@@ -29,8 +30,8 @@
     <BLANKLINE>
     <p><a rel="nofollow" href="http://aa.aa/";>http://<wbr></wbr>aa.aa/</a></p>
 
-Teams are always valid and do not have probation rules; the homepage content
-is formatted HTML.
+Teams are always valid and do not have probation rules; the homepage
+content is formatted HTML.
 
     >>> team = factory.makeTeam()
     >>> login_person(team.teamowner)
@@ -59,13 +60,14 @@
     <BLANKLINE>
     http://aa.aa/
 
-Inactive and suspended users are invalid; the homepage content is escaped
-HTML.
+Inactive and suspended users are invalid; the homepage content is
+escaped HTML.
 
     >>> from canonical.launchpad.interfaces.account import AccountStatus
     >>> from canonical.launchpad.interfaces import IMasterObject
 
     # Only admins can change an account.
+
     >>> admin_user = person_set.getByName('name16')
     >>> login_person(admin_user)
     >>> invalid_user = factory.makePerson(name="ugh")
@@ -109,6 +111,7 @@
     >>> mark = person_set.getByEmail('mark@xxxxxxxxxxx')
     >>> mark.preferredemail.email
     u'mark@xxxxxxxxxxx'
+
     >>> mark.hide_email_addresses
     False
 
@@ -119,21 +122,25 @@
     >>> view = create_initialized_view(mark, '+index')
     >>> view.email_address_visibility.is_login_required
     True
+
     >>> print view.visible_email_address_description
     None
+
     >>> view.visible_email_addresses
     []
 
-Logged in user can see Mark's email addresses. The email addresses
-state is PUBLIC. There is a description of who can see the list of
-email addresses.
+Logged in user can see Mark's email addresses. The email addresses state
+is PUBLIC. There is a description of who can see the list of email
+addresses.
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> view = create_initialized_view(mark, '+index')
     >>> view.email_address_visibility.are_public
     True
+
     >>> view.visible_email_address_description
     'This email address is only visible to Launchpad users.'
+
     >>> view.visible_email_addresses
     [u'mark@xxxxxxxxxxx']
 
@@ -149,33 +156,37 @@
     >>> view = create_initialized_view(sample_person, '+index')
     >>> view.email_address_visibility.is_login_required
     True
+
     >>> view.visible_email_addresses
     []
 
-No Privileges Person cannot see them either because the state is
-HIDDEN. There is no description for the email addresses because
-he cannot view them.
+No Privileges Person cannot see them either because the state is HIDDEN.
+There is no description for the email addresses because he cannot view
+them.
 
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> view = create_initialized_view(sample_person, '+index')
     >>> view.email_address_visibility.are_hidden
     True
+
     >>> print view.visible_email_address_description
     None
+
     >>> view.visible_email_addresses
     []
 
 Admins and commercial admins, like Foo Bar and Commercial Member, can
-see Sample Person's email addresses because the state is ALLOWED.
-The description states that the email addresses are not disclosed to
-others.
+see Sample Person's email addresses because the state is ALLOWED. The
+description states that the email addresses are not disclosed to others.
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> view = create_initialized_view(sample_person, '+index')
     >>> view.email_address_visibility.are_allowed
     True
+
     >>> view.visible_email_address_description
     'This email address is not disclosed to others.'
+
     >>> view.visible_email_addresses
     [u'test@xxxxxxxxxxxxx', u'testing@xxxxxxxxxxxxx']
 
@@ -183,6 +194,7 @@
     >>> view = create_initialized_view(sample_person, '+index')
     >>> view.email_address_visibility.are_allowed
     True
+
     >>> view.visible_email_addresses
     [u'test@xxxxxxxxxxxxx', u'testing@xxxxxxxxxxxxx']
 
@@ -194,22 +206,24 @@
     >>> view = create_initialized_view(ubuntu_team, '+index')
     >>> view.email_address_visibility.is_login_required
     True
+
     >>> view.visible_email_addresses
     []
 
-A logged in user can see the team's contact address because it cannot
-be hidden.
+A logged in user can see the team's contact address because it cannot be
+hidden.
 
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> view = create_initialized_view(ubuntu_team, '+index')
     >>> view.email_address_visibility.are_public
     True
+
     >>> view.visible_email_addresses
     [u'support@xxxxxxxxxx']
 
-It is possible for a team to have more than two addresses (from a mailing
-list), but only the preferred address is listed in the visible_email_addresses
-property.
+It is possible for a team to have more than two addresses (from a
+mailing list), but only the preferred address is listed in the
+visible_email_addresses property.
 
     >>> email_address = factory.makeEmail(
     ...     'ubuntu_team@xxxxxxxxxxxxx', ubuntu_team)
@@ -227,16 +241,19 @@
     >>> view = create_initialized_view(landscape_developers, '+index')
     >>> view.email_address_visibility.are_none_available
     True
+
     >>> print view.visible_email_address_description
     None
+
     >>> view.visible_email_addresses
     []
 
 
-== Languages ==
+Languages
+---------
 
-The PersonView provides a comma separated list of languages that a person
-speaks. The contact details portlet displays the user languages.
+The PersonView provides a comma separated list of languages that a
+person speaks. The contact details portlet displays the user languages.
 
 English is the default language in Launchpad. If the user has not set
 his preferred languages, English is used.
@@ -249,9 +266,9 @@
     >>> print view.languages
     English
 
-This assumption is visible to the user when he views his own profile page,
-and he can set his preferred languages if he wants to make a correction.
-The list of languages is alphabetized.
+This assumption is visible to the user when he views his own profile
+page, and he can set his preferred languages if he wants to make a
+correction. The list of languages is alphabetized.
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
 
@@ -274,8 +291,8 @@
     English
 
 Teams most often set just one language that is used for the Answers
-application. If the language is a variant, the variation is shown
-in parenthesis.
+application. If the language is a variant, the variation is shown in
+parenthesis.
 
     >>> landscape_developers.addLanguage(
     ...     languageset.getLanguageByCode('pt_BR'))
@@ -284,13 +301,14 @@
     Portuguese (Brazil)
 
 
-== Location ==
+Location
+--------
 
 The Person profile page contains the location portlet that shows a map.
 The map requires the google GMap JavaScript to display, so the views set
 the state of the request's needs_gmap2 attribute to True only when the
-user has set his latitude, it is visible, and the viewing user wishes
-to see it. The map is not rendered if the user has not set his location.
+user has set his latitude, it is visible, and the viewing user wishes to
+see it. The map is not rendered if the user has not set his location.
 
     >>> sample_person.latitude is None
     True
@@ -319,6 +337,7 @@
     >>> person_view = create_initialized_view(sample_person, '+index')
     >>> person_view.request.needs_gmap2
     False
+
     >>> print person_view.map_portlet_html
     Traceback (most recent call last):
      ...
@@ -334,20 +353,22 @@
     >>> person_view = create_initialized_view(sample_person, '+index')
     >>> person_view.request.needs_gmap2
     True
+
     >>> print person_view.map_portlet_html
     <script type="text/javascript">
       YUI().use('node', 'lp.mapping', function(Y) { ...
 
-The small_maps key in the launchpad_views cookie can be set of the viewing
-user to 'false' to indicate that small maps are not wanted. While needs_gmap2
-is False, the map_portlet_html property's markup is still needed to render
-the 'Show maps' checkbox.
+The small_maps key in the launchpad_views cookie can be set of the
+viewing user to 'false' to indicate that small maps are not wanted.
+While needs_gmap2 is False, the map_portlet_html property's markup is
+still needed to render the 'Show maps' checkbox.
 
     >>> cookie = 'launchpad_views=small_maps=false'
     >>> person_view = create_initialized_view(
     ...     sample_person, '+index', cookie=cookie)
     >>> person_view.request.needs_gmap2
     False
+
     >>> print person_view.map_portlet_html
     <script type="text/javascript">
       YUI().use('node', 'lp.mapping', function(Y) { ...
@@ -358,6 +379,7 @@
     >>> user = factory.makePerson()
     >>> user.latitude is None
     True
+
     >>> login_person(user)
     >>> person_view = create_initialized_view(
     ...     user, '+index')
@@ -372,7 +394,8 @@
     >>> person_view.should_show_map_portlet
     False
 
-If a user has a location set and it is visibible then the portlet is shown.
+If a user has a location set and it is visibible then the portlet is
+shown.
 
     >>> person_view = create_initialized_view(
     ...     sample_person, '+index')
@@ -380,33 +403,37 @@
     True
 
 
-== Things a person is working on ==
+Things a person is working on
+-----------------------------
 
 PersonView is the base for many views for Person objects. It provides
 several properties to help display things the user is working on.
 
-The +portlet-currentfocus view is responsible for rendering the
-"Working on..." section in the Person profile page (+index). Nothing
-is rendered when the user does not have any assigned bug or specs
-that are not in progress.
+The +portlet-currentfocus view is responsible for rendering the "Working
+on..." section in the Person profile page (+index). Nothing is rendered
+when the user does not have any assigned bug or specs that are not in
+progress.
 
     >>> user = factory.makePerson(name='ken', password='test')
     >>> view = create_initialized_view(user, name='+portlet-currentfocus')
     >>> view.has_assigned_bugs_or_specs_in_progress
     False
+
     >>> len(view.assigned_bugs_in_progress)
     0
+
     >>> len(view.assigned_specs_in_progress)
     0
+
     >>> from canonical.launchpad.testing.pages import extract_text
     >>> len(extract_text(view.render()))
     0
 
-Assigned specifications that do not display when they are not in an
-in progress state.
+Assigned specifications that do not display when they are not in an in
+progress state.
 
     >>> from canonical.launchpad.interfaces import (
-    ... SpecificationImplementationStatus)
+    ...    SpecificationImplementationStatus)
 
     >>> login(user.preferredemail.email)
     >>> product = factory.makeProduct(name="tool", owner=user)
@@ -415,18 +442,20 @@
     >>> spec.assignee = user
     >>> view.has_assigned_bugs_or_specs_in_progress
     False
+
     >>> len(view.assigned_bugs_in_progress)
     0
+
     >>> len(view.assigned_specs_in_progress)
     0
 
 The specification is displayed only when it is in a in progress state
-(The state may be any from STARTED though DEPLOYMENT). Below the
-list of specifications is a link to show all the specifications that
-the user is working on.
+(The state may be any from STARTED though DEPLOYMENT). Below the list of
+specifications is a link to show all the specifications that the user is
+working on.
 
     >>> from canonical.launchpad.interfaces import (
-    ... SpecificationDefinitionStatus)
+    ...    SpecificationDefinitionStatus)
 
     >>> spec.definition_status = SpecificationDefinitionStatus.APPROVED
     >>> newstate = spec.updateLifecycleStatus(user)
@@ -435,10 +464,13 @@
     >>> view = create_initialized_view(user, name='+portlet-currentfocus')
     >>> view.has_assigned_bugs_or_specs_in_progress
     True
+
     >>> len(view.assigned_bugs_in_progress)
     0
+
     >>> len(view.assigned_specs_in_progress)
     1
+
     >>> print view.render()
     <div id="working-on"...
     <a href="/~ken/+specs?role=assignee"> All assigned blueprints </a>...
@@ -453,26 +485,32 @@
     >>> bug.bugtasks[0].transitionToAssignee(user)
     >>> view.has_assigned_bugs_or_specs_in_progress
     True
+
     >>> len(view.assigned_bugs_in_progress)
     0
+
     >>> len(view.assigned_specs_in_progress)
     1
 
-The assigned bug is displayed in the "Working on..." section when
-its status is in INPROGRESS.
+The assigned bug is displayed in the "Working on..." section when its
+status is in INPROGRESS.
 
     >>> from canonical.launchpad.interfaces import BugTaskStatus
     >>> bug.bugtasks[0].transitionToStatus(BugTaskStatus.INPROGRESS, user)
 
     # Create a new view because we're testing some cached properties.
+
     >>> view = create_initialized_view(user, name='+portlet-currentfocus')
 
     >>> view.has_assigned_bugs_or_specs_in_progress
     True
+
     >>> len(view.assigned_bugs_in_progress)
     1
+
     >>> len(view.assigned_specs_in_progress)
     1
+
     >>> print view.render()
     <div id="working-on"...
     <a href="http://launchpad.dev/~ken/+assignedbugs?...";>
@@ -494,12 +532,15 @@
     ...     BugTaskStatus.INPROGRESS, user)
 
     # Create a new view because we're testing some cached properties.
+
     >>> view = create_initialized_view(user, name='+portlet-currentfocus')
 
     >>> view.has_assigned_bugs_or_specs_in_progress
     True
+
     >>> len(view.assigned_bugs_in_progress)
     2
+
     >>> len(view.assigned_specs_in_progress)
     1
 
@@ -508,106 +549,21 @@
     >>> another_bug.duplicateof = bug
 
     # Create a new view because we're testing some cached properties.
+
     >>> view = create_initialized_view(user, name='+portlet-currentfocus')
 
     >>> view.has_assigned_bugs_or_specs_in_progress
     True
+
     >>> len(view.assigned_bugs_in_progress)
     1
+
     >>> len(view.assigned_specs_in_progress)
     1
 
 
-== Person Packages ==
-
-The page at ~user/+related-software contains 4 sections,
-"Maintained Packages", "Uploaded Packages", "PPA Packages" and "Related
-projects".
-
-Each section is limited to displaying at most N packages, where N is the value
-of config.launchpad.default_batch_size, so that the page does not time out
-before Zope can render it.
-
-Before continuing, create lots of packages that will appear in each
-section of Foo Bar's +related-software page, such that there's more available
-than we're willing to display.
-
-    >>> login("admin@xxxxxxxxxxxxx")
-    >>> from lp.registry.interfaces.distribution import (
-    ...     IDistributionSet)
-    >>> from lp.soyuz.interfaces.publishing import (
-    ...     PackagePublishingStatus)
-    >>> name16 = person_set.getByName('name16')
-    >>> mark = person_set.getByName('mark')
-    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
-    >>> warty = ubuntu['warty']
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...     SoyuzTestPublisher)
-    >>> test_pub = SoyuzTestPublisher()
-    >>> test_pub.person = name16
-
-    >>> view = create_initialized_view(name16, '+related-software')
-    >>> max_results = view.max_results_to_display
-    >>> for count in range(0, max_results + 3):
-    ...     source_name = "foo" + str(count)
-    ...     # Add the PPA packages.
-    ...     discard = test_pub.getPubSource(
-    ...         sourcename=source_name,
-    ...         status=PackagePublishingStatus.PUBLISHED,
-    ...         archive=mark.archive,
-    ...         distroseries=warty)
-    ...     # Add the maintained packages.
-    ...     discard = test_pub.getPubSource(
-    ...         sourcename=source_name,
-    ...         status=PackagePublishingStatus.PUBLISHED,
-    ...         distroseries=warty)
-    ...     # Add the uploaded packages.
-    ...     discard = test_pub.getPubSource(
-    ...         maintainer=mark,
-    ...         sourcename=source_name,
-    ...         status=PackagePublishingStatus.PUBLISHED,
-    ...         distroseries=warty)
-    >>> import transaction
-    >>> transaction.commit()
-
-There are many more new packages to be displayed on the page now:
-
-    >>> name16.getLatestUploadedPPAPackages().count() > max_results
-    True
-
-    >>> name16.getLatestMaintainedPackages().count() > max_results
-    True
-
-    >>> (name16.getLatestUploadedButNotMaintainedPackages().count() >
-    ...     max_results)
-    True
-
-The view enforces the limit.
-
-    >>> len(view.get_latest_uploaded_ppa_packages_with_stats) == max_results
-    True
-
-    >>> len(view.get_latest_maintained_packages_with_stats) == max_results
-    True
-
-    >>> (len(view.get_latest_uploaded_but_not_maintained_packages_with_stats)
-    ...     == max_results)
-    True
-
-The view has a helper method that returns a message that can be used
-at the head of each table.
-
-    >>> view._tableHeaderMessage(100)
-    'Displaying first ... packages out of 100 total'
-
-    >>> view._tableHeaderMessage(max_results)
-    '... packages'
-
-    >>> view._tableHeaderMessage(1)
-    '1 package'
-
-
-== Person contacting another person ==
+Person contacting another person
+--------------------------------
 
 The PersonView provides information to make the link to contact a user.
 No Privileges Person can send a message to Sample Person, even though
@@ -621,75 +577,88 @@
     >>> print view.contact_link_title
     Send an email to this user through Launchpad
 
-The EmailToPersonView provides many properties to the page template
-to explain exactly who is being contacted.
+The EmailToPersonView provides many properties to the page template to
+explain exactly who is being contacted.
 
     >>> view = create_initialized_view(sample_person, '+contactuser')
     >>> print view.label
-    Contact Sample Person
+    Contact user
+
     >>> print view.specific_contact_title_text
     Contact this user
+
     >>> print view.recipients.description
     You are contacting Sample Person (name12).
+
     >>> [recipient.name for recipient in view.recipients]
     [u'name12']
 
 
-== Person contacting himself ==
+Person contacting himself
+-------------------------
 
-For consistency and testing purposes, the "+contactuser" page is available
-even when someone is looking at his own profile page.  The wording on the
-tooltip is different though. No Privileges Person can send a message to
-himself.
+For consistency and testing purposes, the "+contactuser" page is
+available even when someone is looking at his own profile page.  The
+wording on the tooltip is different though. No Privileges Person can
+send a message to himself.
 
     >>> no_priv = person_set.getByEmail('no-priv@xxxxxxxxxxxxx')
     >>> view = create_initialized_view(no_priv, '+index')
     >>> print view.contact_link_title
     Send an email to yourself through Launchpad
 
-The EmailToPersonView provides the explanation about who is being contacted.
+The EmailToPersonView provides the explanation about who is being
+contacted.
 
     >>> view = create_initialized_view(no_priv, '+contactuser')
     >>> print view.label
-    Contact No Privileges Person
+    Contact user
+
     >>> print view.specific_contact_title_text
     Contact yourself
+
     >>> print view.recipients.description
     You are contacting No Privileges Person (no-priv).
+
     >>> [recipient.name for recipient in view.recipients]
     [u'no-priv']
 
 
-== Non-member contacting a Team ==
+Non-member contacting a Team
+----------------------------
 
-Users can contact teams, but the behaviour depends upon whether the
-user is a member of the team. No Privileges Person is not a member of
-the Landscape Developers team.
+Users can contact teams, but the behaviour depends upon whether the user
+is a member of the team. No Privileges Person is not a member of the
+Landscape Developers team.
 
     >>> view = create_initialized_view(landscape_developers, '+index')
     >>> print view.contact_link_title
     Send an email to this team's owner through Launchpad
 
-The EmailToPersonView can be used by non-members to contact the
-team owner.
+The EmailToPersonView can be used by non-members to contact the team
+owner.
 
     >>> view = create_initialized_view(landscape_developers, '+contactuser')
     >>> print view.label
-    Contact Landscape Developers
+    Contact user
+
     >>> print view.specific_contact_title_text
     Contact this team
+
     >>> print view.recipients.description
     You are contacting the Landscape Developers (landscape-developers) team
     owner, Sample Person (name12).
+
     >>> [recipient.name for recipient in view.recipients]
     [u'name12']
 
 
-== Member contacting a Team ==
+Member contacting a Team
+------------------------
 
 Members can contact their team. How they are contacted depends upon
-whether the team's contact address is set. Sample Person can contact
-his team, Landscape developers, even though they do not have a contact
+whether the team's contact address is set. Sample Person can contact his
+team, Landscape developers, even though they do not have a contact
 address.
 
     >>> login('test@xxxxxxxxxxxxx')
@@ -701,12 +670,15 @@
 
     >>> view = create_initialized_view(landscape_developers, '+contactuser')
     >>> print view.label
-    Contact Landscape Developers
+    Contact user
+
     >>> print view.specific_contact_title_text
     Contact your team
+
     >>> print view.recipients.description
     You are contacting 2 members of the Landscape Developers
     (landscape-developers) team directly.
+
     >>> [recipient.name for recipient in view.recipients]
     [u'salgado', u'name12']
 
@@ -715,22 +687,26 @@
     >>> recipients = view.recipients
     >>> len(recipients)
     2
+
     >>> bool(recipients)
     True
 
 If there is only one member of the team, who must therefore be the user
-sending the email, and also be the team owner, The view provides a special
-message just for him.
+sending the email, and also be the team owner, The view provides a
+special message just for him.
 
     >>> vanity_team = factory.makeTeam(
     ...     sample_person, displayname='Vanity', name='vanity')
     >>> view = create_initialized_view(vanity_team, '+contactuser')
     >>> print view.label
-    Contact Vanity
+    Contact user
+
     >>> print view.specific_contact_title_text
     Contact your team
+
     >>> print view.recipients.description
     You are contacting 1 member of the Vanity (vanity) team directly.
+
     >>> [recipient.name for recipient in view.recipients]
     [u'name12']
 
@@ -741,39 +717,39 @@
     >>> landscape_developers.setContactAddress(email_address)
 
     >>> view = create_initialized_view(landscape_developers, '+contactuser')
-    >>> print view.label
-    Contact Landscape Developers
-    >>> print view.specific_contact_title_text
-    Contact your team
     >>> print view.recipients.description
     You are contacting the Landscape Developers (landscape-developers) team.
+
     >>> [recipient.name for recipient in view.recipients]
     [u'landscape-developers']
 
 
-== Contact this user/team valid addresses and quotas ==
+Contact this user/team valid addresses and quotas
+-------------------------------------------------
 
-The EmailToPersonView has_valid_email_address property is normally
-True. The is_possible property is True when contact_is_allowed and
+The EmailToPersonView has_valid_email_address property is normally True.
+The is_possible property is True when contact_is_allowed and
 has_valid_email_address are both True.
 
     >>> view = create_initialized_view(landscape_developers, '+contactuser')
     >>> view.has_valid_email_address
     True
+
     >>> view.contact_is_possible
     True
 
 The EmailToPersonView provides two properties that check that the user
-is_allowed to send emails because he has not exceeded the daily quota. The
-next_try property is the date when the user will be allowed to send emails
-again. The is_possible property is True when both contact_is_allowed and
-as_valid_email_address are True.
-
-The daily quota is set to 3 emails per day. See the "Message quota"
-in `doc/user-to-user.txt` to see how these two attributes are used.
-
-
-== Invalid users and anonymous contacters ==
+is_allowed to send emails because he has not exceeded the daily quota.
+The next_try property is the date when the user will be allowed to send
+emails again. The is_possible property is True when both
+contact_is_allowed and as_valid_email_address are True.
+
+The daily quota is set to 3 emails per day. See the "Message quota" in
+`doc/user-to-user.txt` to see how these two attributes are used.
+
+
+Invalid users and anonymous contacters
+--------------------------------------
 
 Inactive users and users without a preferred email address are invalid
 and cannot be contacted.
@@ -782,11 +758,14 @@
     >>> view = create_initialized_view(former_user, '+contactuser')
     >>> view.request.response.getStatus()
     302
+
     >>> print view.request.response.getHeader('Location')
     http://launchpad.dev/~former-user-deactivatedaccount
+
     >>> recipients = view.recipients
     >>> len(recipients)
     0
+
     >>> bool(recipients)
     False
 
@@ -798,14 +777,16 @@
     >>> view = create_initialized_view(landscape_developers, '+contactuser')
     >>> view.request.response.getStatus()
     302
+
     >>> print view.request.response.getHeader('Location')
     http://launchpad.dev/~landscape-developers
 
 
-== Messages and subjects cannot be empty ==
+Messages and subjects cannot be empty
+-------------------------------------
 
-Messages or subjects that contain only whitespace are treated as an error
-that the user must fix.
+Messages or subjects that contain only whitespace are treated as an
+error that the user must fix.
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> view = create_initialized_view(
@@ -819,13 +800,15 @@
     [u'You must provide a subject and a message.']
 
 
-== Person +index "Personal package archives" section ==
+Person +index "Personal package archives" section
+-------------------------------------------------
 
-The person:+index page has a section titled "Personal package
-archives", which is conditionally displayed depending on the value of the
-view property `should_show_ppa_section`.
+The person:+index page has a section titled "Personal package archives",
+which is conditionally displayed depending on the value of the view
+property `should_show_ppa_section`.
 
 The property checks two things to decide whether to return True or not:
+
  * Return True if the current user has launchpad.Edit permission
  * Return True if the person has PPAs and at least one of them is viewable
    by the current user.
@@ -879,9 +862,8 @@
     >>> view.should_show_ppa_section
     False
 
-For a user with no PPAs, nobody will see the section apart from
-himself. This aspect allows him to access the 'Create a new PPA'
-link.
+For a user with no PPAs, nobody will see the section apart from himself.
+This aspect allows him to access the 'Create a new PPA' link.
 
     >>> print sample_person.archive
     None
@@ -896,15 +878,22 @@
     >>> view.should_show_ppa_section
     False
 
-If the person is a member of teams with PPAs but doesn't own any himself, the
-section will still not appear for anyone but people with lp.edit.
+If the person is a member of teams with PPAs but doesn't own any
+himself, the section will still not appear for anyone but people with
+lp.edit.
+
+    >>> from canonical.launchpad.interfaces.launchpad import (
+    ...     ILaunchpadCelebrities)
 
     >>> login("admin@xxxxxxxxxxxxx")
     >>> team = factory.makeTeam()
     >>> ignored = team.addMember(sample_person, sample_person)
+    >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
     >>> ppa = factory.makeArchive(distribution=ubuntu, owner=team)
 
     >>> login(ANONYMOUS)
     >>> view = create_initialized_view(sample_person, "+index")
     >>> view.should_show_ppa_section
     False
+
+

=== added file 'lib/lp/registry/browser/tests/test_branding.py'
--- lib/lp/registry/browser/tests/test_branding.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_branding.py	2010-07-16 10:17:44 +0000
@@ -0,0 +1,33 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for Branding."""
+
+__metaclass__ = type
+
+import unittest
+
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.registry.browser.branding import BrandingChangeView
+from lp.testing import TestCaseWithFactory
+
+
+class TestBrandingChangeView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestBrandingChangeView, self).setUp()
+        self.context = self.factory.makePerson(name='cow')
+        self.view = BrandingChangeView(self.context, LaunchpadTestRequest())
+
+    def test_common_attributes(self):
+        # The canonical URL of a GPG key is ssh-keys
+        label = 'Change the images used to represent Cow in Launchpad'
+        self.assertEqual(label, self.view.label)
+        self.assertEqual('Change branding', self.view.page_title)
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py	2010-06-18 15:06:32 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py	2010-07-16 10:17:44 +0000
@@ -8,7 +8,9 @@
 import transaction
 from zope.component import getUtility
 
+from canonical.config import config
 from canonical.launchpad.ftests import ANONYMOUS, login
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.webapp.interfaces import NotFoundError
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.testing import (
@@ -21,8 +23,9 @@
 from lp.registry.model.karma import KarmaCategory
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.soyuz.interfaces.archive import ArchiveStatus
+from lp.soyuz.interfaces.publishing import PackagePublishingStatus
 from lp.testing import TestCaseWithFactory, login_person
-from lp.testing.views import create_view
+from lp.testing.views import create_initialized_view, create_view
 
 
 class TestPersonViewKarma(TestCaseWithFactory):
@@ -357,6 +360,149 @@
         self.assertEqual(True, self.view.has_participations)
 
 
+class TestPersonRelatedSoftwareView(TestCaseWithFactory):
+    """Test the related software view."""
+
+    layer = LaunchpadFunctionalLayer
+
+    def setUp(self):
+        super(TestPersonRelatedSoftwareView, self).setUp()
+        self.user = self.factory.makePerson()
+        self.factory.makeGPGKey(self.user)
+        self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+        self.warty = self.ubuntu.getSeries('warty')
+        self.view = create_initialized_view(self.user, '+related-software')
+
+    def publishSource(self, archive, maintainer):
+        publisher = SoyuzTestPublisher()
+        publisher.person = self.user
+        login('foo.bar@xxxxxxxxxxxxx')
+        for count in range(0, self.view.max_results_to_display + 3):
+            source_name = "foo" + str(count)
+            publisher.getPubSource(
+                sourcename=source_name,
+                status=PackagePublishingStatus.PUBLISHED,
+                archive=archive,
+                maintainer = maintainer,
+                creator = self.user,
+                distroseries=self.warty)
+        login(ANONYMOUS)
+
+    def test_view_helper_attributes(self):
+        # Verify view helper attributes.
+        self.assertEqual('Related software', self.view.page_title)
+        self.assertEqual('summary_list_size', self.view._max_results_key)
+        self.assertEqual(
+            config.launchpad.summary_list_size,
+            self.view.max_results_to_display)
+
+    def test_tableHeaderMessage(self):
+        limit = self.view.max_results_to_display
+        expected = 'Displaying first %s packages out of 100 total' % limit
+        self.assertEqual(expected, self.view._tableHeaderMessage(100))
+        expected = '%s packages' % limit
+        self.assertEqual(expected, self.view._tableHeaderMessage(limit))
+        expected = '1 package'
+        self.assertEqual(expected, self.view._tableHeaderMessage(1))
+
+    def test_latest_uploaded_ppa_packages_with_stats(self):
+        # Verify number of PPA packages to display.
+        ppa = self.factory.makeArchive(owner=self.user)
+        self.publishSource(ppa, self.user)
+        count = len(self.view.latest_uploaded_ppa_packages_with_stats)
+        self.assertEqual(self.view.max_results_to_display, count)
+
+    def test_latest_maintained_packages_with_stats(self):
+        # Verify number of maintained packages to display.
+        self.publishSource(self.warty.main_archive, self.user)
+        count = len(self.view.latest_maintained_packages_with_stats)
+        self.assertEqual(self.view.max_results_to_display, count)
+
+    def test_latest_uploaded_nonmaintained_packages_with_stats(self):
+        # Verify number of non maintained packages to display.
+        maintainer = self.factory.makePerson()
+        self.publishSource(self.warty.main_archive, maintainer)
+        count = len(
+            self.view.latest_uploaded_but_not_maintained_packages_with_stats)
+        self.assertEqual(self.view.max_results_to_display, count)
+
+
+class TestPersonMaintainedPackagesView(TestCaseWithFactory):
+    """Test the maintained packages view."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestPersonMaintainedPackagesView, self).setUp()
+        self.user = self.factory.makePerson()
+        self.view = create_initialized_view(self.user, '+maintained-packages')
+
+    def test_view_helper_attributes(self):
+        # Verify view helper attributes.
+        self.assertEqual('Maintained Packages', self.view.page_title)
+        self.assertEqual('default_batch_size', self.view._max_results_key)
+        self.assertEqual(
+            config.launchpad.default_batch_size,
+            self.view.max_results_to_display)
+
+
+class TestPersonUploadedPackagesView(TestCaseWithFactory):
+    """Test the maintained packages view."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestPersonUploadedPackagesView, self).setUp()
+        self.user = self.factory.makePerson()
+        self.view = create_initialized_view(self.user, '+uploaded-packages')
+
+    def test_view_helper_attributes(self):
+        # Verify view helper attributes.
+        self.assertEqual('Uploaded packages', self.view.page_title)
+        self.assertEqual('default_batch_size', self.view._max_results_key)
+        self.assertEqual(
+            config.launchpad.default_batch_size,
+            self.view.max_results_to_display)
+
+
+class TestPersonPPAPackagesView(TestCaseWithFactory):
+    """Test the maintained packages view."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestPersonPPAPackagesView, self).setUp()
+        self.user = self.factory.makePerson()
+        self.view = create_initialized_view(self.user, '+ppa-packages')
+
+    def test_view_helper_attributes(self):
+        # Verify view helper attributes.
+        self.assertEqual('PPA packages', self.view.page_title)
+        self.assertEqual('default_batch_size', self.view._max_results_key)
+        self.assertEqual(
+            config.launchpad.default_batch_size,
+            self.view.max_results_to_display)
+
+
+class TestPersonRelatedProjectsView(TestCaseWithFactory):
+    """Test the maintained packages view."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestPersonRelatedProjectsView, self).setUp()
+        self.user = self.factory.makePerson()
+        self.view = create_initialized_view(self.user, '+related-projects')
+
+    def test_view_helper_attributes(self):
+        # Verify view helper attributes.
+        self.assertEqual('Related projects', self.view.page_title)
+        self.assertEqual('default_batch_size', self.view._max_results_key)
+        self.assertEqual(
+            config.launchpad.default_batch_size,
+            self.view.max_results_to_display)
+
+
 class TestPersonRelatedSoftwareFailedBuild(TestCaseWithFactory):
     """The related software views display links to failed builds."""
 

=== modified file 'lib/lp/registry/browser/tests/user-to-user-views.txt'
--- lib/lp/registry/browser/tests/user-to-user-views.txt	2009-08-22 16:51:26 +0000
+++ lib/lp/registry/browser/tests/user-to-user-views.txt	2010-07-16 10:17:44 +0000
@@ -1,7 +1,8 @@
-= User-to-user direct email contact =
+User-to-user direct email contact
+=================================
 
-A Launchpad user can contact another Launchpad user directly, even if the
-recipient is hiding their email addresses.
+A Launchpad user can contact another Launchpad user directly, even if
+the recipient is hiding their email addresses.
 
     >>> def create_view(sender, recipient, form=None):
     ...     return create_initialized_view(
@@ -29,7 +30,8 @@
 This contact is allowed.
 
     >>> print view.label
-    Contact Guilherme Salgado
+    Contact user
+
     >>> view.contact_is_allowed
     True
 
@@ -51,6 +53,7 @@
     Message sent to Guilherme Salgado
 
     # Capture the date of the last contact for later.
+
     >>> from canonical.config import config
     >>> from canonical.launchpad.database.message import UserToUserEmail
     >>> from lazr.config import as_timedelta
@@ -61,30 +64,31 @@
     >>> expires = first_contact.date_sent + as_timedelta(
     ...     config.launchpad.user_to_user_throttle_interval)
 
-No Priv sends two more messages to Salgado.  Each of these are allowed too.
-
-    >>> view = create_view(
-    ...     no_priv, salgado, {
-    ...     'field.field.from_': 'no-priv@xxxxxxxxxxxxx',
-    ...     'field.subject': 'Hello Salgado',
-    ...     'field.message': 'Can you tell me about your project?',
-    ...     'field.actions.send': 'Send',
-    ...     })
-    >>> print_notifications(view)
-    Message sent to Guilherme Salgado
-
-    >>> view = create_view(
-    ...     no_priv, salgado, {
-    ...     'field.field.from_': 'no-priv@xxxxxxxxxxxxx',
-    ...     'field.subject': 'Hello Salgado',
-    ...     'field.message': 'Can you tell me about your project?',
-    ...     'field.actions.send': 'Send',
-    ...     })
-    >>> print_notifications(view)
-    Message sent to Guilherme Salgado
-
-Now however, No Priv had reached her quota for direct user-to-user contact and
-is not allowed to send a fourth message today.
+No Priv sends two more messages to Salgado.  Each of these are allowed
+too.
+
+    >>> view = create_view(
+    ...     no_priv, salgado, {
+    ...     'field.field.from_': 'no-priv@xxxxxxxxxxxxx',
+    ...     'field.subject': 'Hello Salgado',
+    ...     'field.message': 'Can you tell me about your project?',
+    ...     'field.actions.send': 'Send',
+    ...     })
+    >>> print_notifications(view)
+    Message sent to Guilherme Salgado
+
+    >>> view = create_view(
+    ...     no_priv, salgado, {
+    ...     'field.field.from_': 'no-priv@xxxxxxxxxxxxx',
+    ...     'field.subject': 'Hello Salgado',
+    ...     'field.message': 'Can you tell me about your project?',
+    ...     'field.actions.send': 'Send',
+    ...     })
+    >>> print_notifications(view)
+    Message sent to Guilherme Salgado
+
+Now however, No Priv had reached her quota for direct user-to-user
+contact and is not allowed to send a fourth message today.
 
     >>> view = create_view(no_priv, salgado)
     >>> view.contact_is_allowed
@@ -95,13 +99,13 @@
     >>> view.next_try == expires
     True
 
-As a corner case, let's say the number of notifications allowed was greater
-yesterday than it was today.
+As a corner case, let's say the number of notifications allowed was
+greater yesterday than it was today.
 
     >>> config.push('seven_allowed', """\
-    ... [launchpad]
-    ... user_to_user_max_messages: 7
-    ... """)
+    ...    [launchpad]
+    ...    user_to_user_max_messages: 7
+    ...    """)
 
 No Priv can actually try again right now.
 
@@ -133,6 +137,7 @@
     >>> view = create_view(no_priv, salgado)
     >>> view.contact_is_allowed
     False
+
     >>> view.next_try == expires
     True
 
@@ -140,6 +145,7 @@
 
     >>> config.pop('seven_allowed')
     (...)
+
     >>> contacts = Store.of(no_priv).find(
     ...     UserToUserEmail,
     ...     UserToUserEmail.sender == no_priv)
@@ -148,10 +154,11 @@
     ...     config.launchpad.user_to_user_throttle_interval)
 
 
-== Non-ASCII names ==
+Non-ASCII names
+---------------
 
-Carlos has non-ASCII characters in his name.  When he sends a message to a
-user, his real name will be properly RFC 2047 encoded.
+Carlos has non-ASCII characters in his name.  When he sends a message to
+a user, his real name will be properly RFC 2047 encoded.
 
     >>> transaction.abort()
     >>> from lp.services.mail import stub
@@ -172,6 +179,7 @@
 
     >>> len(stub.test_emails)
     1
+
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
     >>> print raw_msg
     Content-Type: text/plain; charset="us-ascii"
@@ -180,8 +188,8 @@
     To: No Privileges Person <no-priv@xxxxxxxxxxxxx>
     ...
 
-Similarly, if Carlos is the recipient of a message, his real name will be
-properly RFC 2047 encoded as well.
+Similarly, if Carlos is the recipient of a message, his real name will
+be properly RFC 2047 encoded as well.
 
     >>> del stub.test_emails[:]
 
@@ -197,6 +205,7 @@
 
     >>> len(stub.test_emails)
     1
+
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
     >>> print raw_msg
     Content-Type: text/plain; charset="us-ascii"
@@ -206,7 +215,8 @@
     ...
 
 
-== Hidden addresses ==
+Hidden addresses
+----------------
 
 Salgado decides to hide his email addresses.
 
@@ -230,13 +240,15 @@
     Message sent to Guilherme Salgado
 
 
-== Contacting teams ==
+Contacting teams
+----------------
 
 Teams can also be contacted directly, regardless of whether they have no
-official contact address, use a Launchpad mailing list, or have the contact
-address set to an explicit address.
+official contact address, use a Launchpad mailing list, or have the
+contact address set to an explicit address.
 
     # Clear out left over crud.
+
     >>> transaction.commit()
     >>> del stub.test_emails[:]
 
@@ -266,7 +278,8 @@
     ...         print '   ', recipient
 
 
-=== Non-member to team ===
+Non-member to team
+..................
 
 Non-members may only contact the team owner.
 
@@ -301,7 +314,8 @@
         Foo Bar <foo.bar@xxxxxxxxxxxxx>
 
 
-=== Member to team ===
+Member to team
+..............
 
 Foo Bar is a member of Guadamen team, he is not restricted to contacting
 the team owner. The Guadamen team has no contact address, so contacting
@@ -319,9 +333,9 @@
     >>> print_notifications(view)
     Message sent to GuadaMen
 
-There are 10 members of the team, so exactly 10 unique copies of the message
-are sent, one to each team member. Everyone gets a message with the same
-subject and body from the same sender.
+There are 10 members of the team, so exactly 10 unique copies of the
+message are sent, one to each team member. Everyone gets a message with
+the same subject and body from the same sender.
 
     >>> transaction.commit()
     >>> print_messages()
@@ -356,12 +370,13 @@
     ...     guadamen.name, guadamen.teamowner.name)
 
     # Ignore the 'new mailing list message'
+
     >>> transaction.commit()
     >>> del stub.test_emails[:]
 
-Foo Bar now contacts them again, which he can do because his quota is still
-not met. This message includes a "%s" combination; it is not a interpolation
-instruction.
+Foo Bar now contacts them again, which he can do because his quota is
+still not met. This message includes a "%s" combination; it is not a
+interpolation instruction.
 
     >>> view = create_view(
     ...     foo_bar, guadamen, {
@@ -410,9 +425,9 @@
     >>> address = email_set.new('guadamen@xxxxxxxxxxx', guadamen)
     >>> guadamen.setContactAddress(address)
 
-Foo Bar contacts the Guadamen team again, which is allowed because his quota
-was not met by his first message.  This time only one message is sent, and
-that to the new contact address.
+Foo Bar contacts the Guadamen team again, which is allowed because his
+quota was not met by his first message.  This time only one message is
+sent, and that to the new contact address.
 
     >>> view = create_view(
     ...     foo_bar, guadamen, {
@@ -427,9 +442,11 @@
     >>> transaction.commit()
     >>> len(stub.test_emails)
     1
+
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
     >>> print from_addr, to_addrs
     bounces@xxxxxxxxxxxxx [u'GuadaMen <guadamen@xxxxxxxxxxx>']
+
     >>> print raw_msg
     Content-Type: text/plain; charset="us-ascii"
     ...
@@ -449,14 +466,17 @@
     https://help.launchpad.net/YourAccount/ContactingPeople
 
 
-== Message quota ==
+Message quota
+-------------
 
 The EmailToPersonView provides two properties that check that the user
-is_allowed to send emails because he has not exceeded the daily quota. The
-next_try property is the date when the user will be allowed to send emails
-again. The is_possible property will be False if is_allowed is False.
+is_allowed to send emails because he has not exceeded the daily quota.
+The next_try property is the date when the user will be allowed to send
+emails again. The is_possible property will be False if is_allowed is
+False.
 
-Foo Bar has now reached his quota and can send no more contact messages today.
+Foo Bar has now reached his quota and can send no more contact messages
+today.
 
     >>> view = create_view(
     ...     foo_bar, guadamen, {
@@ -467,10 +487,13 @@
     ...         })
     >>> view.contact_is_allowed
     False
+
     >>> view.next_try
     datetime.datetime...
+
     >>> view.contact_is_possible
     False
+
     >>> print_notifications(view)
     Your message was not sent because you have exceeded your daily quota of
     3 messages to contact users. Try again in ...
@@ -481,15 +504,17 @@
     >>> view = create_view(bart, guadamen)
     >>> view.contact_is_allowed
     True
+
     >>> view.contact_is_possible
     True
 
 
-== Identifying information ==
+Identifying information
+-----------------------
 
-Every contact message has a special Launchpad header so that people can tell
-that the message came to them through Launchpad. It has a footer that
-contains an explanation as well.
+Every contact message has a special Launchpad header so that people can
+tell that the message came to them through Launchpad. It has a footer
+that contains an explanation as well.
 
     >>> cris = factory.makePerson(email='cris@xxxxxxxxxxx', name='cris')
     >>> dave = factory.makePerson(email='dave@xxxxxxxxxxx', name='dave')
@@ -523,11 +548,12 @@
     https://help.launchpad.net/YourAccount/ContactingPeople
 
 
-== Message wrapping ==
+Message wrapping
+----------------
 
-The message body is wrapped at 72 characters. The footer is not wrapped, but
-a new line is started after the names of the sender and the recient to
-minimise long lines.
+The message body is wrapped at 72 characters. The footer is not wrapped,
+but a new line is started after the names of the sender and the recient
+to minimise long lines.
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> sample_person = person_set.getByEmail('test@xxxxxxxxxxxxx')
@@ -554,3 +580,4 @@
     ^For more information see$
     ^https://help.launchpad.net/YourAccount/ContactingPeople$
 
+

=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt	2010-04-07 12:50:17 +0000
+++ lib/lp/registry/doc/person-account.txt	2010-07-16 10:17:44 +0000
@@ -1,10 +1,12 @@
-= Person and Account =
+Person and Account
+==================
 
 The Person object is responsible for updating the status of its
 Account object.
 
 
-== Activating user accounts ==
+Activating user accounts
+------------------------
 
 A user may activate their account that was created by an automated
 process. Matsubara's account was created during a code import.
@@ -54,7 +56,8 @@
     u'matsubara@xxxxxxxxxxxx'
 
 
-== Deactivating user accounts ==
+Deactivating user accounts
+--------------------------
 
 Any user can deactivate his own account, in case they don't want it
 anymore or they don't want to be shown as Launchpad users.
@@ -182,7 +185,7 @@
 
 ...no owned or driven pillars...
 
-    >>> foobar.getOwnedOrDrivenPillars().count()
+    >>> len(foobar.getOwnedOrDrivenPillars())
     0
 
 ...and, finally, to not be considered a valid person in Launchpad.
@@ -206,7 +209,8 @@
     True
 
 
-== Reactivating user accounts ==
+Reactivating user accounts
+--------------------------
 
 Accounts can be reactivated. A comment and a non-None preferred email address
 are required to reactivate() an account, though.

=== modified file 'lib/lp/registry/doc/person.txt'
--- lib/lp/registry/doc/person.txt	2010-04-23 16:54:21 +0000
+++ lib/lp/registry/doc/person.txt	2010-07-16 10:17:44 +0000
@@ -1132,25 +1132,22 @@
     ...     elif IProduct.providedBy(pillar):
     ...         pillar_type = 'project'
     ...     print "%s: %s (%s)" % (
-    ...         pillar_type, pillar.title, pillar.name)
+    ...         pillar_type, pillar.displayname, pillar.name)
 
     >>> for pillarname in mark.getOwnedOrDrivenPillars():
     ...     print_pillar(pillarname)
-    distribution: Ubuntu Linux (ubuntu)
-    distribution: Redhat Advanced Server (redhat)
-    distribution: Debian GNU/Linux (debian)
-    distribution: The Gentoo Linux (gentoo)
-    distribution: Kubuntu - Free KDE-based Linux (kubuntu)
-    distribution: Ubuntu Test (ubuntutest)
+    distribution: Debian (debian)
+    distribution: Gentoo (gentoo)
+    distribution: Kubuntu (kubuntu)
+    distribution: Red Hat (redhat)
     project group: Apache (apache)
-    project: Tomcat (tomcat)
-    project: ALSA utilities (alsa-utils)
-    project: Derby - Java Database (derby)
+    project: Derby (derby)
+    project: alsa-utils (alsa-utils)
 
     >>> for pillarname in ubuntu_team.getOwnedOrDrivenPillars():
     ...     print_pillar(pillarname)
-    distribution: Ubuntu Linux (ubuntu)
-    distribution: Ubuntu Test (ubuntutest)
+    distribution: Ubuntu (ubuntu)
+    distribution: ubuntutest (ubuntutest)
     project: Tomcat (tomcat)
 
 

=== modified file 'lib/lp/registry/doc/sshkey.txt'
--- lib/lp/registry/doc/sshkey.txt	2009-04-17 10:32:16 +0000
+++ lib/lp/registry/doc/sshkey.txt	2010-07-16 10:17:44 +0000
@@ -20,13 +20,13 @@
 Adding new keys is pretty easy:
 
     >>> foobar = personset.getByName('name16')
-    >>> key = sshkeyset.new(foobar, SSHKeyType.RSA, "zzzNOT-REALLY",
-    ...     "This is just a test key.")
+    >>> key = sshkeyset.new(
+    ...     foobar, "ssh-rsa zzzNOT-REALLY This is just a test key")
     >>> key, key.keytext
     (<SSHKey at ...>, u'zzzNOT-REALLY')
 
-    >>> key = sshkeyset.new(name12, SSHKeyType.RSA, "zzzNOT-EITHER",
-    ...     "This is just a test key.")
+    >>> key = sshkeyset.new(
+    ...     name12, "ssh-rsa zzzNOT-EITHER This is just a test key.")
     >>> key, key.keytext
     (<SSHKey at ...>, u'zzzNOT-EITHER')
 

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2010-07-13 15:29:08 +0000
+++ lib/lp/registry/interfaces/person.py	2010-07-16 10:17:44 +0000
@@ -999,9 +999,7 @@
         """
 
     def getOwnedOrDrivenPillars():
-        """Return Distribution, Project Groups and Projects that this person
-        owns or drives.
-        """
+        """Return the pillars that this person directly owns or drives."""
 
     def getOwnedProjects(match_name=None):
         """Projects owned by this person or teams to which she belongs.
@@ -2130,6 +2128,7 @@
         requiring a Launchpad account.
         """
 
+
 class ISoftwareCenterAgentApplication(ILaunchpadApplication):
     """XMLRPC application root for ISoftwareCenterAgentAPI."""
 

=== modified file 'lib/lp/registry/interfaces/ssh.py'
--- lib/lp/registry/interfaces/ssh.py	2010-03-24 20:19:52 +0000
+++ lib/lp/registry/interfaces/ssh.py	2010-07-16 10:17:44 +0000
@@ -10,6 +10,8 @@
 __all__ = [
     'ISSHKey',
     'ISSHKeySet',
+    'SSHKeyAdditionError',
+    'SSHKeyCompromisedError',
     'SSHKeyType',
     ]
 
@@ -63,7 +65,7 @@
 class ISSHKeySet(Interface):
     """The set of SSHKeys."""
 
-    def new(person, keytype, keytext, comment):
+    def new(person, sshkey):
         """Create a new SSHKey pointing to the given Person."""
 
     def getByID(id, default=None):
@@ -75,3 +77,11 @@
     def getByPeople(people):
         """Return SSHKey object associated to the people provided."""
 
+
+class SSHKeyAdditionError(Exception):
+    """Raised when the SSH public key is invalid."""
+
+
+class SSHKeyCompromisedError(Exception):
+    """Raised when the SSH public key is known to be easily compromisable."""
+

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2010-07-12 08:45:32 +0000
+++ lib/lp/registry/model/person.py	2010-07-16 10:17:44 +0000
@@ -31,6 +31,7 @@
 import pytz
 import random
 import re
+import subprocess
 import time
 import weakref
 
@@ -126,7 +127,9 @@
     SpecificationDefinitionStatus, SpecificationFilter,
     SpecificationImplementationStatus, SpecificationSort)
 from canonical.launchpad.interfaces.lpstorm import IStore
-from lp.registry.interfaces.ssh import ISSHKey, ISSHKeySet, SSHKeyType
+from lp.registry.interfaces.ssh import (
+    ISSHKey, ISSHKeySet, SSHKeyAdditionError, SSHKeyCompromisedError,
+    SSHKeyType)
 from lp.registry.interfaces.teammembership import (
     TeamMembershipStatus)
 from lp.registry.interfaces.wikiname import IWikiName, IWikiNameSet
@@ -183,7 +186,6 @@
 
     This is readonly, as this is a view in the database.
     """
-    # Look Ma, no columns! (apart from id)
 
 
 def validate_person_visibility(person, attr, value):
@@ -722,7 +724,7 @@
 
         # filter based on completion. see the implementation of
         # Specification.is_complete() for more details
-        completeness =  Specification.completeness_clause
+        completeness = Specification.completeness_clause
 
         if SpecificationFilter.COMPLETE in filter:
             query += ' AND ( %s ) ' % completeness
@@ -898,40 +900,33 @@
 
     def getOwnedOrDrivenPillars(self):
         """See `IPerson`."""
-        query = """
-            SELECT name
-            FROM product, teamparticipation
-            WHERE teamparticipation.person = %(person)s
-                AND (driver = teamparticipation.team
-                     OR owner = teamparticipation.team)
-
-            UNION
-
-            SELECT name
-            FROM project, teamparticipation
-            WHERE teamparticipation.person = %(person)s
-                AND (driver = teamparticipation.team
-                     OR owner = teamparticipation.team)
-
-            UNION
-
-            SELECT name
-            FROM distribution, teamparticipation
-            WHERE teamparticipation.person = %(person)s
-                AND (driver = teamparticipation.team
-                     OR owner = teamparticipation.team)
-            """ % sqlvalues(person=self)
-        cur = cursor()
-        cur.execute(query)
-        names = [sqlvalues(str(name)) for [name] in cur.fetchall()]
-        if not names:
-            return PillarName.select("1=2")
-        quoted_names = ','.join([name for [name] in names])
-        return PillarName.select(
-            "PillarName.name IN (%s) AND PillarName.active IS TRUE" %
-            quoted_names, prejoins=['distribution', 'project', 'product'],
-            orderBy=['PillarName.distribution', 'PillarName.project',
-                     'PillarName.product'])
+        find_spec = (PillarName, SQL('kind'), SQL('displayname'))
+        origin = SQL("""
+            PillarName
+            JOIN (
+                SELECT name, 3 as kind, displayname
+                FROM product
+                WHERE
+                    driver = %(person)s
+                    OR owner = %(person)s
+                UNION
+                SELECT name, 2 as kind, displayname
+                FROM project
+                WHERE
+                    driver = %(person)s
+                    OR owner = %(person)s
+                UNION
+                SELECT name, 1 as kind, displayname
+                FROM distribution
+                WHERE
+                    driver = %(person)s
+                    OR owner = %(person)s
+                ) _pillar
+                ON PillarName.name = _pillar.name
+            """ % sqlvalues(person=self))
+        results = IStore(self).using(origin).find(find_spec)
+        results = results.order_by('kind', 'displayname')
+        return [pillar_name for pillar_name, kind, displayname in results]
 
     def getOwnedProjects(self, match_name=None):
         """See `IPerson`."""
@@ -1444,7 +1439,7 @@
     @property
     def wiki_names(self):
         """See `IPerson`."""
-        result =  Store.of(self).find(WikiName, WikiName.person == self.id)
+        result = Store.of(self).find(WikiName, WikiName.person == self.id)
         return result.order_by(WikiName.wiki, WikiName.wikiname)
 
     @property
@@ -1608,7 +1603,7 @@
             Person.teamowner IS NULL
             """ % sqlvalues(self.id),
             clauseTables=['TeamParticipation', 'Person'],
-            prejoins=['person',], limit=limit)
+            prejoins=['person', ], limit=limit)
 
     def getMappedParticipants(self, limit=None):
         """See `IPersonViewRestricted`."""
@@ -1641,7 +1636,7 @@
         min_lng = 180.0
         locations = self._getMappedParticipantsLocations(limit)
         if self.mapped_participants_count == 0:
-            raise AssertionError, (
+            raise AssertionError(
                 'This method cannot be called when '
                 'mapped_participants_count == 0.')
         latitudes = sorted(location.latitude for location in locations)
@@ -1821,7 +1816,7 @@
             ('teamparticipation', 'team'),
             # Skip mailing lists because if the mailing list is purged, it's
             # not a problem.  Do this check separately below.
-            ('mailinglist', 'team')
+            ('mailinglist', 'team'),
             ])
 
         # Private teams may participate in more areas of Launchpad than
@@ -2034,7 +2029,7 @@
         email = IMasterObject(email)
         assert not self.is_team, "This method must not be used for teams."
         if not IEmailAddress.providedBy(email):
-            raise TypeError, (
+            raise TypeError(
                 "Any person's email address must provide the IEmailAddress "
                 "interface. %s doesn't." % email)
         # XXX Steve Alexander 2005-07-05:
@@ -2082,7 +2077,7 @@
             mailing_list_email = None
         all_addresses = IMasterStore(self).find(
             EmailAddress, EmailAddress.personID == self.id)
-        for address in all_addresses :
+        for address in all_addresses:
             if address not in (email, mailing_list_email):
                 address.destroySelf()
 
@@ -2114,7 +2109,7 @@
         this person.
         """
         if not IEmailAddress.providedBy(email):
-            raise TypeError, (
+            raise TypeError(
                 "Any person's email address must provide the IEmailAddress "
                 "interface. %s doesn't." % email)
         assert email.personID == self.id
@@ -2220,12 +2215,12 @@
             by this person.
 
         :param ppa_only: controls if we are interested only in source
-            package releases targeted to any PPAs or, if False, sources targeted
-            to primary archives.
+            package releases targeted to any PPAs or, if False, sources
+            targeted to primary archives.
 
-        Active 'ppa_only' flag is usually associated with active 'uploader_only'
-        because there shouldn't be any sense of maintainership for packages
-        uploaded to PPAs by someone else than the user himself.
+        Active 'ppa_only' flag is usually associated with active
+        'uploader_only' because there shouldn't be any sense of maintainership
+        for packages uploaded to PPAs by someone else than the user himself.
         """
         clauses = ['sourcepackagerelease.upload_archive = archive.id']
 
@@ -2709,7 +2704,7 @@
             private_query = None
 
         base_query = SQL("Person.visibility = ?",
-                         (PersonVisibility.PUBLIC.value,),
+                         (PersonVisibility.PUBLIC.value, ),
                          tables=['Person'])
 
         if private_query is None:
@@ -2730,8 +2725,7 @@
             Not(Person.teamowner == None),
             Person.merged == None,
             EmailAddress.person == Person.id,
-            StartsWith(Lower(EmailAddress.email), text)
-            )
+            StartsWith(Lower(EmailAddress.email), text))
         return team_email_query
 
     def _teamNameQuery(self, text):
@@ -2744,8 +2738,7 @@
             TeamParticipation.team == Person.id,
             Not(Person.teamowner == None),
             Person.merged == None,
-            SQL("Person.fti @@ ftq(?)", (text,))
-            )
+            SQL("Person.fti @@ ftq(?)", (text, )))
         return team_name_query
 
     def find(self, text=""):
@@ -2767,8 +2760,7 @@
             EmailAddress.person == Person.id,
             Person.account == Account.id,
             Not(In(Account.status, inactive_statuses)),
-            StartsWith(Lower(EmailAddress.email), text)
-            )
+            StartsWith(Lower(EmailAddress.email), text))
 
         store = IStore(Person)
 
@@ -2811,8 +2803,7 @@
             status.value for status in INACTIVE_ACCOUNT_STATUSES)
         base_query = And(
             Person.teamowner == None,
-            Person.merged == None
-            )
+            Person.merged == None)
 
         clause_tables = []
 
@@ -2821,25 +2812,21 @@
             base_query = And(
                 base_query,
                 Person.account == Account.id,
-                Not(In(Account.status, inactive_statuses))
-                )
+                Not(In(Account.status, inactive_statuses)))
         email_clause_tables = clause_tables + ['EmailAddress']
         if must_have_email:
             clause_tables = email_clause_tables
             base_query = And(
                 base_query,
-                EmailAddress.person == Person.id
-                )
+                EmailAddress.person == Person.id)
         if created_after is not None:
             base_query = And(
                 base_query,
-                Person.datecreated > created_after
-                )
+                Person.datecreated > created_after)
         if created_before is not None:
             base_query = And(
                 base_query,
-                Person.datecreated < created_before
-                )
+                Person.datecreated < created_before)
 
         # Short circuit for returning all users in order
         if not text:
@@ -2852,13 +2839,11 @@
         email_query = And(
             base_query,
             EmailAddress.person == Person.id,
-            StartsWith(Lower(EmailAddress.email), text)
-            )
+            StartsWith(Lower(EmailAddress.email), text))
 
         name_query = And(
             base_query,
-            SQL("Person.fti @@ ftq(?)", (text,))
-            )
+            SQL("Person.fti @@ ftq(?)", (text, )))
         email_results = store.find(Person, email_query).order_by()
         name_results = store.find(Person, name_query).order_by()
         combined_results = email_results.union(name_results)
@@ -3453,8 +3438,7 @@
                 if updact != 'c':
                     raise RuntimeError(
                         '%s.%s reference to %s.%s must be ON UPDATE CASCADE'
-                        % (src_tab, src_col, ref_tab, ref_col)
-                        )
+                        % (src_tab, src_col, ref_tab, ref_col))
 
         # These rows are in a UNIQUE index, and we can only move them
         # to the new Person if there is not already an entry. eg. if
@@ -3475,16 +3459,16 @@
         cur.execute(
             'UPDATE GPGKey SET owner=%(to_id)d WHERE owner=%(from_id)d'
             % vars())
-        skip.append(('gpgkey','owner'))
+        skip.append(('gpgkey', 'owner'))
 
         # Update the Branches that will not conflict, and fudge the names of
         # ones that *do* conflict.
         self._mergeBranches(cur, from_id, to_id)
-        skip.append(('branch','owner'))
+        skip.append(('branch', 'owner'))
 
         # XXX MichaelHudson 2010-01-13: Write _mergeSourcePackageRecipes!
         #self._mergeSourcePackageRecipes(cur, from_id, to_id))
-        skip.append(('sourcepackagerecipe','owner'))
+        skip.append(('sourcepackagerecipe', 'owner'))
 
         self._mergeMailingListSubscriptions(cur, from_id, to_id)
         skip.append(('mailinglistsubscription', 'person'))
@@ -3565,17 +3549,14 @@
                 raise NotImplementedError(
                         '%s.%s reference to %s.%s is in a UNIQUE index '
                         'but has not been handled' % (
-                            src_tab, src_col, ref_tab, ref_col
-                            )
-                        )
+                            src_tab, src_col, ref_tab, ref_col))
 
         # Handle all simple cases
         for src_tab, src_col, ref_tab, ref_col, updact, delact in references:
             if (src_tab, src_col) in skip:
                 continue
             cur.execute('UPDATE %s SET %s=%d WHERE %s=%d' % (
-                src_tab, src_col, to_person.id, src_col, from_person.id
-                ))
+                src_tab, src_col, to_person.id, src_col, from_person.id))
 
         self._mergeTeamMembership(cur, from_id, to_id)
 
@@ -3766,7 +3747,29 @@
 class SSHKeySet:
     implements(ISSHKeySet)
 
-    def new(self, person, keytype, keytext, comment):
+    def new(self, person, sshkey):
+        try:
+            kind, keytext, comment = sshkey.split(' ', 2)
+        except ValueError:
+            raise SSHKeyAdditionError
+
+        if not (kind and keytext and comment):
+            raise SSHKeyAdditionError
+
+        process = subprocess.Popen(
+            '/usr/bin/ssh-vulnkey -', shell=True, stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        (out, err) = process.communicate(sshkey.encode('utf-8'))
+        if 'compromised' in out.lower():
+            raise SSHKeyCompromisedError
+
+        if kind == 'ssh-rsa':
+            keytype = SSHKeyType.RSA
+        elif kind == 'ssh-dss':
+            keytype = SSHKeyType.DSA
+        else:
+            raise SSHKeyAdditionError
+
         return SSHKey(person=person, keytype=keytype, keytext=keytext,
                       comment=comment)
 
@@ -3899,6 +3902,7 @@
     domain_parts = domain.split(".")
 
     person_set = PersonSet()
+
     def _valid_nick(nick):
         if not valid_name(nick):
             return False
@@ -3959,8 +3963,7 @@
         raise NicknameGenerationError(
             "No nickname could be generated. "
             "This should be impossible to trigger unless some twonk has "
-            "registered a match everything regexp in the black list."
-            )
+            "registered a match everything regexp in the black list.")
 
     finally:
         random.setstate(random_state)

=== removed file 'lib/lp/registry/stories/person/xx-person-packages.txt'
--- lib/lp/registry/stories/person/xx-person-packages.txt	2009-09-18 15:24:30 +0000
+++ lib/lp/registry/stories/person/xx-person-packages.txt	1970-01-01 00:00:00 +0000
@@ -1,16 +0,0 @@
-==========================
-Package Maintenance Report
-==========================
-
-From the main person page, the user's Package Maintenance Report can be
-accessed by clicking on the 'Related Software' menu item.
-
-    >>> anon_browser.open('http://launchpad.dev/~mark')
-    >>> anon_browser.getLink('Related software').click()
-
-    >>> print anon_browser.title
-    Software related to Mark Shuttleworth...
-    >>> print anon_browser.url
-    http://launchpad.dev/~mark/+related-software
-
-Please see pagetests/soyuz/xx-person-packages.txt for details.

=== modified file 'lib/lp/registry/stories/person/xx-person-projects.txt'
--- lib/lp/registry/stories/person/xx-person-projects.txt	2009-09-18 15:24:30 +0000
+++ lib/lp/registry/stories/person/xx-person-projects.txt	2010-07-16 10:17:44 +0000
@@ -1,4 +1,5 @@
-== List of owned or driven projects ==
+List of owned or driven projects
+================================
 
 A Team home page displays a list of projects owned or driven by that
 team.
@@ -16,8 +17,8 @@
 unimplemented specs and open questions.
 
     >>> anon_browser.getLink('Show related projects').click()
-    >>> anon_browser.title
-    'Software related to Ubuntu Team...
+    >>> print anon_browser.title
+    Related software : ...Ubuntu Team... team
 
     >>> related_projects = find_tag_by_id(
     ...     anon_browser.contents, 'related-projects')
@@ -44,7 +45,7 @@
     >>> print anon_browser.url
     http://launchpad.dev/~mark/+related-software
     >>> print anon_browser.title
-    Software related to Mark Shuttleworth...
+    Related software : Mark Shuttleworth
 
 In the case of a person that owns/drives more than
 config.launchpad.default_batch_size, a message is displayed and the
@@ -55,14 +56,14 @@
 
     >>> print extract_text(
     ...     find_tag_by_id(anon_browser.contents, 'limit-encountered'))
-    Displaying first 5 projects out of 10 total
+    Displaying first 5 projects out of 7 total
 
     >>> related_projects = find_tag_by_id(
     ...     anon_browser.contents, 'related-projects')
     >>> print extract_text(related_projects)
     Name                            Bugs    Blueprints  Questions
-    Ubuntu Linux                    4       1           8
-    Redhat Advanced Server          0       0           0
     Debian GNU/Linux                3       0           0
     The Gentoo Linux                0       0           0
     Kubuntu - Free KDE-based Linux  0       4           0
+    Redhat Advanced Server          0       0           0
+    Apache                          1       0           0

=== modified file 'lib/lp/registry/stories/person/xx-user-to-user.txt'
--- lib/lp/registry/stories/person/xx-user-to-user.txt	2009-12-03 20:54:00 +0000
+++ lib/lp/registry/stories/person/xx-user-to-user.txt	2010-07-16 10:17:44 +0000
@@ -12,7 +12,7 @@
     >>> user_browser.open('http://launchpad.dev/~salgado')
     >>> user_browser.getLink('Contact this user').click()
     >>> print user_browser.title
-    Contact Guilherme Salgado...
+    Contact user : Guilherme Salgado
 
     >>> user_browser.getControl('Subject').value = 'Hi Salgado'
     >>> user_browser.getControl('Message').value = 'Just saying hello'
@@ -163,81 +163,3 @@
     >>> print_errors(browser_4.contents)
     Your message was not sent because you have exceeded your daily quota of 3
     messages to contact users. Try again in ... hours.
-
-
-Your own profile page
-=====================
-
-For consistency and testing purposes, the "contact" page is available even
-when someone is looking at their own profile page.  The wording on the profile
-page is different though.
-
-    >>> user_browser.open('http://launchpad.dev/~no-priv')
-    >>> user_browser.getLink('Contact this user').click()
-    >>> print user_browser.title
-    Contact No Privileges Person...
-
-This holds true even for Sample Person, who is hiding her email addresses.
-
-    >>> user_browser.open('http://launchpad.dev/~name12')
-    >>> user_browser.getLink('Contact this user').click()
-    >>> print user_browser.title
-    Contact Sample Person...
-
-    >>> name12_browser = setupBrowser('Basic test@xxxxxxxxxxxxx:test')
-    >>> name12_browser.open('http://launchpad.dev/~name12')
-    >>> name12_browser.getLink('Contact this user').click()
-    >>> print name12_browser.title
-    Contact Sample Person...
-
-
-Teams
-=====
-
-Teams can also be contacted directly by team members, regardless of whether
-the team has set a contact address or uses a Launchpad mailing list.
-
-Guadamen have no contact address, so contacting them contacts all users
-directly.
-
-    >>> admin_browser.open('http://launchpad.dev/~guadamen')
-    >>> admin_browser.getLink('Contact this team').click()
-    >>> admin_browser.title
-    'Contact GuadaMen...
-
-Foo Bar registers an explicit contact address for Guadamen...
-
-    >>> admin_browser.open('http://launchpad.dev/~guadamen/+contactaddress')
-    >>> admin_browser.getControl('Another e-mail address').selected = True
-    >>> admin_browser.getControl(
-    ...     name='field.contact_address').value = 'foo@xxxxxxxxxxx'
-    >>> admin_browser.getControl('Change').click()
-
-    >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
-
-    # Extract the link (from the email we just sent) the user will have to
-    # use to finish the registration process.
-    >>> from canonical.launchpad.ftests.logintoken import (
-    ...     get_token_url_from_email)
-    >>> token_url = get_token_url_from_email(raw_msg)
-    >>> admin_browser.open(token_url)
-    >>> admin_browser.getControl('Continue').click()
-
-...which can also be contacted directly.
-
-    >>> admin_browser.open('http://launchpad.dev/~guadamen')
-    >>> admin_browser.getLink('Contact this team').click()
-    >>> admin_browser.title
-    'Contact GuadaMen...
-
-Foo Bar later registers a Launchpad mailing list for Guadamen...
-
-    >>> admin_browser.open('http://launchpad.dev/~guadamen/+mailinglist')
-    >>> admin_browser.getControl('Apply for Mailing List').click()
-
-...which too can be contacted directly.
-
-    >>> admin_browser.open('http://launchpad.dev/~guadamen')
-    >>> admin_browser.getLink('Contact this team').click()
-    >>> admin_browser.title
-    'Contact GuadaMen...

=== modified file 'lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt'
--- lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt	2010-07-08 12:32:59 +0000
+++ lib/lp/registry/stories/vouchers/xx-voucher-redemption.txt	2010-07-16 10:17:44 +0000
@@ -1,4 +1,5 @@
-= Voucher Redemption =
+Voucher Redemption
+==================
 
 For a project to use Launchpad it must either be released under an
 approved open source license or the project administrators must buy a
@@ -11,7 +12,8 @@
 is related to a person.
 
 
-== Accessing the voucher redemption page ==
+Accessing the voucher redemption page
+-------------------------------------
 
 Mark is an administrator for at least one project that does not have a
 valid open source license so he is displayed the voucher redemption
@@ -32,8 +34,8 @@
     >>> main = find_main_content(browser.contents)
     >>> print extract_text(main)
     Redeem Vouchers for Commercial Subscriptions...
-    Marilize Coetzee does not own any commercial projects. Only project owners can redeem
-    vouchers for a project.
+    Marilize Coetzee does not own any commercial projects. Only project
+    owners can redeem vouchers for a project.
 
 A user can access her voucher page but not someone else's.  Here
 Marilize tries to access '+vouchers' on another user and is not
@@ -65,7 +67,8 @@
     Here are the steps to obtain a commercial subscription:...
 
 
-== Redeeming a voucher ==
+Redeeming a voucher
+------------------
 
 Selecting a project the user owns and a valid voucher result in a
 successful voucher redemption.
@@ -125,7 +128,7 @@
     ...     'http://launchpad.dev/~commercial-member/+related-projects')
     >>> main = find_main_content(browser.contents)
     >>> print extract_text(main)
-    Projects related to Commercial Member
+    Related projects
     ...
     Commercial Member doesn't own or drive any projects.
 
@@ -142,7 +145,8 @@
     Voucher redeemed successfully
 
 
-== OOPS handling ==
+OOPS handling
+-------------
 
 If an error occurs in the proxy while trying to redeem a voucher an
 OOPS is recorded but an error is not raised.  The user is shown an
@@ -189,7 +193,9 @@
 
     >>> SalesforceXMLRPCTestTransport.forced_fault = None
 
-== Canceling the request ==
+
+Canceling the request
+---------------------
 
 If the 'Cancel' button is selected the person's overview page is shown.
 

=== modified file 'lib/lp/registry/templates/person-related-software.pt'
--- lib/lp/registry/templates/person-related-software.pt	2010-06-24 20:07:30 +0000
+++ lib/lp/registry/templates/person-related-software.pt	2010-07-16 10:17:44 +0000
@@ -22,7 +22,7 @@
   <div id="packages">
 
   <tal:maintained-packages
-    define="sourcepackagereleases view/get_latest_maintained_packages_with_stats"
+    define="sourcepackagereleases view/latest_maintained_packages_with_stats"
     condition="sourcepackagereleases">
 
   <div class="top-portlet">
@@ -49,7 +49,7 @@
   </tal:maintained-packages>
 
   <tal:uploaded-packages
-    define="sourcepackagereleases view/get_latest_uploaded_but_not_maintained_packages_with_stats"
+    define="sourcepackagereleases view/latest_uploaded_but_not_maintained_packages_with_stats"
     condition="sourcepackagereleases">
 
   <div class="top-portlet">
@@ -75,7 +75,7 @@
   </tal:uploaded-packages>
 
   <tal:ppa-packages
-    define="sourcepackagereleases view/get_latest_uploaded_ppa_packages_with_stats"
+    define="sourcepackagereleases view/latest_uploaded_ppa_packages_with_stats"
     condition="sourcepackagereleases">
 
   <div class="top-portlet">

=== modified file 'lib/lp/registry/tests/test_distroseries.py'
--- lib/lp/registry/tests/test_distroseries.py	2010-05-13 19:24:12 +0000
+++ lib/lp/registry/tests/test_distroseries.py	2010-07-16 10:17:44 +0000
@@ -233,7 +233,7 @@
         sourcepackagename = self.factory.makeSourcePackageName(name)
         self.factory.makeSourcePackagePublishingHistory(
             sourcepackagename=sourcepackagename, distroseries=self.series,
-            component=component, section=section)
+            component=component, section_name=section)
         source_package = self.factory.makeSourcePackage(
             sourcepackagename=sourcepackagename, distroseries=self.series)
         if heat is not None:

=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py	2010-06-30 15:58:24 +0000
+++ lib/lp/soyuz/browser/archive.py	2010-07-16 10:17:44 +0000
@@ -201,12 +201,8 @@
 
         # The ID is not enough on its own to identify the publication,
         # we need to make sure it matches the context archive as well.
-        results = getUtility(IPublishingSet).getByIdAndArchive(
+        return getUtility(IPublishingSet).getByIdAndArchive(
             pub_id, self.context, source)
-        if results.count() == 1:
-            return results[0]
-
-        return None
 
     @stepthrough('+binaryhits')
     def traverse_binaryhits(self, name_str):

=== modified file 'lib/lp/soyuz/model/publishing.py'
--- lib/lp/soyuz/model/publishing.py	2010-06-22 16:08:05 +0000
+++ lib/lp/soyuz/model/publishing.py	2010-07-16 10:17:44 +0000
@@ -1266,7 +1266,7 @@
         return Store.of(archive).find(
             baseclass,
             baseclass.id == id,
-            baseclass.archive == archive.id)
+            baseclass.archive == archive.id).one()
 
     def _extractIDs(self, one_or_more_source_publications):
         """Return a list of database IDs for the given list or single object.

=== modified file 'lib/lp/soyuz/stories/soyuz/xx-person-packages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-person-packages.txt	2010-06-24 20:07:30 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-person-packages.txt	2010-07-16 10:17:44 +0000
@@ -1,4 +1,5 @@
-= Person Packages =
+Person Packages
+===============
 
 All packages maintained or uploaded by a given person can be seen on
 that person's +related-software page, which is linked to from the
@@ -6,7 +7,7 @@
 
     >>> browser.open("http://launchpad.dev/~mark/+related-software";)
     >>> print browser.title
-    Software related to Mark Shuttleworth...
+    Related software : Mark Shuttleworth
 
 This page is just a summary of the user's packages and will only
 display up to the most recent 30 items in each category.  However, it
@@ -67,7 +68,7 @@
     >>> browser.open("http://launchpad.dev/~name16/+related-software";)
     >>> link = browser.getLink(url="/ubuntu/hoary/+source/cnews")
     >>> print link
-    <Link text='Ubuntu Hoary' url='http://launchpad.dev/ubuntu/hoary/+source/cnews'>
+    <Link text='Ubuntu Hoary' ...>
     >>> link.click()
     >>> browser.title
     '...cnews... package : Hoary (5.04) : Ubuntu'
@@ -78,13 +79,14 @@
     >>> browser.open("http://launchpad.dev/~name16/+related-software";)
     >>> link = browser.getLink(url="/ubuntu/+source/cnews/cr.g7-37")
     >>> print link
-    <Link text='cr.g7-37' url='http://launchpad.dev/ubuntu/+source/cnews/cr.g7-37'>
+    <Link ... url='http://launchpad.dev/ubuntu/+source/cnews/cr.g7-37'>
     >>> link.click()
     >>> browser.title
     'cr.g7-37 : \xe2\x80\x9ccnews\xe2\x80\x9d package : Ubuntu'
 
 
-== Batched listing pages ==
+Batched listing pages
+---------------------
 
 Following the navigation link to "Maintained packages" takes the user
 to the page that lists maintained packages in batches.
@@ -120,8 +122,8 @@
     >>> print extract_text(find_tag_by_id(browser.contents, 'packages'))
     1...5 of 6 results
     ...
-    Name    Uploaded to          Version When        Failures   Bugs  Questions
-    foobar  Ubuntu Breezy-autotest  1.0  2006-12-01  i386       0     0
+    Name    Uploaded to          Version When        Failures  Bugs Questions
+    foobar  Ubuntu Breezy-autotest  1.0  2006-12-01  i386      0    0
     ...
 
 The navigation link to "PPA packages" takes the user to the
@@ -134,19 +136,9 @@
     Name      Uploaded to           Version  When        Failures
     iceweasel PPA for Mark...Warty  1.0      2006-04-11  None
 
-And finally the Related projects navigation link takes the user to the
-page that lists related projects in batches.
-
-    >>> browser.getLink("Related projects").click()
-    >>> print extract_text(find_tag_by_id(browser.contents, 'projects'))
-    1...5 of 5 results
-    ...
-    Name           Bugs  Blueprints  Questions
-    Ubuntu Linux   4     1           8
-    ...
-
-
-== Private PPA packages ==
+
+Private PPA packages
+--------------------
 
 Packages listed in the PPA section of this page are filtered so that
 if the user is not allowed to see a private package they are not present
@@ -215,7 +207,9 @@
 
     >>> user_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
 
-=== Cprov's +related-software page ===
+
+Cprov's +related-software page
+------------------------------
 
 For unprivileged users, cprov's displayed PPA packages only display
 the one in his own public PPA because source2 is only published
@@ -351,7 +345,8 @@
         ...ago None - -
 
 
-== Packages deleted from a PPA ==
+Packages deleted from a PPA
+---------------------------
 
 When a package is deleted from a PPA, in contrast to the archive index
 it will continue to appear in the related-software packages list.  This

=== modified file 'lib/lp/soyuz/tests/test_publishing.py'
--- lib/lp/soyuz/tests/test_publishing.py	2010-05-05 14:50:42 +0000
+++ lib/lp/soyuz/tests/test_publishing.py	2010-07-16 10:17:44 +0000
@@ -19,7 +19,8 @@
 from canonical.database.constants import UTC_NOW
 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
 from canonical.launchpad.webapp.interfaces import NotFoundError
-from canonical.testing import LaunchpadZopelessLayer
+from canonical.testing import (
+    DatabaseFunctionalLayer, LaunchpadZopelessLayer)
 from lp.archivepublisher.config import Config
 from lp.archivepublisher.diskpool import DiskPool
 from lp.buildmaster.interfaces.buildbase import BuildStatus
@@ -38,7 +39,7 @@
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.section import ISectionSet
 from lp.soyuz.interfaces.publishing import (
-    PackagePublishingPriority, PackagePublishingStatus)
+    IPublishingSet, PackagePublishingPriority, PackagePublishingStatus)
 from lp.soyuz.interfaces.queue import PackageUploadStatus
 from canonical.launchpad.scripts import FakeLogger
 from lp.testing import TestCaseWithFactory
@@ -975,5 +976,61 @@
         self.assertEquals(self.sparc_distroarch, builds[1].distro_arch_series)
 
 
+class PublishingSetTests(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(PublishingSetTests, self).setUp()
+        self.distroseries = self.factory.makeDistroSeries()
+        self.archive = self.factory.makeArchive(
+            distribution=self.distroseries.distribution)
+        self.publishing = self.factory.makeSourcePackagePublishingHistory(
+            distroseries=self.distroseries, archive=self.archive)
+        self.publishing_set = getUtility(IPublishingSet)
+
+    def test_getByIdAndArchive_finds_record(self):
+        record = self.publishing_set.getByIdAndArchive(
+            self.publishing.id, self.archive)
+        self.assertEqual(self.publishing, record)
+
+    def test_getByIdAndArchive_finds_record_explicit_source(self):
+        record = self.publishing_set.getByIdAndArchive(
+            self.publishing.id, self.archive, source=True)
+        self.assertEqual(self.publishing, record)
+
+    def test_getByIdAndArchive_wrong_archive(self):
+        wrong_archive = self.factory.makeArchive()
+        record = self.publishing_set.getByIdAndArchive(
+            self.publishing.id, wrong_archive)
+        self.assertEqual(None, record)
+
+    def makeBinaryPublishing(self):
+        distroarchseries = self.factory.makeDistroArchSeries(
+            distroseries=self.distroseries)
+        binary_publishing = self.factory.makeBinaryPackagePublishingHistory(
+            archive=self.archive, distroarchseries=distroarchseries)
+        return binary_publishing
+
+    def test_getByIdAndArchive_wrong_type(self):
+        self.makeBinaryPublishing()
+        record = self.publishing_set.getByIdAndArchive(
+            self.publishing.id, self.archive, source=False)
+        self.assertEqual(None, record)
+
+    def test_getByIdAndArchive_finds_binary(self):
+        binary_publishing = self.makeBinaryPublishing()
+        record = self.publishing_set.getByIdAndArchive(
+            binary_publishing.id, self.archive, source=False)
+        self.assertEqual(binary_publishing, record)
+
+    def test_getByIdAndArchive_binary_wrong_archive(self):
+        binary_publishing = self.makeBinaryPublishing()
+        wrong_archive = self.factory.makeArchive()
+        record = self.publishing_set.getByIdAndArchive(
+            binary_publishing.id, wrong_archive, source=False)
+        self.assertEqual(None, record)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-07-10 16:13:53 +0000
+++ lib/lp/testing/factory.py	2010-07-16 10:17:44 +0000
@@ -132,17 +132,22 @@
 from lp.services.worlddata.interfaces.country import ICountrySet
 from lp.services.worlddata.interfaces.language import ILanguageSet
 
+from lp.soyuz.adapters.packagelocation import PackageLocation
 from lp.soyuz.interfaces.archive import (
     default_name_by_purpose, IArchiveSet, ArchivePurpose)
-from lp.soyuz.adapters.packagelocation import PackageLocation
 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
+from lp.soyuz.interfaces.binarypackagerelease import BinaryPackageFormat
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.packageset import IPackagesetSet
 from lp.soyuz.interfaces.processor import IProcessorFamilySet
-from lp.soyuz.interfaces.publishing import PackagePublishingStatus
+from lp.soyuz.interfaces.publishing import (
+    PackagePublishingPriority, PackagePublishingStatus)
 from lp.soyuz.interfaces.section import ISectionSet
+from lp.soyuz.model.binarypackagename import BinaryPackageName
+from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
 from lp.soyuz.model.processor import ProcessorFamily, ProcessorFamilySet
-from lp.soyuz.model.publishing import SourcePackagePublishingHistory
+from lp.soyuz.model.publishing import (
+    BinaryPackagePublishingHistory, SourcePackagePublishingHistory)
 
 from lp.testing import run_with_login, time_counter, login, logout, temp_dir
 
@@ -2199,9 +2204,10 @@
 
     def makeSourcePackageRelease(self, archive=None, sourcepackagename=None,
                                  distroseries=None, maintainer=None,
-                                 creator=None, component=None, section=None,
-                                 urgency=None, version=None,
-                                 builddepends=None, builddependsindep=None,
+                                 creator=None, component=None,
+                                 section_name=None, urgency=None,
+                                 version=None, builddepends=None,
+                                 builddependsindep=None,
                                  build_conflicts=None,
                                  build_conflicts_indep=None,
                                  architecturehintlist='all',
@@ -2236,9 +2242,7 @@
         if urgency is None:
             urgency = self.getAnySourcePackageUrgency()
 
-        if section is None:
-            section = self.getUniqueString('section')
-        section = getUtility(ISectionSet).ensure(section)
+        section = self.makeSection(name=section_name)
 
         if maintainer is None:
             maintainer = self.makePerson()
@@ -2324,8 +2328,9 @@
     def makeSourcePackagePublishingHistory(self, sourcepackagename=None,
                                            distroseries=None, maintainer=None,
                                            creator=None, component=None,
-                                           section=None, urgency=None,
-                                           version=None, archive=None,
+                                           section_name=None,
+                                           urgency=None, version=None,
+                                           archive=None,
                                            builddepends=None,
                                            builddependsindep=None,
                                            build_conflicts=None,
@@ -2365,7 +2370,7 @@
             distroseries=distroseries,
             maintainer=maintainer,
             creator=creator, component=component,
-            section=section,
+            section_name=section_name,
             urgency=urgency,
             version=version,
             builddepends=builddepends,
@@ -2394,6 +2399,97 @@
         # of SSPPH and other useful attributes.
         return SourcePackagePublishingHistory.get(sspph.id)
 
+    def makeBinaryPackagePublishingHistory(self, binarypackagerelease=None,
+                                           distroarchseries=None,
+                                           component=None, section_name=None,
+                                           priority=None, status=None,
+                                           scheduleddeletiondate=None,
+                                           dateremoved=None,
+                                           pocket=None, archive=None):
+        """Make a `BinaryPackagePublishingHistory`."""
+        if distroarchseries is None:
+            if archive is None:
+                distribution = None
+            else:
+                distribution = archive.distribution
+            distroseries = self.makeDistroSeries(distribution=distribution)
+            distroarchseries = self.makeDistroArchSeries(
+                distroseries=distroseries)
+
+        if archive is None:
+            archive = self.makeArchive(
+                distribution=distroarchseries.distroseries.distribution,
+                purpose=ArchivePurpose.PRIMARY)
+
+        if pocket is None:
+            pocket = self.getAnyPocket()
+
+        if status is None:
+            status = PackagePublishingStatus.PENDING
+
+        if priority is None:
+            priority = PackagePublishingPriority.OPTIONAL
+
+        bpr = self.makeBinaryPackageRelease(
+            component=component,
+            section_name=section_name,
+            priority=priority)
+
+        return BinaryPackagePublishingHistory(
+            distroarchseries=distroarchseries,
+            binarypackagerelease=bpr,
+            component=bpr.component,
+            section=bpr.section,
+            status=status,
+            dateremoved=dateremoved,
+            scheduleddeletiondate=scheduleddeletiondate,
+            pocket=pocket,
+            priority=priority,
+            archive=archive)
+
+    def makeBinaryPackageName(self, name=None):
+        if name is None:
+            name = self.getUniqueString("binarypackage")
+        return BinaryPackageName(name=name)
+
+    def makeBinaryPackageRelease(self, binarypackagename=None,
+                                 version=None, build=None,
+                                 binpackageformat=None, component=None,
+                                 section_name=None, priority=None,
+                                 architecturespecific=False,
+                                 summary=None, description=None):
+        """Make a `BinaryPackageRelease`."""
+        if binarypackagename is None:
+            binarypackagename = self.makeBinaryPackageName()
+        if version is None:
+            version = self.getUniqueString("version")
+        if build is None:
+            build = self.makeBinaryPackageBuild()
+        if binpackageformat is None:
+            binpackageformat = BinaryPackageFormat.DEB
+        if component is None:
+            component = self.makeComponent()
+        section = self.makeSection(name=section_name)
+        if priority is None:
+            priority = PackagePublishingPriority.OPTIONAL
+        if summary is None:
+            summary = self.getUniqueString("summary")
+        if description is None:
+            description = self.getUniqueString("description")
+        return BinaryPackageRelease(binarypackagename=binarypackagename,
+                                    version=version, build=build,
+                                    binpackageformat=binpackageformat,
+                                    component=component, section=section,
+                                    priority=priority, summary=summary,
+                                    description=description,
+                                    architecturespecific=architecturespecific)
+
+    def makeSection(self, name=None):
+        """Make a `Section`."""
+        if name is None:
+            name = self.getUniqueString('section')
+        return getUtility(ISectionSet).ensure(name)
+
     def makePackageset(self, name=None, description=None, owner=None,
                        packages=(), distroseries=None):
         """Make an `IPackageset`."""
@@ -2597,13 +2693,13 @@
         return getUtility(IHWSubmissionDeviceSet).create(
             device_driver_link, submission, parent, hal_device_id)
 
-    def makeSSHKey(self, person=None, keytype=SSHKeyType.RSA):
+    def makeSSHKey(self, person=None):
         """Create a new SSHKey."""
         if person is None:
             person = self.makePerson()
-        return getUtility(ISSHKeySet).new(
-            person=person, keytype=keytype, keytext=self.getUniqueString(),
-            comment=self.getUniqueString())
+        public_key = "ssh-rsa %s %s" % (
+            self.getUniqueString(), self.getUniqueString())
+        return getUtility(ISSHKeySet).new(person, public_key)
 
     def makeBlob(self, blob=None, expires=None):
         """Create a new TemporaryFileStorage BLOB."""

=== modified file 'standard_test_template.py'
--- standard_test_template.py	2010-01-12 03:07:09 +0000
+++ standard_test_template.py	2010-07-16 10:17:44 +0000
@@ -5,10 +5,20 @@
 
 __metaclass__ = type
 
-import unittest
-
+from canonical.testing import DatabaseFunctionalLayer
 from lp.testing import TestCase
 
 
-def test_suite():
-    return unittest.TestLoader().loadTestsFromName(__name__)
+class TestSomething(TestCase):
+    # XXX: Sample test class.  Replace with your own test class(es).
+
+    # XXX: Optional layer--see lib/canonical/testing/layers.py
+    # Get the simplest layer that your test will work on, or if you
+    # don't even use the database, don't set it at all.
+    layer = DatabaseFunctionalLayer
+
+    # XXX: Sample test.  Replace with your own test methods.
+    def test_baseline(self):
+
+        # XXX: Assertions take expected value first, actual value second.
+        self.assertEqual(4, 2 + 2)


Follow ups