Merge lp:~wallyworld/launchpad/tales-linkify-broken-links into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 11564
Proposed branch: lp:~wallyworld/launchpad/tales-linkify-broken-links
Merge into: lp:launchpad
Diff against target: 376 lines (+218/-39)
4 files modified
.bzrignore (+2/-0)
lib/canonical/launchpad/browser/launchpad.py (+30/-10)
lib/canonical/launchpad/browser/tests/test_launchpad.py (+184/-27)
lib/lp/testing/factory.py (+2/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/tales-linkify-broken-links
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Review via email: mp+35268@code.launchpad.net

Commit message

Fix the +branch/xxxx parsing functionality to traverse to a product/series/distropackage if the linked branch does not exist (or is private). Display a better error message is the URL is totally broken.

Description of the change

Bug 404131 describes some issues with the linkification functionality of tales.FormattersAPI. If a branch URL refers to a product where the development focus is a private branch, a 404 error occurs. Similarly, if no development focus has been set, an error occurs.

Proposed fix

The redirect_branch() method in lib/canonical/launchpad/browser/launchpad.py tries to resolve the URL as a branch and simply catches a NotFound exception when the URL is invalid (ether non existsent branch or totally bogus URL). The behaviour of this method can be enhanced to:

1. Catch the NoLinkedBranch exception if a branch cannot be resolved and attempt to find an ICanHasLinkedBranch target instead
2. Add a page notification to let the user know the target has no linked branch and stay on the current page if an ICanHasLinkedBranch target is resolved instead of a branch
3. Add a page error and stay on the current page if the URL is totally invalid instead of going to a 404 page

Implementation details

For cases where the URL is invalid, the HTTP referer is used as the redirection target.

The only code changes were to:
    - lib/canonical/launchpad/browser/launchpad.py
    - lib/canonical/launchpad/browser/tests/test_launchpad.py

Tests

    bin/test -vvm canonical.launchpad.browser.tests.test_launchpad TestBranchTraversal

    Changed tests:
    These tests have primarily been changed to reflect the removal of the NotFound exceptions
    - test_no_such_unique_name
    - test_nonexistent_product
    - test_product_without_dev_focus
    - test_nonexistent_product_series
    - test_no_branch_for_series
    - test_too_short_branch_name
    - test_invalid_product_name

    New tests:
    These tests were perhaps missing from the original set
    - test_private_branch
    - test_private_branch_for_series
    - test_private_branch_for_product
    - test_distro_package_alias
    - test_private_branch_for_distro_package

lint

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/browser/launchpad.py
  lib/canonical/launchpad/browser/tests/test_launchpad.py

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Hi Ian,

The code path firstly looks things up and then determins the url based on what
it found. Instead of remembering everything, you could just determine the
url, especially given that we only care about the trailing at one place.
In fact, I'm not even sure we want to deal with the trailing at all.

        try:
            # first check for a valid branch url
            try:
                branch, trailing = getUtility(IBranchLookup).getByLPPath(path)
                url = canonical_url(branch)
            except (NoLinkedBranch):
                # a valid ICanHasLinkedBranch target exists but there's no
                # branch or it's not visible, so get the target instead
                target = getUtility(ILinkedBranchTraverser).traverse(path)
                userMessage = (
                    "The requested branch does not exist. "
                    "You have landed at lp:%s instead." % path)

They haven't landed at lp:whatever instead. All you need to say is something
like:

    There is no branch linked for lp:whatever.

I'm beginning to wonder if we want to send them to the target at all...

                self.request.response.addNotification(userMessage)
                url = canonical_url(target, rootsite='mainsite')
        except (CannotHaveLinkedBranch, InvalidNamespace,
                InvalidProductName, NotFoundError) as e:
            self.request.response.addErrorNotification(
                    "Invalid branch lp:%s. %s" % (path, e.message))
            url = self.request.getHeader('referer')

That way you don't need the:
    target = branch = trailing = None

And skip the appending of the trailing.

I'm also a little concerned that you are getting a deprecation warning about
e.message. The docs say you should just use __str__, so...
    "Invalid branch lp:%s. %s" % (path, e))
should work fine.

Revision history for this message
Tim Penhey (thumper) wrote :

Also... I've had an thought...

How much do you like javascript?

Revision history for this message
Ian Booth (wallyworld) wrote :

Changes made to implementation:

- for private or non existent branches, do not redirect to target
product/series etc, stay on current page and display message
- improve error message for invalid links
- other minor improvements

Revision history for this message
Tim Penhey (thumper) wrote :

Getting pretty close now...

> if len(error_msg)==0:

PEP-8 says spaces either side of the operator, although I think

  if error_msg == '':

Is more obvious.

The _validateNotificationContext has a extra blank lines it doesn't need.
You can set the linked branch when you are making a series with the factory
using branch=...

Setting the development focus should be:
  ICanHasLinkedBranch(product).setBranch(...)

review: Needs Fixing
Revision history for this message
Ian Booth (wallyworld) wrote :

>
> The _validateNotificationContext has a extra blank lines it doesn't need.

Sometimes I like to leave blank lines to aid readability, although the
following one should go:

+ self.assertEqual(len(notifications), 1)
+ self.assertEquals(notifications[0].level, level)
+
+ self.assertEqual(notification, notifications[0].message)

I can remove the others as well.

> You can set the linked branch when you are making a series with the factory
> using branch=...
>

I don't know about that, unless I am missing something :-)

lp.testing.factory.py:
    def makeProductSeries(self, product=None, name=None, owner=None,
                          summary=None, date_created=None):
        """Create and return a new ProductSeries."""

There's no named parameter for branch. Hence it complains if one tries
to use branch=.

Revision history for this message
Tim Penhey (thumper) wrote :

On Wed, 15 Sep 2010 17:25:59 you wrote:
> lp.testing.factory.py:
> def makeProductSeries(self, product=None, name=None, owner=None,
> summary=None, date_created=None):
> """Create and return a new ProductSeries."""
>
> There's no named parameter for branch. Hence it complains if one tries
> to use branch=.

Found it. It is called makeSeries. Damn. We now have two factory methods to
do the same thing. Can you please consolidate?

Revision history for this message
Ian Booth (wallyworld) wrote :

A usage analysis shows:

215 usages of makeProductSeries
38 usages of makeSeries

There's too many changes to do a simple drive by. I think we should land
this branch as is and then do the consolidation in another branch.

Ok?

On 15/09/10 18:01, Tim Penhey wrote:
> On Wed, 15 Sep 2010 17:25:59 you wrote:
>> lp.testing.factory.py:
>> def makeProductSeries(self, product=None, name=None, owner=None,
>> summary=None, date_created=None):
>> """Create and return a new ProductSeries."""
>>
>> There's no named parameter for branch. Hence it complains if one tries
>> to use branch=.
>
> Found it. It is called makeSeries. Damn. We now have two factory methods to
> do the same thing. Can you please consolidate?

Revision history for this message
Ian Booth (wallyworld) wrote :

Initial work to add branch as a named parameter to
factory.makeProductSeries() is done. This allows about 4 or 5 lines of
code to be deleted from browser.test_launchpad.py

On 15/09/10 18:01, Tim Penhey wrote:
> On Wed, 15 Sep 2010 17:25:59 you wrote:
>> lp.testing.factory.py:
>> def makeProductSeries(self, product=None, name=None, owner=None,
>> summary=None, date_created=None):
>> """Create and return a new ProductSeries."""
>>
>> There's no named parameter for branch. Hence it complains if one tries
>> to use branch=.
>
> Found it. It is called makeSeries. Damn. We now have two factory methods to
> do the same thing. Can you please consolidate?

Revision history for this message
Ian Booth (wallyworld) wrote :

Made a small change to ensure unproxied variables were called naked_xxx

Revision history for this message
Tim Penhey (thumper) wrote :

Last thing:

   target = ICanHasLinkedBranch(distro_package)
   run_with_login(
       registrant,
       target.setBranch, branch, registrant)

IMO reads more easily as:

    with person_logged_in(registrant):
        ICanHasLinkedBranch(distro_package).setBranch(branch, registrant)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2010-09-05 20:29:40 +0000
+++ .bzrignore 2010-09-16 06:06:06 +0000
@@ -78,3 +78,5 @@
78lib/canonical/buildd/debian/*78lib/canonical/buildd/debian/*
79lib/canonical/buildd/launchpad-files/*79lib/canonical/buildd/launchpad-files/*
80*.pt.py80*.pt.py
81.project
82.pydevproject
8183
=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
--- lib/canonical/launchpad/browser/launchpad.py 2010-08-24 10:45:57 +0000
+++ lib/canonical/launchpad/browser/launchpad.py 2010-09-16 06:06:06 +0000
@@ -514,19 +514,39 @@
514514
515 'foo' can be the unique name of the branch, or any of the aliases for515 'foo' can be the unique name of the branch, or any of the aliases for
516 the branch.516 the branch.
517 If 'foo' resolves to an ICanHasLinkedBranch instance but the linked
518 branch is not yet set, redirect back to the referring page with a
519 suitable notification message.
520 If 'foo' is completely invalid, redirect back to the referring page
521 with a suitable error message.
517 """522 """
523
524 # The default url to go to will be back to the referring page (in
525 # the case that there is an error resolving the branch url)
526 url = self.request.getHeader('referer')
518 path = '/'.join(self.request.stepstogo)527 path = '/'.join(self.request.stepstogo)
519 try:528 try:
520 branch_data = getUtility(IBranchLookup).getByLPPath(path)529 # first check for a valid branch url
521 except (CannotHaveLinkedBranch, NoLinkedBranch, InvalidNamespace,530 try:
522 InvalidProductName):531 branch_data = getUtility(IBranchLookup).getByLPPath(path)
523 raise NotFoundError532 branch, trailing = branch_data
524 branch, trailing = branch_data533 url = canonical_url(branch)
525 if branch is None:534 if trailing is not None:
526 raise NotFoundError535 url = urlappend(url, trailing)
527 url = canonical_url(branch)536
528 if trailing is not None:537 except (NoLinkedBranch):
529 url = urlappend(url, trailing)538 # a valid ICanHasLinkedBranch target exists but there's no
539 # branch or it's not visible
540 self.request.response.addNotification(
541 "The target %s does not have a linked branch." % path)
542
543 except (CannotHaveLinkedBranch, InvalidNamespace,
544 InvalidProductName, NotFoundError) as e:
545 error_msg = str(e)
546 if error_msg == '':
547 error_msg = "Invalid branch lp:%s." % path
548 self.request.response.addErrorNotification(error_msg)
549
530 return self.redirectSubTree(url)550 return self.redirectSubTree(url)
531551
532 @stepto('+builds')552 @stepto('+builds')
533553
=== modified file 'lib/canonical/launchpad/browser/tests/test_launchpad.py'
--- lib/canonical/launchpad/browser/tests/test_launchpad.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/browser/tests/test_launchpad.py 2010-09-16 06:06:06 +0000
@@ -1,7 +1,7 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for traversal from the root branch object.."""4"""Tests for traversal from the root branch object."""
55
6__metaclass__ = type6__metaclass__ = type
77
@@ -13,17 +13,21 @@
1313
14from canonical.launchpad.browser.launchpad import LaunchpadRootNavigation14from canonical.launchpad.browser.launchpad import LaunchpadRootNavigation
15from canonical.launchpad.interfaces.account import AccountStatus15from canonical.launchpad.interfaces.account import AccountStatus
16from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
16from canonical.launchpad.webapp import canonical_url17from canonical.launchpad.webapp import canonical_url
18from canonical.launchpad.webapp.interfaces import BrowserNotificationLevel
17from canonical.launchpad.webapp.servers import LaunchpadTestRequest19from canonical.launchpad.webapp.servers import LaunchpadTestRequest
18from canonical.launchpad.webapp.url import urlappend20from canonical.launchpad.webapp.url import urlappend
19from canonical.testing.layers import DatabaseFunctionalLayer21from canonical.testing.layers import DatabaseFunctionalLayer
20from lp.app.errors import GoneError22from lp.app.errors import GoneError
23from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
21from lp.registry.interfaces.person import (24from lp.registry.interfaces.person import (
22 IPersonSet,25 IPersonSet,
23 PersonVisibility,26 PersonVisibility,
24 )27 )
25from lp.testing import (28from lp.testing import (
26 login_person,29 login_person,
30 person_logged_in,
27 TestCaseWithFactory,31 TestCaseWithFactory,
28 )32 )
29from lp.testing.views import create_view33from lp.testing.views import create_view
@@ -31,6 +35,48 @@
3135
32class TraversalMixin:36class TraversalMixin:
3337
38 def _validateNotificationContext(
39 self, request, notification=None,
40 level=BrowserNotificationLevel.INFO):
41 """Check the browser notifications associated with the request.
42
43 Ensure that the notification instances attached to the request match
44 the expected values for text and type.
45
46 :param notification: The exact notification text to validate. If None
47 then we don't care what the notification text is, so long as there
48 is some.
49 : param level: the required notification level
50 """
51
52 notifications = request.notifications
53 if notification is None:
54 self.assertEquals(len(notifications), 0)
55 return
56 self.assertEqual(len(notifications), 1)
57 self.assertEquals(notifications[0].level, level)
58 self.assertEqual(notification, notifications[0].message)
59
60 def assertDisplaysNotification(
61 self, path, notification=None,
62 level=BrowserNotificationLevel.INFO):
63 """Assert that an invalid path redirects back to referrer.
64
65 The request object is expected to have a notification message to
66 display to the user to explain the reason for the error.
67
68 :param path: The path to check
69 :param notification: The exact notification text to validate. If None
70 then we don't care what the notification text is, so long as there
71 is some.
72 : param level: the required notification level
73 """
74
75 redirection = self.traverse(path)
76 self.assertIs(redirection.target, None)
77 self._validateNotificationContext(
78 redirection.request, notification, level)
79
34 def assertNotFound(self, path):80 def assertNotFound(self, path):
35 self.assertRaises(NotFound, self.traverse, path)81 self.assertRaises(NotFound, self.traverse, path)
3682
@@ -58,8 +104,8 @@
58class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):104class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
59 """Branches are traversed to from IPersons. Test we can reach them.105 """Branches are traversed to from IPersons. Test we can reach them.
60106
61 This class tests the `PersonNavigation` class to see that we can traverse107 This class tests the `LaunchpadRootNavigation` class to see that we can
62 to branches from such objects.108 traverse to branches from URLs of the form +branch/xxxx.
63 """109 """
64110
65 layer = DatabaseFunctionalLayer111 layer = DatabaseFunctionalLayer
@@ -75,26 +121,107 @@
75121
76 def test_no_such_unique_name(self):122 def test_no_such_unique_name(self):
77 # Traversing to /+branch/<unique_name> where 'unique_name' is for a123 # Traversing to /+branch/<unique_name> where 'unique_name' is for a
78 # branch that doesn't exist will generate a 404.124 # branch that doesn't exist will display an error message.
79 branch = self.factory.makeAnyBranch()125 branch = self.factory.makeAnyBranch()
80 self.assertNotFound(branch.unique_name + 'wibble')126 bad_name = branch.unique_name + 'wibble'
127 requiredMessage = "No such branch: '%s'." % (branch.name+"wibble")
128 self.assertDisplaysNotification(
129 bad_name, requiredMessage,
130 BrowserNotificationLevel.ERROR)
131
132 def test_private_branch(self):
133 # If an attempt is made to access a private branch, display an error.
134 branch = self.factory.makeProductBranch()
135 branch_unique_name = branch.unique_name
136 naked_product = removeSecurityProxy(branch.product)
137 ICanHasLinkedBranch(naked_product).setBranch(branch)
138 removeSecurityProxy(branch).private = True
139
140 any_user = self.factory.makePerson()
141 login_person(any_user)
142 requiredMessage = "No such branch: '%s'." % branch_unique_name
143 self.assertDisplaysNotification(
144 branch_unique_name,
145 requiredMessage,
146 BrowserNotificationLevel.ERROR)
81147
82 def test_product_alias(self):148 def test_product_alias(self):
83 # Traversing to /+branch/<product> redirects to the page for the149 # Traversing to /+branch/<product> redirects to the page for the
84 # branch that is the development focus branch for that product.150 # branch that is the development focus branch for that product.
85 branch = self.factory.makeProductBranch()151 branch = self.factory.makeProductBranch()
86 product = removeSecurityProxy(branch.product)152 naked_product = removeSecurityProxy(branch.product)
87 product.development_focus.branch = branch153 ICanHasLinkedBranch(naked_product).setBranch(branch)
88 self.assertRedirects(product.name, canonical_url(branch))154 self.assertRedirects(naked_product.name, canonical_url(branch))
155
156 def test_private_branch_for_product(self):
157 # If the development focus of a product is private, display a
158 # message telling the user there is no linked branch.
159 branch = self.factory.makeProductBranch()
160 naked_product = removeSecurityProxy(branch.product)
161 ICanHasLinkedBranch(naked_product).setBranch(branch)
162 removeSecurityProxy(branch).private = True
163
164 any_user = self.factory.makePerson()
165 login_person(any_user)
166 requiredMessage = (u"The target %s does not have a linked branch."
167 % naked_product.name)
168 self.assertDisplaysNotification(
169 naked_product.name,
170 requiredMessage,
171 BrowserNotificationLevel.NOTICE)
89172
90 def test_nonexistent_product(self):173 def test_nonexistent_product(self):
91 # Traversing to /+branch/<no-such-product> generates a 404.174 # Traversing to /+branch/<no-such-product> displays an error message.
92 self.assertNotFound('non-existent')175 non_existent = 'non-existent'
176 requiredMessage = u"No such product: '%s'." % non_existent
177 self.assertDisplaysNotification(
178 non_existent, requiredMessage,
179 BrowserNotificationLevel.ERROR)
93180
94 def test_product_without_dev_focus(self):181 def test_product_without_dev_focus(self):
95 # Traversing to a product without a development focus generates a 404.182 # Traversing to a product without a development focus displays a
183 # user message on the same page.
96 product = self.factory.makeProduct()184 product = self.factory.makeProduct()
97 self.assertNotFound(product.name)185 requiredMessage = (u"The target %s does not have a linked branch."
186 % product.name)
187 self.assertDisplaysNotification(
188 product.name,
189 requiredMessage,
190 BrowserNotificationLevel.NOTICE)
191
192 def test_distro_package_alias(self):
193 # Traversing to /+branch/<distro>/<sourcepackage package> redirects
194 # to the page for the branch that is the development focus branch
195 # for that package.
196 sourcepackage = self.factory.makeSourcePackage()
197 branch = self.factory.makePackageBranch(sourcepackage=sourcepackage)
198 distro_package = sourcepackage.distribution_sourcepackage
199 ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
200 registrant = ubuntu_branches.teamowner
201 target = ICanHasLinkedBranch(distro_package)
202 with person_logged_in(registrant):
203 target.setBranch(branch, registrant)
204 self.assertRedirects("%s" % target.bzr_path, canonical_url(branch))
205
206 def test_private_branch_for_distro_package(self):
207 # If the development focus of a distro package is private, display a
208 # message telling the user there is no linked branch.
209 sourcepackage = self.factory.makeSourcePackage()
210 branch = self.factory.makePackageBranch(
211 sourcepackage=sourcepackage, private=True)
212 distro_package = sourcepackage.distribution_sourcepackage
213 ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
214 registrant = ubuntu_branches.teamowner
215 with person_logged_in(registrant):
216 ICanHasLinkedBranch(distro_package).setBranch(branch, registrant)
217
218 any_user = self.factory.makePerson()
219 login_person(any_user)
220 path = ICanHasLinkedBranch(distro_package).bzr_path
221 requiredMessage = (u"The target %s does not have a linked branch."
222 % path)
223 self.assertDisplaysNotification(
224 path, requiredMessage, BrowserNotificationLevel.NOTICE)
98225
99 def test_trailing_path_redirect(self):226 def test_trailing_path_redirect(self):
100 # If there are any trailing path segments after the branch identifier,227 # If there are any trailing path segments after the branch identifier,
@@ -106,33 +233,63 @@
106 def test_product_series_redirect(self):233 def test_product_series_redirect(self):
107 # Traversing to /+branch/<product>/<series> redirects to the branch234 # Traversing to /+branch/<product>/<series> redirects to the branch
108 # for that series, if there is one.235 # for that series, if there is one.
109 branch = self.factory.makeProductBranch()236 branch = self.factory.makeBranch()
110 product = branch.product237 series = self.factory.makeProductSeries(branch=branch)
111 series = self.factory.makeProductSeries(product=product)
112 removeSecurityProxy(series).branch = branch
113 self.assertRedirects(238 self.assertRedirects(
114 '%s/%s' % (product.name, series.name), canonical_url(branch))239 ICanHasLinkedBranch(series).bzr_path, canonical_url(branch))
115240
116 def test_nonexistent_product_series(self):241 def test_nonexistent_product_series(self):
117 # /+branch/<product>/<series> generates a 404 if there is no such242 # /+branch/<product>/<series> displays an error message if there is
118 # series.243 # no such series.
119 product = self.factory.makeProduct()244 product = self.factory.makeProduct()
120 self.assertNotFound('%s/nonexistent' % product.name)245 non_existent = 'nonexistent'
246 requiredMessage = u"No such product series: '%s'." % non_existent
247 self.assertDisplaysNotification(
248 '%s/%s' % (product.name, non_existent),
249 requiredMessage,
250 BrowserNotificationLevel.ERROR)
121251
122 def test_no_branch_for_series(self):252 def test_no_branch_for_series(self):
123 # If there's no branch for a product series, generate a 404.253 # If there's no branch for a product series, display a
254 # message telling the user there is no linked branch.
124 series = self.factory.makeProductSeries()255 series = self.factory.makeProductSeries()
125 self.assertNotFound('%s/%s' % (series.product.name, series.name))256 path = ICanHasLinkedBranch(series).bzr_path
257 requiredMessage = ("The target %s does not have a linked branch."
258 % path)
259 self.assertDisplaysNotification(
260 path, requiredMessage, BrowserNotificationLevel.NOTICE)
261
262 def test_private_branch_for_series(self):
263 # If the development focus of a product series is private, display a
264 # message telling the user there is no linked branch.
265 branch = self.factory.makeBranch(private=True)
266 series = self.factory.makeProductSeries(branch=branch)
267
268 any_user = self.factory.makePerson()
269 login_person(any_user)
270 path = ICanHasLinkedBranch(series).bzr_path
271 requiredMessage = (u"The target %s does not have a linked branch."
272 % path)
273 self.assertDisplaysNotification(
274 path, requiredMessage, BrowserNotificationLevel.NOTICE)
126275
127 def test_too_short_branch_name(self):276 def test_too_short_branch_name(self):
128 # 404 if the thing following +branch is a unique name that's too short277 # error notification if the thing following +branch is a unique name
129 # to be a real unique name.278 # that's too short to be a real unique name.
130 owner = self.factory.makePerson()279 owner = self.factory.makePerson()
131 self.assertNotFound('~%s' % owner.name)280 requiredMessage = (u"Cannot understand namespace name: '%s'"
281 % owner.name)
282 self.assertDisplaysNotification(
283 '~%s' % owner.name,
284 requiredMessage,
285 BrowserNotificationLevel.ERROR)
132286
133 def test_invalid_product_name(self):287 def test_invalid_product_name(self):
134 # 404 if the thing following +branch has an invalid product name.288 # error notification if the thing following +branch has an invalid
135 self.assertNotFound('a')289 # product name.
290 self.assertDisplaysNotification(
291 'a', u"Invalid name for product: a.",
292 BrowserNotificationLevel.ERROR)
136293
137294
138class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):295class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
139296
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-09-10 03:20:48 +0000
+++ lib/lp/testing/factory.py 2010-09-16 06:06:06 +0000
@@ -881,7 +881,7 @@
881 return product881 return product
882882
883 def makeProductSeries(self, product=None, name=None, owner=None,883 def makeProductSeries(self, product=None, name=None, owner=None,
884 summary=None, date_created=None):884 summary=None, date_created=None, branch=None):
885 """Create and return a new ProductSeries."""885 """Create and return a new ProductSeries."""
886 if product is None:886 if product is None:
887 product = self.makeProduct()887 product = self.makeProduct()
@@ -895,7 +895,7 @@
895 # so we remove the security proxy before creating the series.895 # so we remove the security proxy before creating the series.
896 naked_product = removeSecurityProxy(product)896 naked_product = removeSecurityProxy(product)
897 series = naked_product.newSeries(897 series = naked_product.newSeries(
898 owner=owner, name=name, summary=summary)898 owner=owner, name=name, summary=summary, branch=branch)
899 if date_created is not None:899 if date_created is not None:
900 series.datecreated = date_created900 series.datecreated = date_created
901 return ProxyFactory(series)901 return ProxyFactory(series)