← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:doctest-bad-indentation into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:doctest-bad-indentation into launchpad:master.

Commit message:
Fix bad indentation in doctests

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/406255
-- 
The attached diff has been truncated due to its size.
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:doctest-bad-indentation into launchpad:master.
diff --git a/lib/lp/app/doc/launchpadform.txt b/lib/lp/app/doc/launchpadform.txt
index 5bf686b..a42833f 100644
--- a/lib/lp/app/doc/launchpadform.txt
+++ b/lib/lp/app/doc/launchpadform.txt
@@ -47,36 +47,36 @@ The schema can be an interface implemented by your content object, or
 an interface specifically tailored for data entry.  Below is an
 example schema:
 
-  >>> from zope.interface import Interface, implementer
-  >>> from zope.schema import TextLine
+    >>> from zope.interface import Interface, implementer
+    >>> from zope.schema import TextLine
 
-  >>> class IFormTest(Interface):
-  ...     name = TextLine(title=u"Name")
-  ...     displayname = TextLine(title=u"Title")
-  ...     password = TextLine(title=u"Password")
+    >>> class IFormTest(Interface):
+    ...     name = TextLine(title=u"Name")
+    ...     displayname = TextLine(title=u"Title")
+    ...     password = TextLine(title=u"Password")
 
-  >>> @implementer(IFormTest)
-  ... class FormTest:
-  ...     name = 'fred'
-  ...     displayname = 'Fred'
-  ...     password = 'password'
+    >>> @implementer(IFormTest)
+    ... class FormTest:
+    ...     name = 'fred'
+    ...     displayname = 'Fred'
+    ...     password = 'password'
 
 
 A form that handles all fields in the schema needs only set the
 "schema" attribute:
 
-  >>> from lp.app.browser.launchpadform import LaunchpadFormView
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.app.browser.launchpadform import LaunchpadFormView
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
-  >>> class FormTestView1(LaunchpadFormView):
-  ...     schema = IFormTest
+    >>> class FormTestView1(LaunchpadFormView):
+    ...     schema = IFormTest
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = FormTestView1(context, request)
-  >>> view.setUpFields()
-  >>> [field.__name__ for field in view.form_fields]
-  ['name', 'displayname', 'password']
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = FormTestView1(context, request)
+    >>> view.setUpFields()
+    >>> [field.__name__ for field in view.form_fields]
+    ['name', 'displayname', 'password']
 
 
 Restricting Displayed Fields
@@ -84,16 +84,16 @@ Restricting Displayed Fields
 
 The list of fields can be restricted with the "field_names" attribute:
 
-  >>> class FormTestView2(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['name', 'displayname']
+    >>> class FormTestView2(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['name', 'displayname']
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = FormTestView2(context, request)
-  >>> view.setUpFields()
-  >>> [field.__name__ for field in view.form_fields]
-  ['name', 'displayname']
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = FormTestView2(context, request)
+    >>> view.setUpFields()
+    >>> [field.__name__ for field in view.form_fields]
+    ['name', 'displayname']
 
 
 Custom Adapters
@@ -103,28 +103,28 @@ Sometimes a schema is used for a form that is not actually implemented
 by the context widget.  This can be handled by providing some custom
 adapters for the form.
 
-  >>> class IFormTest2(Interface):
-  ...     name = TextLine(title=u"Name")
-  >>> class FormAdaptersTestView(LaunchpadFormView):
-  ...     schema = IFormTest2
-  ...     @property
-  ...     def adapters(self):
-  ...         return {IFormTest2: self.context}
-
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> IFormTest2.providedBy(context)
-  False
-  >>> view = FormAdaptersTestView(context, request)
-  >>> view.setUpFields()
-  >>> view.setUpWidgets()
+    >>> class IFormTest2(Interface):
+    ...     name = TextLine(title=u"Name")
+    >>> class FormAdaptersTestView(LaunchpadFormView):
+    ...     schema = IFormTest2
+    ...     @property
+    ...     def adapters(self):
+    ...         return {IFormTest2: self.context}
+
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> IFormTest2.providedBy(context)
+    False
+    >>> view = FormAdaptersTestView(context, request)
+    >>> view.setUpFields()
+    >>> view.setUpWidgets()
 
 We now check to see that the widget is bound to our FormTest
 instance.  The context for the widget is a bound field object, who
 should in turn have the FormTest instance as a context:
 
-  >>> view.widgets['name'].context.context is context
-  True
+    >>> view.widgets['name'].context.context is context
+    True
 
 
 Custom Widgets
@@ -134,25 +134,25 @@ In some cases we will want to use a custom widget for a particular
 field.  These can be installed easily with a "custom_widget_NAME"
 attribute:
 
-  >>> from zope.formlib.widget import CustomWidgetFactory
-  >>> from zope.formlib.widgets import TextWidget
+    >>> from zope.formlib.widget import CustomWidgetFactory
+    >>> from zope.formlib.widgets import TextWidget
 
-  >>> class FormTestView3(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     custom_widget_displayname = CustomWidgetFactory(
-  ...         TextWidget, displayWidth=50)
+    >>> class FormTestView3(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     custom_widget_displayname = CustomWidgetFactory(
+    ...         TextWidget, displayWidth=50)
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = FormTestView3(context, request)
-  >>> view.setUpFields()
-  >>> view.setUpWidgets()
-  >>> view.widgets['displayname']
-  <...TextWidget object at ...>
-  >>> view.widgets['displayname'].displayWidth
-  50
-  >>> view.widgets['password']
-  <...TextWidget object at ...>
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = FormTestView3(context, request)
+    >>> view.setUpFields()
+    >>> view.setUpWidgets()
+    >>> view.widgets['displayname']
+    <...TextWidget object at ...>
+    >>> view.widgets['displayname'].displayWidth
+    50
+    >>> view.widgets['password']
+    <...TextWidget object at ...>
 
 
 Using Another Context
@@ -161,16 +161,16 @@ Using Another Context
 setUpWidgets() uses the view's context by default when setting up the
 widgets, but it's also possible to specify the context explicitly.
 
-  >>> view_context = FormTest()
-  >>> another_context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = FormTestView3(view_context, request)
-  >>> view.setUpFields()
-  >>> view.setUpWidgets(context=another_context)
-  >>> view.widgets['displayname'].context.context is view_context
-  False
-  >>> view.widgets['displayname'].context.context is another_context
-  True
+    >>> view_context = FormTest()
+    >>> another_context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = FormTestView3(view_context, request)
+    >>> view.setUpFields()
+    >>> view.setUpWidgets(context=another_context)
+    >>> view.widgets['displayname'].context.context is view_context
+    False
+    >>> view.widgets['displayname'].context.context is another_context
+    True
 
 
 Actions
@@ -180,28 +180,28 @@ In order for a form to accept submissions, it will need one or more
 submit actions.  These are added to the view class using the "action"
 decorator:
 
-  >>> from lp.app.browser.launchpadform import action
-  >>> class FormTestView4(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['displayname']
-  ...
-  ...     @action(u"Change Name", name="change")
-  ...     def change_action(self, action, data):
-  ...         self.context.displayname = data['displayname']
+    >>> from lp.app.browser.launchpadform import action
+    >>> class FormTestView4(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['displayname']
+    ...
+    ...     @action(u"Change Name", name="change")
+    ...     def change_action(self, action, data):
+    ...         self.context.displayname = data['displayname']
 
 This will create a submit button at the bottom of the form labeled
 "Change Name", and cause change_action() to be called when the form is
 submitted with that button.
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.displayname': 'bob',
-  ...           'field.actions.change': 'Change Name'})
-  >>> view = FormTestView4(context, request)
-  >>> view.initialize()
-  >>> print(context.displayname)
-  bob
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.displayname': 'bob',
+    ...           'field.actions.change': 'Change Name'})
+    >>> view = FormTestView4(context, request)
+    >>> view.initialize()
+    >>> print(context.displayname)
+    bob
 
 Note that input validation should not be performed inside the action
 method.  Instead, it should be performed in the validate() method, or
@@ -220,61 +220,61 @@ LaunchpadFormView.  If validity errors are detected, they should be
 reported using the addError() method (for form wide errors) or the
 setFieldError() method (for errors specific to a field):
 
-  >>> class FormTestView5(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['name', 'password']
-  ...
-  ...     def validate(self, data):
-  ...         if data.get('name') == data.get('password'):
-  ...             self.addError('your password may not be the same '
-  ...                           'as your name')
-  ...         if data.get('password') == 'password':
-  ...             self.setFieldError(six.ensure_str('password'),
-  ...                                'your password must not be "password"')
-
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.name': 'fred', 'field.password': '12345'})
-  >>> view = FormTestView5(context, request)
-  >>> view.setUpFields()
-  >>> view.setUpWidgets()
-  >>> data = {}
-  >>> view._validate(None, data)
-  []
+    >>> class FormTestView5(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['name', 'password']
+    ...
+    ...     def validate(self, data):
+    ...         if data.get('name') == data.get('password'):
+    ...             self.addError('your password may not be the same '
+    ...                           'as your name')
+    ...         if data.get('password') == 'password':
+    ...             self.setFieldError(six.ensure_str('password'),
+    ...                                'your password must not be "password"')
+
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.name': 'fred', 'field.password': '12345'})
+    >>> view = FormTestView5(context, request)
+    >>> view.setUpFields()
+    >>> view.setUpWidgets()
+    >>> data = {}
+    >>> view._validate(None, data)
+    []
 
 
 Check that form wide errors can be reported:
 
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.name': 'fred', 'field.password': 'fred'})
-  >>> view = FormTestView5(context, request)
-  >>> view.setUpFields()
-  >>> view.setUpWidgets()
-  >>> data = {}
-  >>> for error in view._validate(None, data):
-  ...     print(error)
-  your password may not be the same as your name
-  >>> for error in view.form_wide_errors:
-  ...     print(error)
-  your password may not be the same as your name
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.name': 'fred', 'field.password': 'fred'})
+    >>> view = FormTestView5(context, request)
+    >>> view.setUpFields()
+    >>> view.setUpWidgets()
+    >>> data = {}
+    >>> for error in view._validate(None, data):
+    ...     print(error)
+    your password may not be the same as your name
+    >>> for error in view.form_wide_errors:
+    ...     print(error)
+    your password may not be the same as your name
 
 Check that widget specific errors can be reported:
 
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.name': 'fred', 'field.password': 'password'})
-  >>> view = FormTestView5(context, request)
-  >>> view.setUpFields()
-  >>> view.setUpWidgets()
-  >>> data = {}
-  >>> for error in view._validate(None, data):
-  ...     print(error)
-  your password must not be &quot;password&quot;
-  >>> for field, error in view.widget_errors.items():
-  ...     print("%s: %s" % (field, error))
-  password: your password must not be &quot;password&quot;
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.name': 'fred', 'field.password': 'password'})
+    >>> view = FormTestView5(context, request)
+    >>> view.setUpFields()
+    >>> view.setUpWidgets()
+    >>> data = {}
+    >>> for error in view._validate(None, data):
+    ...     print(error)
+    your password must not be &quot;password&quot;
+    >>> for field, error in view.widget_errors.items():
+    ...     print("%s: %s" % (field, error))
+    password: your password must not be &quot;password&quot;
 
 The base template used for LaunchpadFormView classes takes care of
 displaying these errors in the appropriate locations.
@@ -288,43 +288,43 @@ easy for an action to validate its widgets, while ignoring widgets
 that belong to other actions. Here, we'll define a form with two
 required fields, and show how to validate one field at a time.
 
-  >>> class INameAndPasswordForm(Interface):
-  ...     name = TextLine(title=u"Name", required=True)
-  ...     password = TextLine(title=u"Password", required=True)
-
-  >>> class FormViewForWidgetValidation(LaunchpadFormView):
-  ...     schema = INameAndPasswordForm
-
-  >>> def print_widget_validation(names):
-  ...     data = {'field.name': '', 'field.password': ''}
-  ...     context = FormTest()
-  ...     request = LaunchpadTestRequest(method='POST', form=data)
-  ...     view = FormViewForWidgetValidation(context, request)
-  ...     view.setUpFields()
-  ...     view.setUpWidgets()
-  ...     for error in view.validate_widgets(data, names=names):
-  ...         if isinstance(error, str):
-  ...            print(error)
-  ...         else:
-  ...             print("%s: %s" % (error.widget_title, error.doc()))
+    >>> class INameAndPasswordForm(Interface):
+    ...     name = TextLine(title=u"Name", required=True)
+    ...     password = TextLine(title=u"Password", required=True)
+
+    >>> class FormViewForWidgetValidation(LaunchpadFormView):
+    ...     schema = INameAndPasswordForm
+
+    >>> def print_widget_validation(names):
+    ...     data = {'field.name': '', 'field.password': ''}
+    ...     context = FormTest()
+    ...     request = LaunchpadTestRequest(method='POST', form=data)
+    ...     view = FormViewForWidgetValidation(context, request)
+    ...     view.setUpFields()
+    ...     view.setUpWidgets()
+    ...     for error in view.validate_widgets(data, names=names):
+    ...         if isinstance(error, str):
+    ...            print(error)
+    ...         else:
+    ...             print("%s: %s" % (error.widget_title, error.doc()))
 
 Only the fields we specify will be validated:
 
-  >>> print_widget_validation(['name'])
-  Name: Required input is missing.
+    >>> print_widget_validation(['name'])
+    Name: Required input is missing.
 
-  >>> print_widget_validation(['password'])
-  Password: Required input is missing.
+    >>> print_widget_validation(['password'])
+    Password: Required input is missing.
 
-  >>> print_widget_validation(['name', 'password'])
-  Name: Required input is missing.
-  Password: Required input is missing.
+    >>> print_widget_validation(['name', 'password'])
+    Name: Required input is missing.
+    Password: Required input is missing.
 
 The default behaviour is to validate all widgets.
 
-  >>> print_widget_validation(None)
-  Name: Required input is missing.
-  Password: Required input is missing.
+    >>> print_widget_validation(None)
+    Name: Required input is missing.
+    Password: Required input is missing.
 
 
 Redirect URL
@@ -334,94 +334,95 @@ If the form is successfully posted, then LaunchpadFormView will
 redirect the user to another URL.  The URL is specified by the
 "next_url" attribute:
 
-  >>> from zope.formlib.form import action
-  >>> class FormTestView6(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['displayname']
-  ...     next_url = 'http://www.ubuntu.com/'
-  ...
-  ...     @action(u"Change Name", name="change")
-  ...     def change_action(self, action, data):
-  ...         self.context.displayname = data['displayname']
-
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.displayname': 'bob',
-  ...           'field.actions.change': 'Change Name'})
-  >>> view = FormTestView6(context, request)
-  >>> view.initialize()
-  >>> request.response.getStatus()
-  302
-  >>> print(request.response.getHeader('location'))
-  http://www.ubuntu.com/
+    >>> from zope.formlib.form import action
+    >>> class FormTestView6(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['displayname']
+    ...     next_url = 'http://www.ubuntu.com/'
+    ...
+    ...     @action(u"Change Name", name="change")
+    ...     def change_action(self, action, data):
+    ...         self.context.displayname = data['displayname']
+
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.displayname': 'bob',
+    ...           'field.actions.change': 'Change Name'})
+    >>> view = FormTestView6(context, request)
+    >>> view.initialize()
+    >>> request.response.getStatus()
+    302
+    >>> print(request.response.getHeader('location'))
+    http://www.ubuntu.com/
 
 
 Form Rendering
 --------------
 
-  (Let's define the view for the rendering tests.)
-  >>> class RenderFormTest(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['displayname']
-  ...
-  ...     def template(self):
-  ...         return u'Content that comes from a ZCML registered template.'
-  ...
-  ...     @action(u'Redirect', name='redirect')
-  ...     def redirect_action(self, action, data):
-  ...         self.next_url = 'http://launchpad.test/'
-  ...
-  ...     def handleUpdateFailure(self, action, data, errors):
-  ...         return u'Some errors occured.'
-  ...
-  ...     @action(u'Update', name='update', failure=handleUpdateFailure)
-  ...     def update_action(self, action, data):
-  ...         return u'Display name changed to: %s.' % data['displayname']
+(Let's define the view for the rendering tests.)
+
+    >>> class RenderFormTest(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['displayname']
+    ...
+    ...     def template(self):
+    ...         return u'Content that comes from a ZCML registered template.'
+    ...
+    ...     @action(u'Redirect', name='redirect')
+    ...     def redirect_action(self, action, data):
+    ...         self.next_url = 'http://launchpad.test/'
+    ...
+    ...     def handleUpdateFailure(self, action, data, errors):
+    ...         return u'Some errors occured.'
+    ...
+    ...     @action(u'Update', name='update', failure=handleUpdateFailure)
+    ...     def update_action(self, action, data):
+    ...         return u'Display name changed to: %s.' % data['displayname']
 
 Like with LaunchpadView, the view content will usually be rendered by
 executing the template attribute (which can be set from ZCML):
 
-  >>> context = FormTest()
-  >>> view = RenderFormTest(context, LaunchpadTestRequest(form={}))
-  >>> print(view())
-  Content that comes from a ZCML registered template.
+    >>> context = FormTest()
+    >>> view = RenderFormTest(context, LaunchpadTestRequest(form={}))
+    >>> print(view())
+    Content that comes from a ZCML registered template.
 
 When a redirection is done (either by calling
 self.request.response.redirect() or setting the next_url attribute), the
 rendered content is always the empty string.
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.displayname': 'bob',
-  ...           'field.actions.redirect': 'Redirect'})
-  >>> view = RenderFormTest(context, request)
-  >>> print(view())
-  <BLANKLINE>
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.displayname': 'bob',
+    ...           'field.actions.redirect': 'Redirect'})
+    >>> view = RenderFormTest(context, request)
+    >>> print(view())
+    <BLANKLINE>
 
 As an alternative to executing the template attribute, an action handler
 can directly return the rendered content:
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.displayname': 'bob',
-  ...           'field.actions.update': 'Update'})
-  >>> view = RenderFormTest(context, request)
-  >>> print(view())
-  Display name changed to: bob.
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.displayname': 'bob',
+    ...           'field.actions.update': 'Update'})
+    >>> view = RenderFormTest(context, request)
+    >>> print(view())
+    Display name changed to: bob.
 
 This is also true of failure handlers:
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.displayname': '',
-  ...           'field.actions.update': 'Update'})
-  >>> view = RenderFormTest(context, request)
-  >>> print(view())
-  Some errors occured.
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.displayname': '',
+    ...           'field.actions.update': 'Update'})
+    >>> view = RenderFormTest(context, request)
+    >>> print(view())
+    Some errors occured.
 
 
 Initial Focused Widget
@@ -431,45 +432,45 @@ The standard template for LaunchpadFormView can set the initial focus
 on a form element.  This is achieved by some javascript that gets run
 on page load.  By default, the first form widget will be focused:
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = FormTestView5(context, request)
-  >>> view.initialize()
-  >>> print(view.focusedElementScript())
-  <!--
-  setFocusByName('field.name');
-  // -->
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = FormTestView5(context, request)
+    >>> view.initialize()
+    >>> print(view.focusedElementScript())
+    <!--
+    setFocusByName('field.name');
+    // -->
 
 The focus can also be set explicitly by overriding initial_focus_widget:
 
-  >>> class FormTestView7(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['name', 'password']
-  ...     initial_focus_widget = 'password'
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = FormTestView7(context, request)
-  >>> view.initialize()
-  >>> print(view.focusedElementScript())
-  <!--
-  setFocusByName('field.password');
-  // -->
+    >>> class FormTestView7(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['name', 'password']
+    ...     initial_focus_widget = 'password'
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = FormTestView7(context, request)
+    >>> view.initialize()
+    >>> print(view.focusedElementScript())
+    <!--
+    setFocusByName('field.password');
+    // -->
 
 If initial_focus_widget is set to None, then no element will be focused
 initially:
 
-  >>> view.initial_focus_widget = None
-  >>> view.focusedElementScript()
-  ''
+    >>> view.initial_focus_widget = None
+    >>> view.focusedElementScript()
+    ''
 
 Note that if the form is being redisplayed because of a validation
 error, the generated script will focus the first widget with an error:
 
-  >>> view.setFieldError('password', 'Bad password')
-  >>> print(view.focusedElementScript())
-  <!--
-  setFocusByName('field.password');
-  // -->
+    >>> view.setFieldError('password', 'Bad password')
+    >>> print(view.focusedElementScript())
+    <!--
+    setFocusByName('field.password');
+    // -->
 
 
 Hidden widgets
@@ -483,46 +484,46 @@ using a custom widget.
 First we'll create a fake pagetemplate which doesn't use Launchpad's main
 template and thus is way simpler.
 
-  >>> from tempfile import mkstemp
-  >>> from zope.browserpage import ViewPageTemplateFile
-  >>> file, filename = mkstemp()
-  >>> f = open(filename, 'w')
-  >>> _ = f.write(u'<div metal:use-macro="context/@@launchpad_form/form" />')
-  >>> f.close()
+    >>> from tempfile import mkstemp
+    >>> from zope.browserpage import ViewPageTemplateFile
+    >>> file, filename = mkstemp()
+    >>> f = open(filename, 'w')
+    >>> _ = f.write(u'<div metal:use-macro="context/@@launchpad_form/form" />')
+    >>> f.close()
 
 By default, all widgets are visible.
 
-  >>> class TestWidgetVisibility(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['displayname']
-  ...     template = ViewPageTemplateFile(filename)
+    >>> class TestWidgetVisibility(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['displayname']
+    ...     template = ViewPageTemplateFile(filename)
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = TestWidgetVisibility(context, request)
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = TestWidgetVisibility(context, request)
 
-  >>> from lp.services.beautifulsoup import BeautifulSoup
-  >>> soup = BeautifulSoup(view())
-  >>> for input in soup.find_all('input'):
-  ...     print(input)
-  <input ... name="field.displayname" ... type="text" ...
+    >>> from lp.services.beautifulsoup import BeautifulSoup
+    >>> soup = BeautifulSoup(view())
+    >>> for input in soup.find_all('input'):
+    ...     print(input)
+    <input ... name="field.displayname" ... type="text" ...
 
 If we change a widget's 'visible' flag to False, that widget is rendered
 using its hidden() method, which should return a hidden <input> tag.
 
-  >>> class TestWidgetVisibility2(TestWidgetVisibility):
-  ...     custom_widget_displayname = CustomWidgetFactory(
-  ...         TextWidget, visible=False)
+    >>> class TestWidgetVisibility2(TestWidgetVisibility):
+    ...     custom_widget_displayname = CustomWidgetFactory(
+    ...         TextWidget, visible=False)
 
-  >>> view = TestWidgetVisibility2(context, request)
+    >>> view = TestWidgetVisibility2(context, request)
 
-  >>> soup = BeautifulSoup(view())
-  >>> for input in soup.find_all('input'):
-  ...     print(input)
-  <input ... name="field.displayname" type="hidden" ...
+    >>> soup = BeautifulSoup(view())
+    >>> for input in soup.find_all('input'):
+    ...     print(input)
+    <input ... name="field.displayname" type="hidden" ...
 
-  >>> import os
-  >>> os.remove(filename)
+    >>> import os
+    >>> os.remove(filename)
 
 
 Safe Actions
@@ -543,53 +544,53 @@ via POST requests.  There are a number of reasons for this:
 However, there are cases where a form action is safe (e.g. a "search"
 action).  Those actions can be marked as such:
 
-  >>> from lp.app.browser.launchpadform import safe_action
-  >>> class UnsafeActionTestView(LaunchpadFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['name']
-  ...
-  ...     @action(u'Change', name='change')
-  ...     def redirect_action(self, action, data):
-  ...         print('Change')
-  ...
-  ...     @safe_action
-  ...     @action(u'Search', name='search')
-  ...     def search_action(self, action, data):
-  ...         print('Search')
-  >>> context = FormTest()
+    >>> from lp.app.browser.launchpadform import safe_action
+    >>> class UnsafeActionTestView(LaunchpadFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['name']
+    ...
+    ...     @action(u'Change', name='change')
+    ...     def redirect_action(self, action, data):
+    ...         print('Change')
+    ...
+    ...     @safe_action
+    ...     @action(u'Search', name='search')
+    ...     def search_action(self, action, data):
+    ...         print('Search')
+    >>> context = FormTest()
 
 With this form, the "change" action can only be submitted with a POST
 request:
 
-  >>> request = LaunchpadTestRequest(
-  ...     environ={'REQUEST_METHOD': 'GET'},
-  ...     form={'field.name': 'foo',
-  ...           'field.actions.change': 'Change'})
-  >>> view = UnsafeActionTestView(context, request)
-  >>> view.initialize()
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.services.webapp.interfaces.UnsafeFormGetSubmissionError: field.actions.change
-
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.name': 'foo',
-  ...           'field.actions.change': 'Change'})
-  >>> view = UnsafeActionTestView(context, request)
-  >>> view.initialize()
-  Change
+    >>> request = LaunchpadTestRequest(
+    ...     environ={'REQUEST_METHOD': 'GET'},
+    ...     form={'field.name': 'foo',
+    ...           'field.actions.change': 'Change'})
+    >>> view = UnsafeActionTestView(context, request)
+    >>> view.initialize()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.services.webapp.interfaces.UnsafeFormGetSubmissionError: field.actions.change
+
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.name': 'foo',
+    ...           'field.actions.change': 'Change'})
+    >>> view = UnsafeActionTestView(context, request)
+    >>> view.initialize()
+    Change
 
 
 In contrast, the "search" action can be submitted with a GET request:
 
-  >>> request = LaunchpadTestRequest(
-  ...     environ={'REQUEST_METHOD': 'GET'},
-  ...     form={'field.name': 'foo',
-  ...           'field.actions.search': 'Search'})
-  >>> view = UnsafeActionTestView(context, request)
-  >>> view.initialize()
-  Search
+    >>> request = LaunchpadTestRequest(
+    ...     environ={'REQUEST_METHOD': 'GET'},
+    ...     form={'field.name': 'foo',
+    ...           'field.actions.search': 'Search'})
+    >>> view = UnsafeActionTestView(context, request)
+    >>> view.initialize()
+    Search
 
 
 
@@ -605,51 +606,52 @@ following ways:
 
 In other respects, it is used the same way as LaunchpadFormView:
 
-  >>> from lp.app.browser.launchpadform import LaunchpadEditFormView
-  >>> class FormTestView8(LaunchpadEditFormView):
-  ...     schema = IFormTest
-  ...     field_names = ['displayname']
-  ...     next_url = 'http://www.ubuntu.com/'
-  ...
-  ...     @action(u"Change Name", name="change")
-  ...     def change_action(self, action, data):
-  ...         if self.updateContextFromData(data):
-  ...             print('Context was updated')
+    >>> from lp.app.browser.launchpadform import LaunchpadEditFormView
+    >>> class FormTestView8(LaunchpadEditFormView):
+    ...     schema = IFormTest
+    ...     field_names = ['displayname']
+    ...     next_url = 'http://www.ubuntu.com/'
+    ...
+    ...     @action(u"Change Name", name="change")
+    ...     def change_action(self, action, data):
+    ...         if self.updateContextFromData(data):
+    ...             print('Context was updated')
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest()
-  >>> view = FormTestView8(context, request)
-  >>> view.initialize()
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest()
+    >>> view = FormTestView8(context, request)
+    >>> view.initialize()
 
 
 The field values take their defaults from the context object:
 
-  >>> print(view.widgets['displayname']())
-  <input...value="Fred"...
+    >>> print(view.widgets['displayname']())
+    <input...value="Fred"...
 
 The updateContextFromData() method takes care of updating the context
 object for us too:
 
-  >>> context = FormTest()
-  >>> request = LaunchpadTestRequest(
-  ...     method='POST',
-  ...     form={'field.displayname': 'James Henstridge',
-  ...           'field.actions.change': 'Change Name'})
-  >>> view = FormTestView8(context, request)
-  >>> view.initialize()
-  Context was updated
+    >>> context = FormTest()
+    >>> request = LaunchpadTestRequest(
+    ...     method='POST',
+    ...     form={'field.displayname': 'James Henstridge',
+    ...           'field.actions.change': 'Change Name'})
+    >>> view = FormTestView8(context, request)
+    >>> view.initialize()
+    Context was updated
 
-  >>> request.response.getStatus()
-  302
+    >>> request.response.getStatus()
+    302
 
-  >>> print(context.displayname)
-  James Henstridge
+    >>> print(context.displayname)
+    James Henstridge
 
 By default updateContextFromData() uses the view's context, but it's
 possible to pass in a specific context to use instead:
 
-  >>> custom_context = FormTest()
-  >>> view.updateContextFromData({'displayname': u'New name'}, custom_context)
-  True
-  >>> print(custom_context.displayname)
-  New name
+    >>> custom_context = FormTest()
+    >>> view.updateContextFromData(
+    ...     {'displayname': u'New name'}, custom_context)
+    True
+    >>> print(custom_context.displayname)
+    New name
diff --git a/lib/lp/app/doc/launchpadformharness.txt b/lib/lp/app/doc/launchpadformharness.txt
index 314db9f..89140a5 100644
--- a/lib/lp/app/doc/launchpadformharness.txt
+++ b/lib/lp/app/doc/launchpadformharness.txt
@@ -7,106 +7,106 @@ to check the form's behaviour with different inputs.
 
 To demonstrate its use we'll create a sample schema and view class:
 
-  >>> from zope.interface import Interface, implementer
-  >>> from zope.schema import Int, TextLine
-  >>> from lp.app.browser.launchpadform import LaunchpadFormView, action
-
-  >>> class IHarnessTest(Interface):
-  ...     string = TextLine(title=u"String")
-  ...     number = Int(title=u"Number")
-
-  >>> @implementer(IHarnessTest)
-  ... class HarnessTest:
-  ...     string = None
-  ...     number = 0
-
-  >>> class HarnessTestView(LaunchpadFormView):
-  ...     schema = IHarnessTest
-  ...     next_url = 'https://launchpad.net/'
-  ...
-  ...     def validate(self, data):
-  ...         if len(data.get('string', '')) == data.get('number'):
-  ...             self.addError("number must not be equal to string length")
-  ...         if data.get('number') == 7:
-  ...             self.setFieldError('number', 'number can not be 7')
-  ...
-  ...     @action("Submit")
-  ...     def submit_action(self, action, data):
-  ...         self.context.string = data['string']
-  ...         self.context.number = data['number']
+    >>> from zope.interface import Interface, implementer
+    >>> from zope.schema import Int, TextLine
+    >>> from lp.app.browser.launchpadform import LaunchpadFormView, action
+
+    >>> class IHarnessTest(Interface):
+    ...     string = TextLine(title=u"String")
+    ...     number = Int(title=u"Number")
+
+    >>> @implementer(IHarnessTest)
+    ... class HarnessTest:
+    ...     string = None
+    ...     number = 0
+
+    >>> class HarnessTestView(LaunchpadFormView):
+    ...     schema = IHarnessTest
+    ...     next_url = 'https://launchpad.net/'
+    ...
+    ...     def validate(self, data):
+    ...         if len(data.get('string', '')) == data.get('number'):
+    ...             self.addError("number must not be equal to string length")
+    ...         if data.get('number') == 7:
+    ...             self.setFieldError('number', 'number can not be 7')
+    ...
+    ...     @action("Submit")
+    ...     def submit_action(self, action, data):
+    ...         self.context.string = data['string']
+    ...         self.context.number = data['number']
 
 We can then create a harness to drive the view:
 
-  >>> from lp.testing.deprecated import LaunchpadFormHarness
-  >>> context = HarnessTest()
-  >>> harness = LaunchpadFormHarness(context, HarnessTestView)
+    >>> from lp.testing.deprecated import LaunchpadFormHarness
+    >>> context = HarnessTest()
+    >>> harness = LaunchpadFormHarness(context, HarnessTestView)
 
 As we haven't submitted the form, there are no errors:
 
-  >>> harness.hasErrors()
-  False
+    >>> harness.hasErrors()
+    False
 
 If we submit the form with some invalid data, we will have some errors
 though:
 
-  >>> harness.submit('submit', {'field.string': 'abcdef',
-  ...                           'field.number': '6' })
-  >>> harness.hasErrors()
-  True
+    >>> harness.submit('submit', {'field.string': 'abcdef',
+    ...                           'field.number': '6' })
+    >>> harness.hasErrors()
+    True
 
 We can then get a list of the whole-form errors:
 
-  >>> for message in harness.getFormErrors():
-  ...     print(message)
-  number must not be equal to string length
+    >>> for message in harness.getFormErrors():
+    ...     print(message)
+    number must not be equal to string length
 
 
 We can also check for per-widget errors:
 
-  >>> harness.submit('submit', {'field.string': 'abcdef',
-  ...                           'field.number': 'not a number' })
-  >>> harness.hasErrors()
-  True
-  >>> print(harness.getFieldError('string'))
-  <BLANKLINE>
-  >>> print(harness.getFieldError('number'))
-  Invalid integer data
+    >>> harness.submit('submit', {'field.string': 'abcdef',
+    ...                           'field.number': 'not a number' })
+    >>> harness.hasErrors()
+    True
+    >>> print(harness.getFieldError('string'))
+    <BLANKLINE>
+    >>> print(harness.getFieldError('number'))
+    Invalid integer data
 
 
 The getFieldError() method will also return custom error messages set
 by setFieldError():
 
-  >>> harness.submit('submit', {'field.string': 'abcdef',
-  ...                           'field.number': '7' })
-  >>> harness.hasErrors()
-  True
-  >>> print(harness.getFieldError('number'))
-  number can not be 7
+    >>> harness.submit('submit', {'field.string': 'abcdef',
+    ...                           'field.number': '7' })
+    >>> harness.hasErrors()
+    True
+    >>> print(harness.getFieldError('number'))
+    number can not be 7
 
 
 We can check to see if the view tried to redirect us.  When there are
 input validation problems, the view will not normally redirect you:
 
-  >>> harness.wasRedirected()
-  False
+    >>> harness.wasRedirected()
+    False
 
 But if we submit correct data to the form and get redirected, we can
 see where we were redirected to:
 
-  >>> harness.submit('submit', {'field.string': 'abcdef',
-  ...                           'field.number': '42' })
-  >>> harness.wasRedirected()
-  True
-  >>> harness.redirectionTarget()
-  'https://launchpad.net/'
+    >>> harness.submit('submit', {'field.string': 'abcdef',
+    ...                           'field.number': '42' })
+    >>> harness.wasRedirected()
+    True
+    >>> harness.redirectionTarget()
+    'https://launchpad.net/'
 
 We can also see that the context object was updated by this form
 submission:
 
-  >>> print(context.string)
-  abcdef
-  >>> context.number
-  42
+    >>> print(context.string)
+    abcdef
+    >>> context.number
+    42
 
 By default LaunchpadFormHarness uses LaunchpadTestRequest as its request
 class, but it's possible to change that by passing a request_class argument to
diff --git a/lib/lp/app/doc/presenting-lengths-of-time.txt b/lib/lp/app/doc/presenting-lengths-of-time.txt
index bd2deb2..66e1666 100644
--- a/lib/lp/app/doc/presenting-lengths-of-time.txt
+++ b/lib/lp/app/doc/presenting-lengths-of-time.txt
@@ -3,30 +3,30 @@ Presenting Lengths of Time
 
 First, let's bring in some dependencies:
 
-   >>> from lp.testing import test_tales
-   >>> from datetime import timedelta
+    >>> from lp.testing import test_tales
+    >>> from datetime import timedelta
 
 Exact Duration
 --------------
 
 To display the precise length of a duraction, use fmt:exactduration:
 
-   >>> td = timedelta(days=1, hours=2, minutes=3, seconds=4.567)
-   >>> test_tales('td/fmt:exactduration', td=td)
-   '1 day, 2 hours, 3 minutes, 4.6 seconds'
-   >>> td = timedelta(days=1, minutes=3, seconds=4.567)
-   >>> test_tales('td/fmt:exactduration', td=td)
-   '1 day, 0 hours, 3 minutes, 4.6 seconds'
-   >>> td = timedelta(minutes=3, seconds=4.567)
-   >>> test_tales('td/fmt:exactduration', td=td)
-   '3 minutes, 4.6 seconds'
-
-   >>> td = timedelta(days=1, hours=1, minutes=1, seconds=1)
-   >>> test_tales('td/fmt:exactduration', td=td)
-   '1 day, 1 hour, 1 minute, 1.0 seconds'
-   >>> td = timedelta(days=2, hours=2, minutes=2, seconds=2)
-   >>> test_tales('td/fmt:exactduration', td=td)
-   '2 days, 2 hours, 2 minutes, 2.0 seconds'
+    >>> td = timedelta(days=1, hours=2, minutes=3, seconds=4.567)
+    >>> test_tales('td/fmt:exactduration', td=td)
+    '1 day, 2 hours, 3 minutes, 4.6 seconds'
+    >>> td = timedelta(days=1, minutes=3, seconds=4.567)
+    >>> test_tales('td/fmt:exactduration', td=td)
+    '1 day, 0 hours, 3 minutes, 4.6 seconds'
+    >>> td = timedelta(minutes=3, seconds=4.567)
+    >>> test_tales('td/fmt:exactduration', td=td)
+    '3 minutes, 4.6 seconds'
+
+    >>> td = timedelta(days=1, hours=1, minutes=1, seconds=1)
+    >>> test_tales('td/fmt:exactduration', td=td)
+    '1 day, 1 hour, 1 minute, 1.0 seconds'
+    >>> td = timedelta(days=2, hours=2, minutes=2, seconds=2)
+    >>> test_tales('td/fmt:exactduration', td=td)
+    '2 days, 2 hours, 2 minutes, 2.0 seconds'
 
 Approximate Duration
 --------------------
@@ -34,186 +34,186 @@ Approximate Duration
 To get more friendly-to-display duration output, use
 fmt:approximateduration:
 
-   >>> td = timedelta(seconds=0)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 second'
-
-   >>> td = timedelta(seconds=-1)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 second'
-
-   >>> td = timedelta(seconds=1.1)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 second'
-   >>> td = timedelta(seconds=2.4)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '2 seconds'
-   >>> td = timedelta(seconds=3.0)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '3 seconds'
-   >>> td = timedelta(seconds=3.5)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '4 seconds'
-
-   >>> td = timedelta(seconds=4.5)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '5 seconds'
-   >>> td = timedelta(seconds=6)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '5 seconds'
-
-   >>> td = timedelta(seconds=8)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '10 seconds'
-   >>> td = timedelta(seconds=12.4)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '10 seconds'
-
-   >>> td = timedelta(seconds=12.5)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '15 seconds'
-   >>> td = timedelta(seconds=16.9)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '15 seconds'
-
-   >>> td = timedelta(seconds=17.5)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '20 seconds'
-   >>> td = timedelta(seconds=22)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '20 seconds'
-
-   >>> td = timedelta(seconds=22.5)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '25 seconds'
-   >>> td = timedelta(seconds=27.4)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '25 seconds'
-
-   >>> td = timedelta(seconds=28)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '30 seconds'
-   >>> td = timedelta(seconds=31.2)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '30 seconds'
-
-   >>> td = timedelta(seconds=35)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '40 seconds'
-   >>> td = timedelta(seconds=44.999)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '40 seconds'
-
-   >>> td = timedelta(seconds=45)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '50 seconds'
-   >>> td = timedelta(seconds=54.11)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '50 seconds'
-
-   >>> td = timedelta(seconds=55)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 minute'
-   >>> td = timedelta(seconds=88.123)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 minute'
-
-   >>> td = timedelta(seconds=90)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '2 minutes'
-   >>> td = timedelta(seconds=149.9181)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '2 minutes'
-
-   >>> td = timedelta(seconds=150)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '3 minutes'
-   >>> td = timedelta(seconds=199)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '3 minutes'
-
-   >>> td = timedelta(seconds=330)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '6 minutes'
-   >>> td = timedelta(seconds=375.1)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '6 minutes'
-
-   >>> td = timedelta(seconds=645)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '11 minutes'
-   >>> td = timedelta(seconds=689.9999)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '11 minutes'
-
-   >>> td = timedelta(seconds=3500)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '58 minutes'
-
-   >>> td = timedelta(seconds=3569)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '59 minutes'
-
-   >>> td = timedelta(seconds=3570)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 hour'
-
-   >>> td = timedelta(seconds=3899.99999)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 hour'
-
-   >>> td = timedelta(seconds=5100.181)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 hour 30 minutes'
-
-   >>> td = timedelta(seconds=5655.119)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '1 hour 30 minutes'
-
-   >>> td = timedelta(seconds=35200.1234)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '9 hours 50 minutes'
-
-   >>> td = timedelta(seconds=35850.2828)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '10 hours'
-
-   >>> td = timedelta(seconds=38000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '11 hours'
-
-   >>> td = timedelta(seconds=170000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '47 hours'
-
-   >>> td = timedelta(seconds=171000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '2 days'
-
-   >>> td = timedelta(seconds=900000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '10 days'
-
-   >>> td = timedelta(seconds=1160000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '13 days'
-
-   >>> td = timedelta(seconds=1500000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '2 weeks'
-
-   >>> td = timedelta(seconds=6000000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '10 weeks'
-
-   >>> td = timedelta(seconds=6350400)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '11 weeks'
-
-   >>> td = timedelta(seconds=7560000)
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '13 weeks'
-
-   >>> td = timedelta(days=(365 * 99))
-   >>> test_tales('td/fmt:approximateduration', td=td)
-   '5162 weeks'
+    >>> td = timedelta(seconds=0)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 second'
+
+    >>> td = timedelta(seconds=-1)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 second'
+
+    >>> td = timedelta(seconds=1.1)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 second'
+    >>> td = timedelta(seconds=2.4)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '2 seconds'
+    >>> td = timedelta(seconds=3.0)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '3 seconds'
+    >>> td = timedelta(seconds=3.5)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '4 seconds'
+
+    >>> td = timedelta(seconds=4.5)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '5 seconds'
+    >>> td = timedelta(seconds=6)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '5 seconds'
+
+    >>> td = timedelta(seconds=8)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '10 seconds'
+    >>> td = timedelta(seconds=12.4)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '10 seconds'
+
+    >>> td = timedelta(seconds=12.5)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '15 seconds'
+    >>> td = timedelta(seconds=16.9)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '15 seconds'
+
+    >>> td = timedelta(seconds=17.5)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '20 seconds'
+    >>> td = timedelta(seconds=22)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '20 seconds'
+
+    >>> td = timedelta(seconds=22.5)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '25 seconds'
+    >>> td = timedelta(seconds=27.4)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '25 seconds'
+
+    >>> td = timedelta(seconds=28)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '30 seconds'
+    >>> td = timedelta(seconds=31.2)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '30 seconds'
+
+    >>> td = timedelta(seconds=35)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '40 seconds'
+    >>> td = timedelta(seconds=44.999)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '40 seconds'
+
+    >>> td = timedelta(seconds=45)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '50 seconds'
+    >>> td = timedelta(seconds=54.11)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '50 seconds'
+
+    >>> td = timedelta(seconds=55)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 minute'
+    >>> td = timedelta(seconds=88.123)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 minute'
+
+    >>> td = timedelta(seconds=90)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '2 minutes'
+    >>> td = timedelta(seconds=149.9181)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '2 minutes'
+
+    >>> td = timedelta(seconds=150)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '3 minutes'
+    >>> td = timedelta(seconds=199)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '3 minutes'
+
+    >>> td = timedelta(seconds=330)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '6 minutes'
+    >>> td = timedelta(seconds=375.1)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '6 minutes'
+
+    >>> td = timedelta(seconds=645)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '11 minutes'
+    >>> td = timedelta(seconds=689.9999)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '11 minutes'
+
+    >>> td = timedelta(seconds=3500)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '58 minutes'
+
+    >>> td = timedelta(seconds=3569)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '59 minutes'
+
+    >>> td = timedelta(seconds=3570)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 hour'
+
+    >>> td = timedelta(seconds=3899.99999)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 hour'
+
+    >>> td = timedelta(seconds=5100.181)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 hour 30 minutes'
+
+    >>> td = timedelta(seconds=5655.119)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '1 hour 30 minutes'
+
+    >>> td = timedelta(seconds=35200.1234)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '9 hours 50 minutes'
+
+    >>> td = timedelta(seconds=35850.2828)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '10 hours'
+
+    >>> td = timedelta(seconds=38000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '11 hours'
+
+    >>> td = timedelta(seconds=170000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '47 hours'
+
+    >>> td = timedelta(seconds=171000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '2 days'
+
+    >>> td = timedelta(seconds=900000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '10 days'
+
+    >>> td = timedelta(seconds=1160000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '13 days'
+
+    >>> td = timedelta(seconds=1500000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '2 weeks'
+
+    >>> td = timedelta(seconds=6000000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '10 weeks'
+
+    >>> td = timedelta(seconds=6350400)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '11 weeks'
+
+    >>> td = timedelta(seconds=7560000)
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '13 weeks'
+
+    >>> td = timedelta(days=(365 * 99))
+    >>> test_tales('td/fmt:approximateduration', td=td)
+    '5162 weeks'
diff --git a/lib/lp/app/doc/tales-email-formatting.txt b/lib/lp/app/doc/tales-email-formatting.txt
index 0dd70cb..e4038d9 100644
--- a/lib/lp/app/doc/tales-email-formatting.txt
+++ b/lib/lp/app/doc/tales-email-formatting.txt
@@ -11,7 +11,7 @@ the common use cases.
 
 First, let's bring in a small helper function:
 
-   >>> from lp.testing import test_tales
+    >>> from lp.testing import test_tales
 
 Quoting styles
 --------------
diff --git a/lib/lp/app/stories/basics/copyright.txt b/lib/lp/app/stories/basics/copyright.txt
index 7e71da7..af6789c 100644
--- a/lib/lp/app/stories/basics/copyright.txt
+++ b/lib/lp/app/stories/basics/copyright.txt
@@ -2,15 +2,15 @@ Launchpad has a copyright notice in different templates in the code base.
 
 The tour pages.
 
-  >>> browser.open('http://launchpad.test/')
-  >>> browser.getLink('Take the tour').click()
-  >>> print(
-  ...     extract_text(find_tag_by_id(browser.contents, 'footer-navigation')))
-  Next...© 2004-2021 Canonical Ltd...
+    >>> browser.open('http://launchpad.test/')
+    >>> browser.getLink('Take the tour').click()
+    >>> print(extract_text(find_tag_by_id(
+    ...     browser.contents, 'footer-navigation')))
+    Next...© 2004-2021 Canonical Ltd...
 
 The main template.
 
-  >>> browser.open('http://launchpad.test')
-  >>> print(extract_text(find_tag_by_id(browser.contents, 'footer')))
-  © 2004-2021 Canonical Ltd.
-  ...
+    >>> browser.open('http://launchpad.test')
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'footer')))
+    © 2004-2021 Canonical Ltd.
+    ...
diff --git a/lib/lp/app/stories/basics/notfound-head.txt b/lib/lp/app/stories/basics/notfound-head.txt
index 719a063..b6988b7 100644
--- a/lib/lp/app/stories/basics/notfound-head.txt
+++ b/lib/lp/app/stories/basics/notfound-head.txt
@@ -2,50 +2,48 @@
 HEAD requests should never have a body in their response, even if there are
 errors (such as 404s).
 
-  >>> response = http(r"""
-  ... HEAD / HTTP/1.1
-  ... """)
-  >>> print(str(response).split('\n')[0])
-  HTTP/1.1 200 Ok
-  >>> print(response.getHeader('Content-Length'))
-  0
-  >>> print(six.ensure_text(response.getBody()))
-  <BLANKLINE>
-
-  >>> response = http(r"""
-  ... HEAD /badurl HTTP/1.1
-  ... """)
-  >>> print(str(response).split('\n')[0])
-  HTTP/1.1 404 Not Found
-  >>> print(response.getHeader('Content-Length'))
-  0
-  >>> print(six.ensure_text(response.getBody()))
-  <BLANKLINE>
+    >>> response = http(r"""
+    ... HEAD / HTTP/1.1
+    ... """)
+    >>> print(str(response).split('\n')[0])
+    HTTP/1.1 200 Ok
+    >>> print(response.getHeader('Content-Length'))
+    0
+    >>> print(six.ensure_text(response.getBody()))
+    <BLANKLINE>
+
+    >>> response = http(r"""
+    ... HEAD /badurl HTTP/1.1
+    ... """)
+    >>> print(str(response).split('\n')[0])
+    HTTP/1.1 404 Not Found
+    >>> print(response.getHeader('Content-Length'))
+    0
+    >>> print(six.ensure_text(response.getBody()))
+    <BLANKLINE>
 
 Register a test page that generates HTTP 500 errors.
 
-  >>> from zope.component import provideAdapter
-  >>> from zope.interface import Interface
-  >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
-  >>> class ErrorView(object):
-  ...     """A broken view"""
-  ...     def __init__(self, *args):
-  ...         oops
-  ...
-  >>> provideAdapter(
-  ...       ErrorView, (None, IDefaultBrowserLayer), Interface, "error-test")
+    >>> from zope.component import provideAdapter
+    >>> from zope.interface import Interface
+    >>> from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+    >>> class ErrorView(object):
+    ...     """A broken view"""
+    ...     def __init__(self, *args):
+    ...         oops
+    ...
+    >>> provideAdapter(
+    ...     ErrorView, (None, IDefaultBrowserLayer), Interface, "error-test")
 
 Do a HEAD request on the error test page, and check that its response also has
 no body.
 
-  >>> response = http(r"""
-  ... HEAD /error-test HTTP/1.1
-  ... """)
-  >>> print(str(response).split('\n')[0])
-  HTTP/1.1 500 Internal Server Error
-  >>> print(response.getHeader('Content-Length'))
-  0
-  >>> print(six.ensure_text(response.getBody()))
-  <BLANKLINE>
-
-
+    >>> response = http(r"""
+    ... HEAD /error-test HTTP/1.1
+    ... """)
+    >>> print(str(response).split('\n')[0])
+    HTTP/1.1 500 Internal Server Error
+    >>> print(response.getHeader('Content-Length'))
+    0
+    >>> print(six.ensure_text(response.getBody()))
+    <BLANKLINE>
diff --git a/lib/lp/app/stories/basics/page-request-summaries.txt b/lib/lp/app/stories/basics/page-request-summaries.txt
index 546f8a1..a8a87d9 100644
--- a/lib/lp/app/stories/basics/page-request-summaries.txt
+++ b/lib/lp/app/stories/basics/page-request-summaries.txt
@@ -4,15 +4,14 @@ least" because unfortunately some queries may be issued after the page may
 finish rendering -- the authoritative source is in OOPS reports or in the web
 app's stderr.
 
-  >>> browser.open('http://launchpad.test/')
-  >>> print(browser.contents)
-  <!DOCTYPE...
-  ...<!--... At least ... actions issued in ... seconds ...-->...
+    >>> browser.open('http://launchpad.test/')
+    >>> print(browser.contents)
+    <!DOCTYPE...
+    ...<!--... At least ... actions issued in ... seconds ...-->...
 
 It's available for any page:
 
-
-  >>> browser.open('http://launchpad.test/~mark/')
-  >>> print(browser.contents)
-  <!DOCTYPE...
-  ...<!--... At least ... actions issued in ... seconds ...-->...
+    >>> browser.open('http://launchpad.test/~mark/')
+    >>> print(browser.contents)
+    <!DOCTYPE...
+    ...<!--... At least ... actions issued in ... seconds ...-->...
diff --git a/lib/lp/app/stories/basics/xx-developerexceptions.txt b/lib/lp/app/stories/basics/xx-developerexceptions.txt
index ee8c170..b8373f8 100644
--- a/lib/lp/app/stories/basics/xx-developerexceptions.txt
+++ b/lib/lp/app/stories/basics/xx-developerexceptions.txt
@@ -103,11 +103,10 @@ http handle_errors
 lp.testing.pages.http accepts the handle_errors parameter in case you
 want to see tracebacks instead of error pages.
 
-  >>> print(http(r"""
-  ... GET /whatever HTTP/1.1
-  ... """, handle_errors=False))
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.publisher.interfaces.NotFound: ...
-
+    >>> print(http(r"""
+    ... GET /whatever HTTP/1.1
+    ... """, handle_errors=False))
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.publisher.interfaces.NotFound: ...
diff --git a/lib/lp/app/stories/basics/xx-launchpad-statistics.txt b/lib/lp/app/stories/basics/xx-launchpad-statistics.txt
index 891b1d0..f7185c7 100644
--- a/lib/lp/app/stories/basics/xx-launchpad-statistics.txt
+++ b/lib/lp/app/stories/basics/xx-launchpad-statistics.txt
@@ -2,22 +2,21 @@
 We also have the special Launchpad Statistics summary page. This is only
 acessible to launchpad Admins:
 
-  >>> user_browser.open('http://launchpad.test/+statistics')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  zope.security.interfaces.Unauthorized: ...
+    >>> user_browser.open('http://launchpad.test/+statistics')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    zope.security.interfaces.Unauthorized: ...
 
 
 When we login as an admin, we can see all the stats listed:
 
-  >>> admin_browser.open('http://launchpad.test/+statistics/')
-  >>> print(admin_browser.title)
-  Launchpad statistics
-  >>> 'answered_question_count' in admin_browser.contents
-  True
-  >>> 'products_with_blueprints' in admin_browser.contents
-  True
-  >>> 'solved_question_count' in admin_browser.contents
-  True
-
+    >>> admin_browser.open('http://launchpad.test/+statistics/')
+    >>> print(admin_browser.title)
+    Launchpad statistics
+    >>> 'answered_question_count' in admin_browser.contents
+    True
+    >>> 'products_with_blueprints' in admin_browser.contents
+    True
+    >>> 'solved_question_count' in admin_browser.contents
+    True
diff --git a/lib/lp/app/stories/basics/xx-notifications.txt b/lib/lp/app/stories/basics/xx-notifications.txt
index aca02dd..17797d1 100644
--- a/lib/lp/app/stories/basics/xx-notifications.txt
+++ b/lib/lp/app/stories/basics/xx-notifications.txt
@@ -3,94 +3,96 @@ Ensure that notifications are being displayed and propogated correctly.
 
 This first page adds notifications itself before being rendered.
 
->>> print(http(r"""
-... GET /+notificationtest1 HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... """))
-HTTP/1.1 200 Ok
-Content-Length: ...
-Content-Type: text/html;charset=utf-8
-...
-...<div class="error message">Error notification <b>1</b></div>
-...<div class="error message">Error notification <b>2</b></div>
-...<div class="warning message">Warning notification <b>1</b></div>
-...<div class="warning message">Warning notification <b>2</b></div>
-...<div class="informational message">Info notification <b>1</b></div>
-...<div class="informational message">Info notification <b>2</b></div>
-...<div class="debug message">Debug notification <b>1</b></div>
-...<div class="debug message">Debug notification <b>2</b></div>
-...
+    >>> print(http(r"""
+    ... GET /+notificationtest1 HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... """))
+    HTTP/1.1 200 Ok
+    Content-Length: ...
+    Content-Type: text/html;charset=utf-8
+    ...
+    ...<div class="error message">Error notification <b>1</b></div>
+    ...<div class="error message">Error notification <b>2</b></div>
+    ...<div class="warning message">Warning notification <b>1</b></div>
+    ...<div class="warning message">Warning notification <b>2</b></div>
+    ...<div class="informational message">Info notification <b>1</b></div>
+    ...<div class="informational message">Info notification <b>2</b></div>
+    ...<div class="debug message">Debug notification <b>1</b></div>
+    ...<div class="debug message">Debug notification <b>2</b></div>
+    ...
 
 This second page adds notifications, and then redirects to another page.
 The notification messages should be propogated.
 
->>> result = http(r"""
-... GET /+notificationtest2 HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... """)
->>> print(result)
-HTTP/1.1 303 See Other
-...
-Location: /
-...
->>> import re
->>> destination_url = re.search('(?m)^Location:\s(.*)$', str(result)).group(1)
->>> launchpad_session_cookie = re.search(
-...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
->>> print(http(r"""
-... GET %(destination_url)s HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... Cookie: launchpad_tests=%(launchpad_session_cookie)s
-... """ % vars()))
-HTTP/1.1 200 Ok
-...
-...<div class="error message">Error notification <b>1</b></div>
-...<div class="error message">Error notification <b>2</b></div>
-...<div class="warning message">Warning notification <b>1</b></div>
-...<div class="warning message">Warning notification <b>2</b></div>
-...<div class="informational message">Info notification <b>1</b></div>
-...<div class="informational message">Info notification <b>2</b></div>
-...<div class="debug message">Debug notification <b>1</b></div>
-...<div class="debug message">Debug notification <b>2</b></div>
-...
+    >>> result = http(r"""
+    ... GET /+notificationtest2 HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... """)
+    >>> print(result)
+    HTTP/1.1 303 See Other
+    ...
+    Location: /
+    ...
+    >>> import re
+    >>> destination_url = re.search(
+    ...     '(?m)^Location:\s(.*)$', str(result)).group(1)
+    >>> launchpad_session_cookie = re.search(
+    ...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
+    >>> print(http(r"""
+    ... GET %(destination_url)s HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... Cookie: launchpad_tests=%(launchpad_session_cookie)s
+    ... """ % vars()))
+    HTTP/1.1 200 Ok
+    ...
+    ...<div class="error message">Error notification <b>1</b></div>
+    ...<div class="error message">Error notification <b>2</b></div>
+    ...<div class="warning message">Warning notification <b>1</b></div>
+    ...<div class="warning message">Warning notification <b>2</b></div>
+    ...<div class="informational message">Info notification <b>1</b></div>
+    ...<div class="informational message">Info notification <b>2</b></div>
+    ...<div class="debug message">Debug notification <b>1</b></div>
+    ...<div class="debug message">Debug notification <b>2</b></div>
+    ...
 
 
 Our third test page adds notifications and then redirects to a page that
 adds further notifications. This demonstrates that notifications are
 combined.
 
->>> result = http(r"""
-... GET /+notificationtest3 HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... """)
->>> print(result)
-HTTP/1.1 303 See Other
-...
-Content-Length: 0
-...
-Location: /+notificationtest1
-...
+    >>> result = http(r"""
+    ... GET /+notificationtest3 HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... """)
+    >>> print(result)
+    HTTP/1.1 303 See Other
+    ...
+    Content-Length: 0
+    ...
+    Location: /+notificationtest1
+    ...
 
->>> destination_url = re.search('(?m)^Location:\s(.*)$', str(result)).group(1)
->>> launchpad_session_cookie = re.search(
-...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
->>> print(http(r"""
-... GET %(destination_url)s HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... Cookie: launchpad_tests=%(launchpad_session_cookie)s
-... """ % vars()))
-HTTP/1.1 200 Ok
-...
-...<div class="error message">+notificationtest3 error</div>
-...<div class="error message">Error notification <b>1</b></div>
-...<div class="error message">Error notification <b>2</b></div>
-...<div class="warning message">Warning notification <b>1</b></div>
-...<div class="warning message">Warning notification <b>2</b></div>
-...<div class="informational message">Info notification <b>1</b></div>
-...<div class="informational message">Info notification <b>2</b></div>
-...<div class="debug message">Debug notification <b>1</b></div>
-...<div class="debug message">Debug notification <b>2</b></div>
-...
+    >>> destination_url = re.search(
+    ...     '(?m)^Location:\s(.*)$', str(result)).group(1)
+    >>> launchpad_session_cookie = re.search(
+    ...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
+    >>> print(http(r"""
+    ... GET %(destination_url)s HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... Cookie: launchpad_tests=%(launchpad_session_cookie)s
+    ... """ % vars()))
+    HTTP/1.1 200 Ok
+    ...
+    ...<div class="error message">+notificationtest3 error</div>
+    ...<div class="error message">Error notification <b>1</b></div>
+    ...<div class="error message">Error notification <b>2</b></div>
+    ...<div class="warning message">Warning notification <b>1</b></div>
+    ...<div class="warning message">Warning notification <b>2</b></div>
+    ...<div class="informational message">Info notification <b>1</b></div>
+    ...<div class="informational message">Info notification <b>2</b></div>
+    ...<div class="debug message">Debug notification <b>1</b></div>
+    ...<div class="debug message">Debug notification <b>2</b></div>
+    ...
 
 
 Our fourth test page adds notifications, redirects to a page that
@@ -99,52 +101,54 @@ notifications. This demonstrates that notifications are preserved and
 combined across multiple redirects. Hopefully this functionality won't
 be needed.
 
->>> result = http(r"""
-... GET /+notificationtest4 HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... """)
->>> print(result)
-HTTP/1.1 303 See Other
-...
-Content-Length: 0
-...
-Location: /+notificationtest3
-...
+    >>> result = http(r"""
+    ... GET /+notificationtest4 HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... """)
+    >>> print(result)
+    HTTP/1.1 303 See Other
+    ...
+    Content-Length: 0
+    ...
+    Location: /+notificationtest3
+    ...
 
->>> destination_url = re.search('(?m)^Location:\s(.*)$', str(result)).group(1)
->>> launchpad_session_cookie = re.search(
-...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
->>> result = http(r"""
-... GET %(destination_url)s HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... Cookie: launchpad_tests=%(launchpad_session_cookie)s
-... """ % vars())
->>> print(result)
-HTTP/1.1 303 See Other
-...
-Content-Length: 0
-...
-Location: /+notificationtest1
-...
+    >>> destination_url = re.search(
+    ...     '(?m)^Location:\s(.*)$', str(result)).group(1)
+    >>> launchpad_session_cookie = re.search(
+    ...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
+    >>> result = http(r"""
+    ... GET %(destination_url)s HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... Cookie: launchpad_tests=%(launchpad_session_cookie)s
+    ... """ % vars())
+    >>> print(result)
+    HTTP/1.1 303 See Other
+    ...
+    Content-Length: 0
+    ...
+    Location: /+notificationtest1
+    ...
 
->>> destination_url = re.search('(?m)^Location:\s(.*)$', str(result)).group(1)
->>> launchpad_session_cookie = re.search(
-...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
->>> print(http(r"""
-... GET %(destination_url)s HTTP/1.1
-... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
-... Cookie: launchpad_tests=%(launchpad_session_cookie)s
-... """ % vars()))
-HTTP/1.1 200 Ok
-...
-...<div class="error message">+notificationtest4 error</div>
-...<div class="error message">+notificationtest3 error</div>
-...<div class="error message">Error notification <b>1</b></div>
-...<div class="error message">Error notification <b>2</b></div>
-...<div class="warning message">Warning notification <b>1</b></div>
-...<div class="warning message">Warning notification <b>2</b></div>
-...<div class="informational message">Info notification <b>1</b></div>
-...<div class="informational message">Info notification <b>2</b></div>
-...<div class="debug message">Debug notification <b>1</b></div>
-...<div class="debug message">Debug notification <b>2</b></div>
-...
+    >>> destination_url = re.search(
+    ...     '(?m)^Location:\s(.*)$', str(result)).group(1)
+    >>> launchpad_session_cookie = re.search(
+    ...     '(?m)^Set-Cookie:\slaunchpad_tests=(.*?);', str(result)).group(1)
+    >>> print(http(r"""
+    ... GET %(destination_url)s HTTP/1.1
+    ... Authorization: Basic Y2FybG9zQGNhbm9uaWNhbC5jb206dGVzdA==
+    ... Cookie: launchpad_tests=%(launchpad_session_cookie)s
+    ... """ % vars()))
+    HTTP/1.1 200 Ok
+    ...
+    ...<div class="error message">+notificationtest4 error</div>
+    ...<div class="error message">+notificationtest3 error</div>
+    ...<div class="error message">Error notification <b>1</b></div>
+    ...<div class="error message">Error notification <b>2</b></div>
+    ...<div class="warning message">Warning notification <b>1</b></div>
+    ...<div class="warning message">Warning notification <b>2</b></div>
+    ...<div class="informational message">Info notification <b>1</b></div>
+    ...<div class="informational message">Info notification <b>2</b></div>
+    ...<div class="debug message">Debug notification <b>1</b></div>
+    ...<div class="debug message">Debug notification <b>2</b></div>
+    ...
diff --git a/lib/lp/app/stories/basics/xx-offsite-form-post.txt b/lib/lp/app/stories/basics/xx-offsite-form-post.txt
index 774b5d6..f00a78e 100644
--- a/lib/lp/app/stories/basics/xx-offsite-form-post.txt
+++ b/lib/lp/app/stories/basics/xx-offsite-form-post.txt
@@ -9,137 +9,137 @@ header automatically.  We need to poke into the internals of
 zope.testbrowser.browser.Browser here because it doesn't expose the required
 functionality:
 
-  >>> from contextlib import contextmanager
-  >>> from lp.testing.pages import Browser
-
-  >>> class BrowserWithReferrer(Browser):
-  ...     def __init__(self, referrer):
-  ...         self._referrer = referrer
-  ...         super(BrowserWithReferrer, self).__init__()
-  ...
-  ...     @contextmanager
-  ...     def _preparedRequest(self, url):
-  ...         with super(BrowserWithReferrer, self)._preparedRequest(
-  ...                 url) as reqargs:
-  ...             reqargs["headers"] = [
-  ...                 (key, value) for key, value in reqargs["headers"]
-  ...                 if key.lower() != "referer"]
-  ...             if self._referrer is not None:
-  ...                 reqargs["headers"].append(("Referer", self._referrer))
-  ...             yield reqargs
-
-  >>> def setupBrowserWithReferrer(referrer):
-  ...     browser = BrowserWithReferrer(referrer)
-  ...     browser.handleErrors = False
-  ...     browser.addHeader(
-  ...         "Authorization", "Basic no-priv@xxxxxxxxxxxxx:test")
-  ...     return browser
+    >>> from contextlib import contextmanager
+    >>> from lp.testing.pages import Browser
+
+    >>> class BrowserWithReferrer(Browser):
+    ...     def __init__(self, referrer):
+    ...         self._referrer = referrer
+    ...         super(BrowserWithReferrer, self).__init__()
+    ...
+    ...     @contextmanager
+    ...     def _preparedRequest(self, url):
+    ...         with super(BrowserWithReferrer, self)._preparedRequest(
+    ...                 url) as reqargs:
+    ...             reqargs["headers"] = [
+    ...                 (key, value) for key, value in reqargs["headers"]
+    ...                 if key.lower() != "referer"]
+    ...             if self._referrer is not None:
+    ...                 reqargs["headers"].append(("Referer", self._referrer))
+    ...             yield reqargs
+
+    >>> def setupBrowserWithReferrer(referrer):
+    ...     browser = BrowserWithReferrer(referrer)
+    ...     browser.handleErrors = False
+    ...     browser.addHeader(
+    ...         "Authorization", "Basic no-priv@xxxxxxxxxxxxx:test")
+    ...     return browser
 
 
 If we try to create a new team with with the referrer set to
 "evil.people.com", the post fails:
 
-  >>> browser = setupBrowserWithReferrer('http://evil.people.com/')
-  >>> browser.open('http://launchpad.test/people/+newteam')
-  >>> browser.getControl('Name', index=0).value = 'team1'
-  >>> browser.getControl('Display Name').value = 'Team 1'
-  >>> browser.getControl('Create').click()
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
+    >>> browser = setupBrowserWithReferrer('http://evil.people.com/')
+    >>> browser.open('http://launchpad.test/people/+newteam')
+    >>> browser.getControl('Name', index=0).value = 'team1'
+    >>> browser.getControl('Display Name').value = 'Team 1'
+    >>> browser.getControl('Create').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
 
 
 Similarly, posting with a garbage referer fails:
 
-  >>> browser = setupBrowserWithReferrer('not a url')
-  >>> browser.open('http://launchpad.test/people/+newteam')
-  >>> browser.getControl('Name', index=0).value = 'team2'
-  >>> browser.getControl('Display Name').value = 'Team 2'
-  >>> browser.getControl('Create').click()
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.services.webapp.interfaces.OffsiteFormPostError: not a url
+    >>> browser = setupBrowserWithReferrer('not a url')
+    >>> browser.open('http://launchpad.test/people/+newteam')
+    >>> browser.getControl('Name', index=0).value = 'team2'
+    >>> browser.getControl('Display Name').value = 'Team 2'
+    >>> browser.getControl('Create').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.services.webapp.interfaces.OffsiteFormPostError: not a url
 
 
 It also fails if there is no referrer.
 
-  >>> browser = setupBrowserWithReferrer(None)
-  >>> browser.open('http://launchpad.test/people/+newteam')
-  >>> browser.getControl('Name', index=0).value = 'team3'
-  >>> browser.getControl('Display Name').value = 'Team 3'
-  >>> browser.getControl('Create').click()
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
+    >>> browser = setupBrowserWithReferrer(None)
+    >>> browser.open('http://launchpad.test/people/+newteam')
+    >>> browser.getControl('Name', index=0).value = 'team3'
+    >>> browser.getControl('Display Name').value = 'Team 3'
+    >>> browser.getControl('Create').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
 
 When a POST request is rejected because the REFERER header is missing, it
 may be because the user is trying to enforce anonymity.  Therefore, we
 present a hopefully helpful error message.
 
-  >>> browser.handleErrors = True
-  >>> browser.open('http://launchpad.test/people/+newteam')
-  >>> browser.getControl('Name', index=0).value = 'team3'
-  >>> browser.getControl('Display Name').value = 'Team 3'
-  >>> browser.getControl('Create').click()
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
+    >>> browser.handleErrors = True
+    >>> browser.open('http://launchpad.test/people/+newteam')
+    >>> browser.getControl('Name', index=0).value = 'team3'
+    >>> browser.getControl('Display Name').value = 'Team 3'
+    >>> browser.getControl('Create').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    urllib.error.HTTPError: ...
+    >>> print(browser.headers['status'])
+    403 Forbidden
+    >>> print(extract_text(find_main_content(browser.contents)))
+    No REFERER Header
     ...
-  urllib.error.HTTPError: ...
-  >>> print(browser.headers['status'])
-  403 Forbidden
-  >>> print(extract_text(find_main_content(browser.contents)))
-  No REFERER Header
-  ...
-  >>> browser.getLink('the FAQ').url
-  'https://answers.launchpad.net/launchpad/+faq/1024'
-  >>> browser.handleErrors = False
+    >>> browser.getLink('the FAQ').url
+    'https://answers.launchpad.net/launchpad/+faq/1024'
+    >>> browser.handleErrors = False
 
 We have a few exceptional cases in which we allow POST requests without a
 REFERER header.
 
 To support apport, we allow it for +storeblob.
 
-  >>> browser.post('http://launchpad.test/+storeblob', 'x=1')
+    >>> browser.post('http://launchpad.test/+storeblob', 'x=1')
 
 To support old versions of launchpadlib, we also let POST requests
 without a REFERER header go through to +request-token and
 +access-token.
 
-  >>> body = ('oauth_signature=%26&oauth_consumer_key=test'
-  ...         '&oauth_signature_method=PLAINTEXT')
-  >>> browser.post('http://launchpad.test/+request-token', body)
+    >>> body = ('oauth_signature=%26&oauth_consumer_key=test'
+    ...         '&oauth_signature_method=PLAINTEXT')
+    >>> browser.post('http://launchpad.test/+request-token', body)
 
 This request results in a response code of 401, but if there was no
 exception for +access-token, it would result in an
 OffsiteFormPostError.
 
-  >>> browser.post('http://launchpad.test/+access-token', 'x=1')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  urllib.error.HTTPError: HTTP Error 401: Unauthorized
+    >>> browser.post('http://launchpad.test/+access-token', 'x=1')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    urllib.error.HTTPError: HTTP Error 401: Unauthorized
 
 We also let the request go through if the referrer is from a site
 managed by launchpad:
 
-  # Go behind the curtains and change the hostname of one of our sites so that
-  # we can test this.
-  >>> from lp.services.webapp.vhosts import allvhosts
-  >>> allvhosts._hostnames.add('bzr.dev')
+    # Go behind the curtains and change the hostname of one of our sites so that
+    # we can test this.
+    >>> from lp.services.webapp.vhosts import allvhosts
+    >>> allvhosts._hostnames.add('bzr.dev')
 
-  >>> browser = setupBrowserWithReferrer('http://bzr.dev')
-  >>> browser.open('http://launchpad.test/people/+newteam')
-  >>> browser.getControl('Name', index=0).value = 'team4'
-  >>> browser.getControl('Display Name').value = 'Team 4'
-  >>> browser.getControl('Create').click()
-  >>> print(browser.url)
-  http://launchpad.test/~team4
+    >>> browser = setupBrowserWithReferrer('http://bzr.dev')
+    >>> browser.open('http://launchpad.test/people/+newteam')
+    >>> browser.getControl('Name', index=0).value = 'team4'
+    >>> browser.getControl('Display Name').value = 'Team 4'
+    >>> browser.getControl('Create').click()
+    >>> print(browser.url)
+    http://launchpad.test/~team4
 
-  # Now restore our site's hostname.
-  >>> allvhosts._hostnames.remove('bzr.dev')
+    # Now restore our site's hostname.
+    >>> allvhosts._hostnames.remove('bzr.dev')
 
 Cheaters never prosper
 ----------------------
@@ -149,38 +149,41 @@ specially crafted requests to bypass the referrer check. None of these
 crafted requests work anymore. For instance, you can't cheat by making
 a referrerless POST request to the browser-accessible API.
 
-  >>> browser = setupBrowserWithReferrer('http://evil.people.com/')
-  >>> no_referrer_browser = setupBrowserWithReferrer(None)
+    >>> browser = setupBrowserWithReferrer('http://evil.people.com/')
+    >>> no_referrer_browser = setupBrowserWithReferrer(None)
 
-  >>> browser.post('http://launchpad.test/api/devel/people', 'ws.op=foo&x=1')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
+    >>> browser.post(
+    ...     'http://launchpad.test/api/devel/people', 'ws.op=foo&x=1')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
 
-  >>> no_referrer_browser.post(
-  ...     'http://launchpad.test/api/devel/people', 'ws.op=foo&x=1')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
+    >>> no_referrer_browser.post(
+    ...     'http://launchpad.test/api/devel/people', 'ws.op=foo&x=1')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
 
 You can't cheat by making your referrerless POST request seem as
 though it were signed with OAuth.
 
-  >>> browser.post(
-  ...     'http://launchpad.test/', 'oauth_consumer_key=foo&oauth_token=bar')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
+    >>> browser.post(
+    ...     'http://launchpad.test/',
+    ...     'oauth_consumer_key=foo&oauth_token=bar')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
 
-  >>> no_referrer_browser.post(
-  ...     'http://launchpad.test/', 'oauth_consumer_key=foo&oauth_token=bar')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
+    >>> no_referrer_browser.post(
+    ...     'http://launchpad.test/',
+    ...     'oauth_consumer_key=foo&oauth_token=bar')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
 
 You might think you can actually sign a request with an anonymous
 OAuth credential. You don't need any knowledge of the user account to
@@ -188,28 +191,28 @@ create an anonymous signature, and you don't need to use the name of
 an existing consumer. Maybe the signature will make your request look
 enough like an anonymous OAuth request to bypass the referrer check.
 
-  >>> sig = ('ws.op=new_project&display_name=a&name=bproj&summary=c&title=d'
-  ...        '&oauth_nonce=x&oauth_timestamp=y&oauth_consumer_key=key'
-  ...        '&oauth_signature_method=PLAINTEXT&oauth_version=1.0'
-  ...        '&oauth_token=&oauth_signature=%26')
+    >>> sig = ('ws.op=new_project&display_name=a&name=bproj&summary=c&title=d'
+    ...        '&oauth_nonce=x&oauth_timestamp=y&oauth_consumer_key=key'
+    ...        '&oauth_signature_method=PLAINTEXT&oauth_version=1.0'
+    ...        '&oauth_token=&oauth_signature=%26')
 
 But the browser-accessible API ignores OAuth credentials altogether.
 
-  >>> browser.post(
-  ...     'http://launchpad.test/api/devel/projects', sig)
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
+    >>> browser.post(
+    ...     'http://launchpad.test/api/devel/projects', sig)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
 
 If you go through the 'api' vhost, the signed request will be
 processed despite the bogus referrer, but...
 
-  >>> browser.post('http://api.launchpad.test/devel/projects', sig)
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  storm.exceptions.NoneError: None isn't acceptable as a value for Product...
+    >>> browser.post('http://api.launchpad.test/devel/projects', sig)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    storm.exceptions.NoneError: None isn't acceptable as a value for Product...
 
 You're making an _anonymous_ request. That's a request that 1) is not
 associated with any Launchpad user account (thus the NoneError when
diff --git a/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt b/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt
index 3f1771a..060a023 100644
--- a/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt
+++ b/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt
@@ -5,7 +5,7 @@ Featured Projects
 We maintain a list of featured projects, which are displayed on the home
 page and managed via a special admin-only page.
 
-   >>> MANAGE_LINK = "Manage featured project list"
+    >>> MANAGE_LINK = "Manage featured project list"
 
 
 The home page listing
diff --git a/lib/lp/app/widgets/doc/lower-case-text-widget.txt b/lib/lp/app/widgets/doc/lower-case-text-widget.txt
index d9a0237..15ed121 100644
--- a/lib/lp/app/widgets/doc/lower-case-text-widget.txt
+++ b/lib/lp/app/widgets/doc/lower-case-text-widget.txt
@@ -8,29 +8,29 @@ error message when the user inputs an upper case string, a
 LowerCaseTextWidget can be used to automatically convert the input to
 lower case:
 
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.app.widgets.textwidgets import LowerCaseTextWidget
-  >>> from lp.bugs.interfaces.bug import IBug
-  >>> field = IBug['description']
-  >>> request = LaunchpadTestRequest(form={'field.description':'Foo'})
-  >>> widget = LowerCaseTextWidget(field, request)
-  >>> print(widget.getInputValue())
-  foo
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.app.widgets.textwidgets import LowerCaseTextWidget
+    >>> from lp.bugs.interfaces.bug import IBug
+    >>> field = IBug['description']
+    >>> request = LaunchpadTestRequest(form={'field.description':'Foo'})
+    >>> widget = LowerCaseTextWidget(field, request)
+    >>> print(widget.getInputValue())
+    foo
 
 However, strings without lower case characters are left unchanged:
 
-  >>> field = IBug['description']
-  >>> request = LaunchpadTestRequest(form={'field.description':'foo1'})
-  >>> widget = LowerCaseTextWidget(field, request)
-  >>> print(widget.getInputValue())
-  foo1
+    >>> field = IBug['description']
+    >>> request = LaunchpadTestRequest(form={'field.description':'foo1'})
+    >>> widget = LowerCaseTextWidget(field, request)
+    >>> print(widget.getInputValue())
+    foo1
 
 In addition, the widget also renders itself with a CSS style that causes
 characters to be rendered in lower case as they are typed in by the
 user:
 
-  >>> widget.cssClass
-  'lowerCaseText'
+    >>> widget.cssClass
+    'lowerCaseText'
 
 This style is defined by "lib/canonical/launchpad/icing/style.css". Note
 that the style only causes text to be rendered in lower case, and does
diff --git a/lib/lp/app/widgets/doc/stripped-text-widget.txt b/lib/lp/app/widgets/doc/stripped-text-widget.txt
index 354fe5f..7648db0 100644
--- a/lib/lp/app/widgets/doc/stripped-text-widget.txt
+++ b/lib/lp/app/widgets/doc/stripped-text-widget.txt
@@ -30,37 +30,37 @@ StrippedTextLine Widget
 
 This custom widget is used to strip leading and trailing whitespaces.
 
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.app.widgets.textwidgets import StrippedTextWidget
-  >>> from lp.bugs.interfaces.bugtracker import IRemoteBug
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.app.widgets.textwidgets import StrippedTextWidget
+    >>> from lp.bugs.interfaces.bugtracker import IRemoteBug
 
 We pass a string with leading and trailing whitespaces to the widget
 
-  >>> field = IRemoteBug['remotebug']
-  >>> request = LaunchpadTestRequest(
-  ...     form={'field.remotebug':'    123456    '})
-  >>> widget = StrippedTextWidget(field, request)
+    >>> field = IRemoteBug['remotebug']
+    >>> request = LaunchpadTestRequest(
+    ...     form={'field.remotebug':'    123456    '})
+    >>> widget = StrippedTextWidget(field, request)
 
 And check that the leading and trailing whitespaces were correctly stripped.
 
-  >>> print(widget.getInputValue())
-  123456
+    >>> print(widget.getInputValue())
+    123456
 
 If only whitespace is provided, the widget acts like no input was
 provided.
 
-  >>> non_required_field.missing_value is None
-  True
-  >>> request = LaunchpadTestRequest(form={'field.field':'    \n    '})
-  >>> widget = StrippedTextWidget(non_required_field, request)
-  >>> widget.getInputValue() is None
-  True
-
-  >>> required_field = StrippedTextLine(
-  ...     __name__=six.ensure_str('field'), title=u'Title', required=True)
-  >>> request = LaunchpadTestRequest(form={'field.field':'    \n    '})
-  >>> widget = StrippedTextWidget(required_field, request)
-  >>> widget.getInputValue()  # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.formlib.interfaces.WidgetInputError: ('field', ...'Title', RequiredMissing('field'))
+    >>> non_required_field.missing_value is None
+    True
+    >>> request = LaunchpadTestRequest(form={'field.field':'    \n    '})
+    >>> widget = StrippedTextWidget(non_required_field, request)
+    >>> widget.getInputValue() is None
+    True
+
+    >>> required_field = StrippedTextLine(
+    ...     __name__=six.ensure_str('field'), title=u'Title', required=True)
+    >>> request = LaunchpadTestRequest(form={'field.field':'    \n    '})
+    >>> widget = StrippedTextWidget(required_field, request)
+    >>> widget.getInputValue()  # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.formlib.interfaces.WidgetInputError: ('field', ...'Title', RequiredMissing('field'))
diff --git a/lib/lp/blueprints/stories/blueprints/xx-dependencies.txt b/lib/lp/blueprints/stories/blueprints/xx-dependencies.txt
index 9021466..39b6a01 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-dependencies.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-dependencies.txt
@@ -9,14 +9,14 @@ Let's look at the dependencies of the "canvas" blueprint for Firefox. It
 depends on another blueprint, "e4x". No blueprints depend on "canvas"
 itself.
 
-  >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
-  >>> owner_browser.open(
-  ...   'http://blueprints.launchpad.test/firefox/+spec/canvas')
-  >>> print(find_main_content(owner_browser.contents))
-  <...
-  ...Support E4X in EcmaScript...
-  >>> 'Blocks' not in (owner_browser.contents)
-  True
+    >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
+    >>> owner_browser.open(
+    ...   'http://blueprints.launchpad.test/firefox/+spec/canvas')
+    >>> print(find_main_content(owner_browser.contents))
+    <...
+    ...Support E4X in EcmaScript...
+    >>> 'Blocks' not in (owner_browser.contents)
+    True
 
 
 Adding a new dependency
@@ -26,31 +26,32 @@ Let's add a new dependency for the "canvas" blueprint. We'll add the
 "extension-manager-upgrades" blueprint as a dependency. First, we
 confirm we can see the page for adding a dependency.
 
-  >>> owner_browser.getLink('Add dependency').click()
-  >>> owner_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas/+linkdependency'
+    >>> owner_browser.getLink('Add dependency').click()
+    >>> owner_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas/+linkdependency'
 
 One can decide not to add a dependency after all.
 
-  >>> owner_browser.getLink('Cancel').url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas'
+    >>> owner_browser.getLink('Cancel').url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas'
 
 This +linkdependency page and the link to it are only accessible by
 users with launchpad.Edit permission for the blueprint.
 
-  >>> user_browser.open(
-  ...   'http://blueprints.launchpad.test/firefox/+spec/canvas')
-  >>> user_browser.getLink('Add dependency')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.testbrowser.browser.LinkNotFoundError
-  >>> user_browser.open(
-  ...  'http://blueprints.launchpad.test/firefox/+spec/canvas/+linkdependency')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.security.interfaces.Unauthorized: ...
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/+spec/canvas')
+    >>> user_browser.getLink('Add dependency')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.testbrowser.browser.LinkNotFoundError
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/+spec/canvas/'
+    ...     '+linkdependency')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.security.interfaces.Unauthorized: ...
 
 The page contains a link back to the blueprint, in case we change our
 minds.
@@ -62,11 +63,11 @@ minds.
 Now, lets POST the form, saying we want extension-manager-upgrades as the
 dependency.
 
-  >>> owner_browser.getControl(
-  ...     'Depends On').value = 'extension-manager-upgrades'
-  >>> owner_browser.getControl('Continue').click()
-  >>> owner_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas'
+    >>> owner_browser.getControl(
+    ...     'Depends On').value = 'extension-manager-upgrades'
+    >>> owner_browser.getControl('Continue').click()
+    >>> owner_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas'
 
 
 Removing a dependency
@@ -76,20 +77,20 @@ But we don't want to keep that, so we will remove it as a dependency. First
 we make sure we can see the link to remove a dependency. We need to be
 authenticated.
 
-  >>> owner_browser.getLink('Remove dependency').click()
-  >>> owner_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas/+removedependency'
+    >>> owner_browser.getLink('Remove dependency').click()
+    >>> owner_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas/+removedependency'
 
 One can decide not to remove a dependency after all.
 
-  >>> owner_browser.getLink('Cancel').url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas'
+    >>> owner_browser.getLink('Cancel').url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas'
 
 Now, we make sure we can load the page. It should show two potential
 dependencies we could remove. The extension manager one, and "e4x".
 
-  >>> owner_browser.getControl('Dependency').displayOptions
-  ['Extension Manager Upgrades', 'Support E4X in EcmaScript']
+    >>> owner_browser.getControl('Dependency').displayOptions
+    ['Extension Manager Upgrades', 'Support E4X in EcmaScript']
 
 Again, the page contains a link back to the blueprint in case we change
 our mind.
@@ -101,28 +102,29 @@ our mind.
 We'll POST the form selecting "extension-manager-upgrades" for removal. We
 expect to be redirected to the blueprint page.
 
-  >>> owner_browser.getControl(
-  ...     'Dependency').value = ['1']
-  >>> owner_browser.getControl('Continue').click()
-  >>> owner_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas'
+    >>> owner_browser.getControl(
+    ...     'Dependency').value = ['1']
+    >>> owner_browser.getControl('Continue').click()
+    >>> owner_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas'
 
 This +removedependency page and the link to it are only accessible by
 users with launchpad.Edit permission for the blueprint.
 
-  >>> user_browser.open(
-  ...   'http://blueprints.launchpad.test/firefox/+spec/canvas')
-  >>> user_browser.getLink('Remove dependency')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.testbrowser.browser.LinkNotFoundError
-  >>> user_browser.open(
-  ...   'http://blueprints.launchpad.test/firefox/+spec/canvas/+removedependency')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.security.interfaces.Unauthorized: ...
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/+spec/canvas')
+    >>> user_browser.getLink('Remove dependency')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.testbrowser.browser.LinkNotFoundError
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/+spec/canvas/'
+    ...     '+removedependency')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.security.interfaces.Unauthorized: ...
 
 
 Corner cases
@@ -134,27 +136,28 @@ Cross-project blueprints
 Blueprints can only depend on blueprints in the same project. To
 show this, we register a blueprint for a different project.
 
-  >>> owner_browser.open(
-  ...     'http://blueprints.launchpad.test/jokosher/+addspec')
-  >>> owner_browser.getControl('Name').value = 'test-blueprint'
-  >>> owner_browser.getControl('Title').value = 'Test Blueprint'
-  >>> owner_browser.getControl('Summary').value = (
-  ...     'Another blueprint in a different project')
-  >>> owner_browser.getControl('Register Blueprint').click()
-  >>> owner_browser.url
-  'http://blueprints.launchpad.test/jokosher/+spec/test-blueprint'
+    >>> owner_browser.open(
+    ...     'http://blueprints.launchpad.test/jokosher/+addspec')
+    >>> owner_browser.getControl('Name').value = 'test-blueprint'
+    >>> owner_browser.getControl('Title').value = 'Test Blueprint'
+    >>> owner_browser.getControl('Summary').value = (
+    ...     'Another blueprint in a different project')
+    >>> owner_browser.getControl('Register Blueprint').click()
+    >>> owner_browser.url
+    'http://blueprints.launchpad.test/jokosher/+spec/test-blueprint'
 
 We then try to make the canvas blueprint in firefox depend on the
 blueprint we registered in jokosher.
 
-  >>> owner_browser.open(
-  ...     'http://blueprints.launchpad.test/firefox/'
-  ...     '+spec/canvas/+linkdependency')
-  >>> owner_browser.getControl(
-  ...     'Depends On').value = 'test-blueprint'
-  >>> owner_browser.getControl('Continue').click()
-  >>> 'no blueprint named &quot;test-blueprint&quot;' in owner_browser.contents
-  True
+    >>> owner_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/'
+    ...     '+spec/canvas/+linkdependency')
+    >>> owner_browser.getControl(
+    ...     'Depends On').value = 'test-blueprint'
+    >>> owner_browser.getControl('Continue').click()
+    >>> 'no blueprint named &quot;test-blueprint&quot;' in (
+    ...     owner_browser.contents)
+    True
 
 
 Circular dependencies
@@ -167,13 +170,14 @@ depending on A.
 We know that "canvas" depends on "e4x". We try to make "e4x" depend on
 "canvas".
 
-  >>> owner_browser.open(
-  ...     'http://blueprints.launchpad.test/firefox/+spec/e4x/+linkdependency')
-  >>> owner_browser.getControl(
-  ...     'Depends On').value = 'canvas'
-  >>> owner_browser.getControl('Continue').click()
-  >>> 'no blueprint named &quot;canvas&quot;' in owner_browser.contents
-  True
+    >>> owner_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/+spec/e4x/'
+    ...     '+linkdependency')
+    >>> owner_browser.getControl(
+    ...     'Depends On').value = 'canvas'
+    >>> owner_browser.getControl('Continue').click()
+    >>> 'no blueprint named &quot;canvas&quot;' in owner_browser.contents
+    True
 
 
 Status
@@ -182,25 +186,25 @@ Status
 It should be possible to indicate any blueprint as a dependency,
 regardless of its status. Let's mark mergewin as Implemented:
 
-  >>> owner_browser.open(
-  ...   'http://blueprints.launchpad.test/firefox/+spec/mergewin')
-  >>> owner_browser.getLink(url='+status').click()
-  >>> owner_browser.getControl(
-  ...     'Implementation Status').value = ['IMPLEMENTED']
-  >>> owner_browser.getControl('Change').click()
-  >>> owner_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/mergewin'
+    >>> owner_browser.open(
+    ...   'http://blueprints.launchpad.test/firefox/+spec/mergewin')
+    >>> owner_browser.getLink(url='+status').click()
+    >>> owner_browser.getControl(
+    ...     'Implementation Status').value = ['IMPLEMENTED']
+    >>> owner_browser.getControl('Change').click()
+    >>> owner_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/mergewin'
 
 And ensure it works:
 
-  >>> owner_browser.open(
-  ...   'http://blueprints.launchpad.test/firefox/+spec/canvas')
-  >>> owner_browser.getLink('Add dependency').click()
-  >>> owner_browser.getControl(
-  ...     'Depends On').value = 'mergewin'
-  >>> owner_browser.getControl('Continue').click()
-  >>> owner_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas'
+    >>> owner_browser.open(
+    ...   'http://blueprints.launchpad.test/firefox/+spec/canvas')
+    >>> owner_browser.getLink('Add dependency').click()
+    >>> owner_browser.getControl(
+    ...     'Depends On').value = 'mergewin'
+    >>> owner_browser.getControl('Continue').click()
+    >>> owner_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas'
 
 
 Project dependency charts
@@ -213,56 +217,57 @@ it can be implemented, and nothing depends on having "canvas"
 implemented. The "dependency tree" page for "canvas" should show exactly
 that.
 
-  >>> anon_browser.open('http://launchpad.test/firefox/+spec/canvas/+deptree')
-  >>> print('----'); print(anon_browser.contents)
-  ----
-  ...Blueprints that must be implemented first...
-  ...Support E4X in EcmaScript...
-  ...Merge Open Browser Windows with "Consolidate Windows"...
-  ...Support Native SVG Objects...
-  ...This blueprint...
-  ...Support &lt;canvas&gt; Objects...
-  ...Blueprints that can then be implemented...
-  ...No blueprints depend on this one...
+    >>> anon_browser.open(
+    ...     'http://launchpad.test/firefox/+spec/canvas/+deptree')
+    >>> print('----'); print(anon_browser.contents)
+    ----
+    ...Blueprints that must be implemented first...
+    ...Support E4X in EcmaScript...
+    ...Merge Open Browser Windows with "Consolidate Windows"...
+    ...Support Native SVG Objects...
+    ...This blueprint...
+    ...Support &lt;canvas&gt; Objects...
+    ...Blueprints that can then be implemented...
+    ...No blueprints depend on this one...
 
 We have some nice tools to display the dependency tree as a client side
 image and map.
 
-  >>> anon_browser.open(
-  ...   'http://launchpad.test/firefox/+spec/canvas/+deptreeimgtag')
-  >>> print(anon_browser.contents)
-  <img src="deptree.png" usemap="#deptree" />
-  <map id="deptree" name="deptree">
-  <area shape="poly"
-    ...title="Support &lt;canvas&gt; Objects" .../>
-  <area shape="poly"
-    ...href="http://blueprints.launchpad.test/firefox/+spec/e4x"; .../>
-  <area shape="poly"
-    ...href="http://blueprints.launchpad.test/firefox/+spec/mergewin"; .../>
-  <area shape="poly"
-    ...href="http://blueprints.launchpad.test/firefox/+spec/svg...support"; .../>
-  </map>
+    >>> anon_browser.open(
+    ...     'http://launchpad.test/firefox/+spec/canvas/+deptreeimgtag')
+    >>> print(anon_browser.contents)
+    <img src="deptree.png" usemap="#deptree" />
+    <map id="deptree" name="deptree">
+    <area shape="poly"
+      ...title="Support &lt;canvas&gt; Objects" .../>
+    <area shape="poly"
+      ...href="http://blueprints.launchpad.test/firefox/+spec/e4x"; .../>
+    <area shape="poly"
+      ...href="http://blueprints.launchpad.test/firefox/+spec/mergewin"; .../>
+    <area shape="poly"
+      ...href="http://blueprints.launchpad.test/firefox/+spec/svg...support"; .../>
+    </map>
 
 
 Get the dependency chart, and check that it is a PNG.
 
-  >>> anon_browser.open(
-  ...   'http://launchpad.test/firefox/+spec/canvas/deptree.png')
-  >>> anon_browser.contents.startswith(b'\x89PNG')
-  True
-  >>> anon_browser.headers['content-type']
-  'image/png'
+    >>> anon_browser.open(
+    ...   'http://launchpad.test/firefox/+spec/canvas/deptree.png')
+    >>> anon_browser.contents.startswith(b'\x89PNG')
+    True
+    >>> anon_browser.headers['content-type']
+    'image/png'
 
 We can also get the DOT output for a blueprint dependency graph.  This
 is useful for experimenting with the dot layout using production data.
 
-  >>> anon_browser.open(
-  ...   'http://launchpad.test/firefox/+spec/canvas/+deptreedotfile')
-  >>> anon_browser.headers['content-type']
-  'text/plain;charset=utf-8'
-  >>> print(anon_browser.contents)
-  digraph "deptree" {
-  ...
+    >>> anon_browser.open(
+    ...   'http://launchpad.test/firefox/+spec/canvas/+deptreedotfile')
+    >>> anon_browser.headers['content-type']
+    'text/plain;charset=utf-8'
+    >>> print(anon_browser.contents)
+    digraph "deptree" {
+    ...
 
 Distro blueprints
 -----------------
@@ -270,35 +275,34 @@ Distro blueprints
 Let's look at blueprints targetting a distribution, rather than a product.
 We create two blueprints in `ubuntu`.
 
-  >>> owner_browser.open('http://blueprints.launchpad.test/ubuntu/+addspec')
-  >>> owner_browser.getControl('Name').value = 'distro-blueprint-a'
-  >>> owner_browser.getControl('Title').value = 'A blueprint for a distro'
-  >>> owner_browser.getControl('Summary').value = (
-  ...     'This is a blueprint for the Ubuntu distribution')
-  >>> owner_browser.getControl('Register Blueprint').click()
-  >>> print(owner_browser.url)
-  http://blueprints.launchpad.test/ubuntu/+spec/distro-blueprint-a
-
-  >>> owner_browser.open('http://blueprints.launchpad.test/ubuntu/+addspec')
-  >>> owner_browser.getControl('Name').value = 'distro-blueprint-b'
-  >>> owner_browser.getControl('Title').value = (
-  ...     'Another blueprint for a distro')
-  >>> owner_browser.getControl('Summary').value = (
-  ...     'This is a blueprint for the Ubuntu distribution')
-  >>> owner_browser.getControl('Register Blueprint').click()
-  >>> print(owner_browser.url)
-  http://blueprints.launchpad.test/ubuntu/+spec/distro-blueprint-b
-
-  >>> owner_browser.getLink('Add dependency').click()
-  >>> print(owner_browser.url)
-  http.../ubuntu/+spec/distro-blueprint-b/+linkdependency
-  
-  >>> owner_browser.getControl('Depends On').value = 'distro-blueprint-a'
-  >>> owner_browser.getControl('Continue').click()
+    >>> owner_browser.open('http://blueprints.launchpad.test/ubuntu/+addspec')
+    >>> owner_browser.getControl('Name').value = 'distro-blueprint-a'
+    >>> owner_browser.getControl('Title').value = 'A blueprint for a distro'
+    >>> owner_browser.getControl('Summary').value = (
+    ...     'This is a blueprint for the Ubuntu distribution')
+    >>> owner_browser.getControl('Register Blueprint').click()
+    >>> print(owner_browser.url)
+    http://blueprints.launchpad.test/ubuntu/+spec/distro-blueprint-a
+
+    >>> owner_browser.open('http://blueprints.launchpad.test/ubuntu/+addspec')
+    >>> owner_browser.getControl('Name').value = 'distro-blueprint-b'
+    >>> owner_browser.getControl('Title').value = (
+    ...     'Another blueprint for a distro')
+    >>> owner_browser.getControl('Summary').value = (
+    ...     'This is a blueprint for the Ubuntu distribution')
+    >>> owner_browser.getControl('Register Blueprint').click()
+    >>> print(owner_browser.url)
+    http://blueprints.launchpad.test/ubuntu/+spec/distro-blueprint-b
+
+    >>> owner_browser.getLink('Add dependency').click()
+    >>> print(owner_browser.url)
+    http.../ubuntu/+spec/distro-blueprint-b/+linkdependency
+
+    >>> owner_browser.getControl('Depends On').value = 'distro-blueprint-a'
+    >>> owner_browser.getControl('Continue').click()
 
 The blueprint was linked successfully, and it appears in the dependency
 image map.
 
-  >>> find_tag_by_id(owner_browser.contents, 'deptree')
-  <...A blueprint for a distro...>
-
+    >>> find_tag_by_id(owner_browser.contents, 'deptree')
+    <...A blueprint for a distro...>
diff --git a/lib/lp/blueprints/stories/blueprints/xx-distrorelease.txt b/lib/lp/blueprints/stories/blueprints/xx-distrorelease.txt
index af5e43f..d96e9c2 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-distrorelease.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-distrorelease.txt
@@ -6,39 +6,42 @@ to the Grumpy distroseries.
 
 First we try to access the addspec page.
 
-  >>> user_browser = browser
-  >>> user_browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
-  >>> url = 'http://blueprints.launchpad.test/ubuntu/+addspec'
-  >>> user_browser.open(url)
-  >>> user_browser.url
-  'http://blueprints.launchpad.test/ubuntu/+addspec'
+    >>> user_browser = browser
+    >>> user_browser.addHeader(
+    ...     'Authorization', 'Basic test@xxxxxxxxxxxxx:test')
+    >>> url = 'http://blueprints.launchpad.test/ubuntu/+addspec'
+    >>> user_browser.open(url)
+    >>> user_browser.url
+    'http://blueprints.launchpad.test/ubuntu/+addspec'
 
 Then we try to add a specification to that distro
 
-  >>> user_browser.getControl('Name').value = "testspec"
-  >>> user_browser.getControl('Title').value = "Test Specification"
-  >>> user_browser.getControl('Specification URL').value = (
-  ... "http://wiki.test.com";)
-  >>> user_browser.getControl('Summary').value = "TEst spec add"
-  >>> user_browser.getControl('Definition Status').value = ['NEW']
-  >>> user_browser.getControl('Assignee').value = "test@xxxxxxxxxxxxx"
-  >>> user_browser.getControl('Drafter').value = "test@xxxxxxxxxxxxx"
-  >>> user_browser.getControl('Approver').value = "test@xxxxxxxxxxxxx"
-  >>> user_browser.getControl('Register Blueprint').click()
+    >>> user_browser.getControl('Name').value = "testspec"
+    >>> user_browser.getControl('Title').value = "Test Specification"
+    >>> user_browser.getControl('Specification URL').value = (
+    ... "http://wiki.test.com";)
+    >>> user_browser.getControl('Summary').value = "TEst spec add"
+    >>> user_browser.getControl('Definition Status').value = ['NEW']
+    >>> user_browser.getControl('Assignee').value = "test@xxxxxxxxxxxxx"
+    >>> user_browser.getControl('Drafter').value = "test@xxxxxxxxxxxxx"
+    >>> user_browser.getControl('Approver').value = "test@xxxxxxxxxxxxx"
+    >>> user_browser.getControl('Register Blueprint').click()
 
 We're redirected to the Specification page
 
-  >>> user_browser.url
-  'http://blueprints.launchpad.test/ubuntu/+spec/testspec'
+    >>> user_browser.url
+    'http://blueprints.launchpad.test/ubuntu/+spec/testspec'
 
 Now we try to open the +setdistroseries page, where there is a form to
 target the newly created spec to a distribution.
 
 
-  >>> url = 'http://blueprints.launchpad.test/ubuntu/+spec/testspec/+setdistroseries'
-  >>> user_browser.open(url)
-  >>> user_browser.url
-  'http://blueprints.launchpad.test/ubuntu/+spec/testspec/+setdistroseries'
+    >>> url = (
+    ...     'http://blueprints.launchpad.test/ubuntu/+spec/testspec/'
+    ...     '+setdistroseries')
+    >>> user_browser.open(url)
+    >>> user_browser.url
+    'http://blueprints.launchpad.test/ubuntu/+spec/testspec/+setdistroseries'
 
 The page contains a link back to the blueprint, in case you change your mind.
 
@@ -51,56 +54,59 @@ The page contains a link back to the blueprint, in case you change your mind.
 We are able to target a specification to a distroseries. We expect to be
 redirected back to the spec page when we are done.
 
-  >>> user_browser.open('http://blueprints.launchpad.test/ubuntu/+spec/media-integrity-check/+setdistroseries')
-  >>> user_browser.url
-  'http://blueprints.launchpad.test/ubuntu/+spec/media-integrity-check/+setdistroseries'
-  >>> user_browser.getControl('Goal').value = ['5']
-  >>> user_browser.getControl('Continue').click()
-  >>> user_browser.url
-  'http://blueprints.launchpad.test/ubuntu/+spec/media-integrity-check'
- 
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/ubuntu/+spec/'
+    ...     'media-integrity-check/+setdistroseries')
+    >>> user_browser.url
+    'http://blueprints.launchpad.test/ubuntu/+spec/media-integrity-check/+setdistroseries'
+    >>> user_browser.getControl('Goal').value = ['5']
+    >>> user_browser.getControl('Continue').click()
+    >>> user_browser.url
+    'http://blueprints.launchpad.test/ubuntu/+spec/media-integrity-check'
+
 
 After the POST we should see the goal on the specification page, as
 a "proposed" goal.
 
-  >>> "Series goal" in user_browser.contents
-  True
-  >>> "grumpy" in user_browser.contents
-  True
-  >>> "Proposed" in user_browser.contents
-  True
+    >>> "Series goal" in user_browser.contents
+    True
+    >>> "grumpy" in user_browser.contents
+    True
+    >>> "Proposed" in user_browser.contents
+    True
 
 
 The spec will not show up immediately as a Grumpy goal since it must
 first be approved.
 
-  >>> import six
-  >>> result = six.text_type(http(r"""
-  ... GET /ubuntu/hoary/+specs HTTP/1.1
-  ... """))
-  >>> '<td>CD Media Integrity Check' not in result
-  True
+    >>> import six
+    >>> result = six.text_type(http(r"""
+    ... GET /ubuntu/hoary/+specs HTTP/1.1
+    ... """))
+    >>> '<td>CD Media Integrity Check' not in result
+    True
 
 However, we can expect to find it on the approvals page.
 
-  >>> user_browser.open('http://blueprints.launchpad.test/ubuntu/grumpy/+specs')
-  >>> "CD Media Integrity Check" in user_browser.contents
-  False
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/ubuntu/grumpy/+specs')
+    >>> "CD Media Integrity Check" in user_browser.contents
+    False
 
 We will accept it:
 
-  >>> admin_browser.open('http://blueprints.launchpad.test/ubuntu/grumpy/+setgoals')
-  >>> 'CD Media Integrity' in admin_browser.contents
-  True
-  >>> admin_browser.getControl('CD Media Integrity Check').selected = True
-  >>> admin_browser.getControl('Accept').click()
-  >>> admin_browser.url
-  'http://blueprints.launchpad.test/ubuntu/grumpy'
-  >>> 'Accepted 1 specification(s)' in admin_browser.contents
-  True
+    >>> admin_browser.open(
+    ...     'http://blueprints.launchpad.test/ubuntu/grumpy/+setgoals')
+    >>> 'CD Media Integrity' in admin_browser.contents
+    True
+    >>> admin_browser.getControl('CD Media Integrity Check').selected = True
+    >>> admin_browser.getControl('Accept').click()
+    >>> admin_browser.url
+    'http://blueprints.launchpad.test/ubuntu/grumpy'
+    >>> 'Accepted 1 specification(s)' in admin_browser.contents
+    True
 
 And now it should appear on the Grumpy specs list:
 
-  >>> "CD Media Integrity Check" in admin_browser.contents
-  True
-
+    >>> "CD Media Integrity Check" in admin_browser.contents
+    True
diff --git a/lib/lp/blueprints/stories/blueprints/xx-milestones.txt b/lib/lp/blueprints/stories/blueprints/xx-milestones.txt
index 02ff016..171be1d 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-milestones.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-milestones.txt
@@ -12,42 +12,42 @@ the milestone page lists one feature targeted already, and no bugs:
 We'll target the "canvas" blueprint. Each blueprint has a separate page for
 milestone targeting.
 
-  >>> admin_browser.open(
-  ...     'http://blueprints.launchpad.test/firefox/+spec/canvas')
-  >>> admin_browser.getLink('Target milestone').click()
-  >>> print(admin_browser.title)
-  Target to a milestone : Support <canvas> Objects :
-  Blueprints : Mozilla Firefox
-  >>> back_link = admin_browser.getLink('Support <canvas> Objects')
-  >>> back_link.url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas'
+    >>> admin_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/+spec/canvas')
+    >>> admin_browser.getLink('Target milestone').click()
+    >>> print(admin_browser.title)
+    Target to a milestone : Support <canvas> Objects :
+    Blueprints : Mozilla Firefox
+    >>> back_link = admin_browser.getLink('Support <canvas> Objects')
+    >>> back_link.url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas'
 
 Now, we choose a milestone from the list.
 
-  >>> admin_browser.getControl('Milestone').displayOptions
-  ['(nothing selected)', 'Mozilla Firefox 1.0']
-  >>> admin_browser.getControl('Milestone').value = ['1']
-  >>> admin_browser.getControl('Status Whiteboard').value = 'foo'
-  >>> admin_browser.getControl('Change').click()
+    >>> admin_browser.getControl('Milestone').displayOptions
+    ['(nothing selected)', 'Mozilla Firefox 1.0']
+    >>> admin_browser.getControl('Milestone').value = ['1']
+    >>> admin_browser.getControl('Status Whiteboard').value = 'foo'
+    >>> admin_browser.getControl('Change').click()
 
 We expect to be redirected to the spec home page.
 
-  >>> admin_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/canvas'
+    >>> admin_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/canvas'
 
 And on that page, we expect to see that the spec is targeted to the 1.0
 milestone.
 
-  >>> print(find_main_content(admin_browser.contents))
-  <...Milestone target:...
-  <.../firefox/+milestone/1.0...
+    >>> print(find_main_content(admin_browser.contents))
+    <...Milestone target:...
+    <.../firefox/+milestone/1.0...
 
-  >>> print(admin_browser.getLink('1.0').url)
-  http://launchpad.test/firefox/+milestone/1.0
+    >>> print(admin_browser.getLink('1.0').url)
+    http://launchpad.test/firefox/+milestone/1.0
 
-  >>> admin_browser.getLink('1.0').click()
-  >>> print(admin_browser.getLink('Support <canvas> Objects').url)
-  http://blueprints.launchpad.test/firefox/+spec/canvas
+    >>> admin_browser.getLink('1.0').click()
+    >>> print(admin_browser.getLink('Support <canvas> Objects').url)
+    http://blueprints.launchpad.test/firefox/+spec/canvas
 
 The count of targeted features has also updated.
 
diff --git a/lib/lp/blueprints/stories/blueprints/xx-non-ascii-imagemap.txt b/lib/lp/blueprints/stories/blueprints/xx-non-ascii-imagemap.txt
index f9127fa..14bdedb 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-non-ascii-imagemap.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-non-ascii-imagemap.txt
@@ -1,21 +1,22 @@
 Non-ascii characters in specification titles are allowed.
 
-  >>> admin_browser.open(
-  ...     'http://blueprints.launchpad.test/firefox/+spec/e4x/+edit')
+    >>> admin_browser.open(
+    ...     'http://blueprints.launchpad.test/firefox/+spec/e4x/+edit')
 
-  >>> admin_browser.getControl(
-  ...     'Title').value = 'A title with non-ascii characters \xe1\xe3'
-  >>> admin_browser.getControl('Change').click()
-  >>> admin_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/e4x'
+    >>> admin_browser.getControl(
+    ...     'Title').value = 'A title with non-ascii characters \xe1\xe3'
+    >>> admin_browser.getControl('Change').click()
+    >>> admin_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/e4x'
 
 And they're correctly displayed in the dependency graph imagemap.
 
-  >>> anon_browser.open('http://launchpad.test/firefox/+spec/canvas/+deptreeimgtag')
-  >>> print(anon_browser.contents)
-  <img ...
-  <map id="deptree" name="deptree">
-  <area shape="poly" ...title="Support &lt;canvas&gt; Objects" .../>
-  <area shape="poly" ...title="A title with non&#45;ascii characters áã" .../>
-  ...
+    >>> anon_browser.open(
+    ...     'http://launchpad.test/firefox/+spec/canvas/+deptreeimgtag')
+    >>> print(anon_browser.contents)
+    <img ...
+    <map id="deptree" name="deptree">
+    <area shape="poly" ...title="Support &lt;canvas&gt; Objects" .../>
+    <area shape="poly" ...title="A title with non&#45;ascii characters áã" .../>
+    ...
 
diff --git a/lib/lp/blueprints/stories/blueprints/xx-productseries.txt b/lib/lp/blueprints/stories/blueprints/xx-productseries.txt
index 0de589c..2dc8d95 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-productseries.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-productseries.txt
@@ -44,92 +44,92 @@ Now, we POST the form and expect to be redirected to the spec home page.
 Note that we use a user who DOES NOT have the "driver" role on that series,
 so the targeting should NOT be automatically approved.
 
-  >>> print(http(r"""
-  ... POST /firefox/+spec/svg-support/+setproductseries HTTP/1.1
-  ... Authorization: Basic celso.providelo@xxxxxxxxxxxxx:test
-  ... Referer: https://launchpad.test/
-  ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
-  ...
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries"
-  ...
-  ... 2
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.whiteboard"
-  ...
-  ... would be great to have, but has high risk
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.actions.continue"
-  ...
-  ... Continue
-  ... -----------------------------26999413214087432371486976730--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Content-Length: 0
-  ...
-  Location: http://.../firefox/+spec/svg-support
-  ...
+    >>> print(http(r"""
+    ... POST /firefox/+spec/svg-support/+setproductseries HTTP/1.1
+    ... Authorization: Basic celso.providelo@xxxxxxxxxxxxx:test
+    ... Referer: https://launchpad.test/
+    ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
+    ...
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries"
+    ...
+    ... 2
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries-empty-marker"
+    ...
+    ... 1
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.whiteboard"
+    ...
+    ... would be great to have, but has high risk
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.actions.continue"
+    ...
+    ... Continue
+    ... -----------------------------26999413214087432371486976730--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Content-Length: 0
+    ...
+    Location: http://.../firefox/+spec/svg-support
+    ...
 
 
 When we view that page, we see the targeted product series listed in the
 header.
 
-  >>> print(http(r"""
-  ... GET /firefox/+spec/svg-support HTTP/1.1
-  ... """))
-  HTTP/1.1 200 Ok
-  ...Proposed...
-  ...firefox/1.0...
+    >>> print(http(r"""
+    ... GET /firefox/+spec/svg-support HTTP/1.1
+    ... """))
+    HTTP/1.1 200 Ok
+    ...Proposed...
+    ...firefox/1.0...
 
 
 OK, we will also pitch the e4x spec to the same series:
 
-  >>> print(http(r"""
-  ... POST /firefox/+spec/e4x/+setproductseries HTTP/1.1
-  ... Authorization: Basic celso.providelo@xxxxxxxxxxxxx:test
-  ... Referer: https://launchpad.test/
-  ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
-  ...
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries"
-  ...
-  ... 2
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.whiteboard"
-  ...
-  ... would be great to have, but has high risk
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.actions.continue"
-  ...
-  ... Continue
-  ... -----------------------------26999413214087432371486976730--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Content-Length: 0
-  ...
-  Location: http://.../firefox/+spec/e4x
-  ...
+    >>> print(http(r"""
+    ... POST /firefox/+spec/e4x/+setproductseries HTTP/1.1
+    ... Authorization: Basic celso.providelo@xxxxxxxxxxxxx:test
+    ... Referer: https://launchpad.test/
+    ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
+    ...
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries"
+    ...
+    ... 2
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries-empty-marker"
+    ...
+    ... 1
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.whiteboard"
+    ...
+    ... would be great to have, but has high risk
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.actions.continue"
+    ...
+    ... Continue
+    ... -----------------------------26999413214087432371486976730--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Content-Length: 0
+    ...
+    Location: http://.../firefox/+spec/e4x
+    ...
 
 
 And now both should show up on the "+setgoals" page for that product series.
 
-  >>> print(http(r"""
-  ... GET /firefox/1.0/+setgoals HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...Support Native SVG Objects...
-  ...Support E4X in EcmaScript...
+    >>> print(http(r"""
+    ... GET /firefox/1.0/+setgoals HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...Support Native SVG Objects...
+    ...Support E4X in EcmaScript...
 
 
 Now, we will accept one of them, the svg-support one. We expect to be told
@@ -159,57 +159,57 @@ there are none left in the queue.
 
 The accepted item should show up in the list of specs for this series:
 
-  >>> print(http(r"""
-  ... GET /firefox/1.0/+specs HTTP/1.1
-  ... """))
-  HTTP/1.1 200 Ok
-  ...Support Native SVG Objects...
+    >>> print(http(r"""
+    ... GET /firefox/1.0/+specs HTTP/1.1
+    ... """))
+    HTTP/1.1 200 Ok
+    ...Support Native SVG Objects...
 
 
 As a final check, we will show that there is that spec in the "Deferred"
 listing.
 
-  >>> print(http(r"""
-  ... GET /firefox/1.0/+specs?acceptance=declined HTTP/1.1
-  ... """))
-  HTTP/1.1 200 Ok
-  ...Support E4X in EcmaScript...
+    >>> print(http(r"""
+    ... GET /firefox/1.0/+specs?acceptance=declined HTTP/1.1
+    ... """))
+    HTTP/1.1 200 Ok
+    ...Support E4X in EcmaScript...
 
 
 Now, lets make sure that automatic approval works. We will move the accepted
 spec to the "trunk" series, where it will be automatically approved
 because we are an admin, then we will move it back.
 
-  >>> print(http(r"""
-  ... POST /firefox/+spec/svg-support/+setproductseries HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Referer: https://launchpad.test/
-  ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
-  ...
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries"
-  ...
-  ... 1
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.whiteboard"
-  ...
-  ... would be great to have, but has high risk
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.actions.continue"
-  ...
-  ... Continue
-  ... -----------------------------26999413214087432371486976730--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Content-Length: 0
-  ...
-  Location: http://.../firefox/+spec/svg-support
-  ...
+    >>> print(http(r"""
+    ... POST /firefox/+spec/svg-support/+setproductseries HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Referer: https://launchpad.test/
+    ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
+    ...
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries"
+    ...
+    ... 1
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries-empty-marker"
+    ...
+    ... 1
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.whiteboard"
+    ...
+    ... would be great to have, but has high risk
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.actions.continue"
+    ...
+    ... Continue
+    ... -----------------------------26999413214087432371486976730--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Content-Length: 0
+    ...
+    Location: http://.../firefox/+spec/svg-support
+    ...
 
 
 OK, lets see if it was immediately accepted:
@@ -223,36 +223,36 @@ OK, lets see if it was immediately accepted:
 
 And lets put it back:
 
-  >>> print(http(r"""
-  ... POST /firefox/+spec/svg-support/+setproductseries HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Referer: https://launchpad.test/
-  ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
-  ...
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries"
-  ...
-  ... 2
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.productseries-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.whiteboard"
-  ...
-  ... would be great to have, but has high risk
-  ... -----------------------------26999413214087432371486976730
-  ... Content-Disposition: form-data; name="field.actions.continue"
-  ...
-  ... Continue
-  ... -----------------------------26999413214087432371486976730--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Content-Length: 0
-  ...
-  Location: http://.../firefox/+spec/svg-support
-  ...
+    >>> print(http(r"""
+    ... POST /firefox/+spec/svg-support/+setproductseries HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Referer: https://launchpad.test/
+    ... Content-Type: multipart/form-data; boundary=---------------------------26999413214087432371486976730
+    ...
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries"
+    ...
+    ... 2
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.productseries-empty-marker"
+    ...
+    ... 1
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.whiteboard"
+    ...
+    ... would be great to have, but has high risk
+    ... -----------------------------26999413214087432371486976730
+    ... Content-Disposition: form-data; name="field.actions.continue"
+    ...
+    ... Continue
+    ... -----------------------------26999413214087432371486976730--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Content-Length: 0
+    ...
+    Location: http://.../firefox/+spec/svg-support
+    ...
 
 And again, it should be accepted automatically.
 
diff --git a/lib/lp/blueprints/stories/sprints/sprint-settopics.txt b/lib/lp/blueprints/stories/sprints/sprint-settopics.txt
index 9598918..3f54f58 100644
--- a/lib/lp/blueprints/stories/sprints/sprint-settopics.txt
+++ b/lib/lp/blueprints/stories/sprints/sprint-settopics.txt
@@ -1,90 +1,89 @@
 Any logged in user can propose specs to be discussed in a sprint.
 
-  >>> user_browser.open(
-  ...     'http://blueprints.launchpad.test/ubuntu/'
-  ...     '+spec/media-integrity-check/+linksprint')
-
-  >>> user_browser.getControl('Sprint').value = ['uds-guacamole']
-  >>> user_browser.getControl('Continue').click()
-  >>> meeting_link = user_browser.getLink('uds-guacamole')
-  >>> meeting_link is not None
-  True
-
-  >>> user_browser.open(
-  ...     'http://blueprints.launchpad.test/kubuntu/'
-  ...     '+spec/kde-desktopfile-langpacks/+linksprint')
-  >>> user_browser.getControl('Sprint').value = ['uds-guacamole']
-  >>> user_browser.getControl('Continue').click()
-  >>> meeting_link = user_browser.getLink('uds-guacamole')
-  >>> meeting_link is not None
-  True
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/ubuntu/'
+    ...     '+spec/media-integrity-check/+linksprint')
+
+    >>> user_browser.getControl('Sprint').value = ['uds-guacamole']
+    >>> user_browser.getControl('Continue').click()
+    >>> meeting_link = user_browser.getLink('uds-guacamole')
+    >>> meeting_link is not None
+    True
+
+    >>> user_browser.open(
+    ...     'http://blueprints.launchpad.test/kubuntu/'
+    ...     '+spec/kde-desktopfile-langpacks/+linksprint')
+    >>> user_browser.getControl('Sprint').value = ['uds-guacamole']
+    >>> user_browser.getControl('Continue').click()
+    >>> meeting_link = user_browser.getLink('uds-guacamole')
+    >>> meeting_link is not None
+    True
 
 Regular users can't approve items to be discussed in a sprint.
 
-  >>> user_browser.open('http://launchpad.test/sprints/uds-guacamole')
-  >>> user_browser.getLink('proposed')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.testbrowser.browser.LinkNotFoundError
-
-  >>> user_browser.getLink('Blueprints').click()
-  >>> user_browser.getLink('Set agenda').click()
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.testbrowser.browser.LinkNotFoundError
-
-  >>> user_browser.open(
-  ...     'http://launchpad.test/sprints/uds-guacamole/+settopics')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.security.interfaces.Unauthorized: ...
+    >>> user_browser.open('http://launchpad.test/sprints/uds-guacamole')
+    >>> user_browser.getLink('proposed')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.testbrowser.browser.LinkNotFoundError
+
+    >>> user_browser.getLink('Blueprints').click()
+    >>> user_browser.getLink('Set agenda').click()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.testbrowser.browser.LinkNotFoundError
+
+    >>> user_browser.open(
+    ...     'http://launchpad.test/sprints/uds-guacamole/+settopics')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.security.interfaces.Unauthorized: ...
 
 It's possible to delegate the approval of a sprint agenda items to somebody else.
 First choose a driver for the UDS Guacamole sprint.
 
-  >>> browser = setupBrowser(auth='Basic mark@xxxxxxxxxxx:test')
-  >>> browser.open('http://launchpad.test/sprints/uds-guacamole')
-  >>> browser.getLink('Change details').click()
-  >>> browser.url
-  'http://launchpad.test/sprints/uds-guacamole/+edit'
+    >>> browser = setupBrowser(auth='Basic mark@xxxxxxxxxxx:test')
+    >>> browser.open('http://launchpad.test/sprints/uds-guacamole')
+    >>> browser.getLink('Change details').click()
+    >>> browser.url
+    'http://launchpad.test/sprints/uds-guacamole/+edit'
 
-  >>> browser.getControl('Meeting Driver').value = 'ubuntu-team'
-  >>> browser.getControl('Change').click()
+    >>> browser.getControl('Meeting Driver').value = 'ubuntu-team'
+    >>> browser.getControl('Change').click()
 
-  >>> browser.url
-  'http://launchpad.test/sprints/uds-guacamole'
+    >>> browser.url
+    'http://launchpad.test/sprints/uds-guacamole'
 
-  >>> meeting_drivers = find_tag_by_id(browser.contents, 'meeting-drivers')
-  >>> print(extract_text(meeting_drivers.find_next('a')))
-  Ubuntu Team
+    >>> meeting_drivers = find_tag_by_id(browser.contents, 'meeting-drivers')
+    >>> print(extract_text(meeting_drivers.find_next('a')))
+    Ubuntu Team
 
 Any member of the Ubuntu-Team can now approve and/or decline items to the UDS 
 Guacamole agenda.
 
-  >>> cprov_browser = setupBrowser(auth='Basic celso.providelo@xxxxxxxxxxxxx:test')
-  >>> cprov_browser.open('http://launchpad.test/sprints/uds-guacamole')
-  >>> cprov_browser.getLink('Blueprints').click()
-  >>> cprov_browser.url
-  'http://blueprints.launchpad.test/sprints/uds-guacamole'
-  >>> cprov_browser.getLink('Set agenda').click()
-
-  >>> print(cprov_browser.title)
-  Review discussion topics for “Ubuntu DevSummit Guacamole” sprint :
-  Blueprints :
-  Ubuntu DevSummit Guacamole :
-  Meetings
-
-  >>> cprov_browser.getControl('CD Media Integrity Check').selected = True
-  >>> cprov_browser.getControl('Accept').click()
-
-  >>> cprov_browser.getControl(
-  ...     'KDE Desktop File Language Packs').selected = True
-  >>> cprov_browser.getControl('Decline').click()
-
-  >>> cprov_browser.url
-  'http://blueprints.launchpad.test/sprints/uds-guacamole/+specs'
-
-
+    >>> cprov_browser = setupBrowser(
+    ...     auth='Basic celso.providelo@xxxxxxxxxxxxx:test')
+    >>> cprov_browser.open('http://launchpad.test/sprints/uds-guacamole')
+    >>> cprov_browser.getLink('Blueprints').click()
+    >>> cprov_browser.url
+    'http://blueprints.launchpad.test/sprints/uds-guacamole'
+    >>> cprov_browser.getLink('Set agenda').click()
+
+    >>> print(cprov_browser.title)
+    Review discussion topics for “Ubuntu DevSummit Guacamole” sprint :
+    Blueprints :
+    Ubuntu DevSummit Guacamole :
+    Meetings
+
+    >>> cprov_browser.getControl('CD Media Integrity Check').selected = True
+    >>> cprov_browser.getControl('Accept').click()
+
+    >>> cprov_browser.getControl(
+    ...     'KDE Desktop File Language Packs').selected = True
+    >>> cprov_browser.getControl('Decline').click()
+
+    >>> cprov_browser.url
+    'http://blueprints.launchpad.test/sprints/uds-guacamole/+specs'
diff --git a/lib/lp/blueprints/stories/standalone/sprint-links.txt b/lib/lp/blueprints/stories/standalone/sprint-links.txt
index 2c6ab12..4f027f7 100644
--- a/lib/lp/blueprints/stories/standalone/sprint-links.txt
+++ b/lib/lp/blueprints/stories/standalone/sprint-links.txt
@@ -9,16 +9,16 @@ for the agenda.
 First we open the page for the spec on Support <canvas> objects from the
 sample data. We will use Sample Person, who has no special privileges.
 
-  >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
-  >>> browser.open('http://blueprints.launchpad.test/firefox/+spec/canvas')
-  >>> browser.isHtml
-  True
+    >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
+    >>> browser.open('http://blueprints.launchpad.test/firefox/+spec/canvas')
+    >>> browser.isHtml
+    True
 
 Then we are going to propose it for the meeting agenda:
 
-  >>> browser.getLink('Propose for sprint').click()
-  >>> browser.title
-  'Propose specification for...
+    >>> browser.getLink('Propose for sprint').click()
+    >>> browser.title
+    'Propose specification for...
 
 The page contains a link back to the blueprint, in case we change our
 mind.
@@ -29,21 +29,21 @@ mind.
 
 Now  with a POST, we try to Add the spec to the Guacamole sprint.
 
-  >>> sprint_field = browser.getControl(name='field.sprint')
-  >>> sprint_field.value = ['uds-guacamole']
-  >>> browser.getControl('Continue').click()
-  >>> browser.url # we should have been redirected to the spec page
-  'http://.../firefox/+spec/canvas'
+    >>> sprint_field = browser.getControl(name='field.sprint')
+    >>> sprint_field.value = ['uds-guacamole']
+    >>> browser.getControl('Continue').click()
+    >>> browser.url # we should have been redirected to the spec page
+    'http://.../firefox/+spec/canvas'
 
 Now we test to see if the sprint was added correctly to the
 specification page.
 
-  >>> 'uds-guacamole' in browser.contents
-  True
-  >>> 'Accepted' in browser.contents
-  False
-  >>> 'Proposed' in browser.contents
-  True
+    >>> 'uds-guacamole' in browser.contents
+    True
+    >>> 'Accepted' in browser.contents
+    False
+    >>> 'Proposed' in browser.contents
+    True
 
 
 Sprint Drivers
@@ -94,32 +94,31 @@ Now, if we change our mind, we can go and decline the spec.
 
 First, make sure the page has no "Declined" text.
 
-  >>> 'Declined' not in browser.contents
-  True
+    >>> 'Declined' not in browser.contents
+    True
 
 Now go and change that and verify.
 
-  >>> browser.getLink('Approved').click()
-  >>> browser.url
-  'http://.../kubuntu/+spec/kde-desktopfile-langpacks/rome'
-  >>> back_link = browser.getLink('KDE Desktop File Language Packs')
-  >>> back_link.url
-  'http://blueprints.launchpad.test/kubuntu/+spec/kde-desktopfile-langpacks'
-  >>> browser.getControl('Decline').click()
-  >>> 'Declined for the meeting' not in browser.contents
-  False
+    >>> browser.getLink('Approved').click()
+    >>> browser.url
+    'http://.../kubuntu/+spec/kde-desktopfile-langpacks/rome'
+    >>> back_link = browser.getLink('KDE Desktop File Language Packs')
+    >>> back_link.url
+    'http://blueprints.launchpad.test/kubuntu/+spec/kde-desktopfile-langpacks'
+    >>> browser.getControl('Decline').click()
+    >>> 'Declined for the meeting' not in browser.contents
+    False
 
 Alright. Now lets go accept it again.
 
-  >>> browser.getLink('Declined').click()
-  >>> browser.getControl('Accept').click()
-  >>> 'Declined for the meeting' not in browser.contents
-  True
+    >>> browser.getLink('Declined').click()
+    >>> browser.getControl('Accept').click()
+    >>> 'Declined for the meeting' not in browser.contents
+    True
 
 And finally, we will test the Cancel button on that page.
 
-  >>> browser.getLink('Approved').click()
-  >>> browser.getControl('Cancel').click()
-  >>> 'Declined for the meeting' not in browser.contents
-  True
-
+    >>> browser.getLink('Approved').click()
+    >>> browser.getControl('Cancel').click()
+    >>> 'Declined for the meeting' not in browser.contents
+    True
diff --git a/lib/lp/blueprints/stories/standalone/xx-batching.txt b/lib/lp/blueprints/stories/standalone/xx-batching.txt
index 619b246..cec3c6b 100644
--- a/lib/lp/blueprints/stories/standalone/xx-batching.txt
+++ b/lib/lp/blueprints/stories/standalone/xx-batching.txt
@@ -11,84 +11,86 @@ the user to navigate between the batches on demand.
 
 To demonstrate this, we'll create a new project:
 
-  >>> browser = user_browser
-  >>> browser.open("http://launchpad.test/projects/+new";)
-  >>> browser.getControl('URL', index=0).value = 'big-project'
-  >>> browser.getControl('Name').value = 'Big Project'
-  >>> browser.getControl('Summary').value = 'A big project indeed.'
-  >>> browser.getControl('Continue').click()
-
-  >>> browser.getControl(name='field.licenses').value = ['GNU_GPL_V2']
-  >>> browser.getControl(name='field.license_info').value = 'foo'
-  >>> browser.getControl('Complete Registration').click()
-  >>> browser.url
-  'http://launchpad.test/big-project'
+    >>> browser = user_browser
+    >>> browser.open("http://launchpad.test/projects/+new";)
+    >>> browser.getControl('URL', index=0).value = 'big-project'
+    >>> browser.getControl('Name').value = 'Big Project'
+    >>> browser.getControl('Summary').value = 'A big project indeed.'
+    >>> browser.getControl('Continue').click()
+
+    >>> browser.getControl(name='field.licenses').value = ['GNU_GPL_V2']
+    >>> browser.getControl(name='field.license_info').value = 'foo'
+    >>> browser.getControl('Complete Registration').click()
+    >>> browser.url
+    'http://launchpad.test/big-project'
 
 In the beginning, a project hasn't had blueprints set up:
 
-  >>> browser.open("http://blueprints.launchpad.test/big-project";)
-  >>> print(extract_text(find_main_content(browser.contents)))
-  Blueprints...does not know how...Configure Blueprints...
+    >>> browser.open("http://blueprints.launchpad.test/big-project";)
+    >>> print(extract_text(find_main_content(browser.contents)))
+    Blueprints...does not know how...Configure Blueprints...
 
 But it's easy to change that.
-  
-  >>> browser.open("http://blueprints.launchpad.test/big-project/+configure-blueprints";)
-  >>> browser.getControl(name='field.blueprints_usage').value = ['LAUNCHPAD']
-  >>> browser.getControl('Change').click()
-  >>> browser.url
-  'http://blueprints.launchpad.test/big-project'
+
+    >>> browser.open(
+    ...     "http://blueprints.launchpad.test/big-project/+configure-blueprints";)
+    >>> browser.getControl(
+    ...     name='field.blueprints_usage').value = ['LAUNCHPAD']
+    >>> browser.getControl('Change').click()
+    >>> browser.url
+    'http://blueprints.launchpad.test/big-project'
 
 Initially the newly enabled feature has no blueprints.
 
-  >>> browser.open("http://blueprints.launchpad.test/big-project";)
-  >>> print(extract_text(find_main_content(browser.contents)))
-  Blueprints...first blueprint in this project!...
-  
+    >>> browser.open("http://blueprints.launchpad.test/big-project";)
+    >>> print(extract_text(find_main_content(browser.contents)))
+    Blueprints...first blueprint in this project!...
+
 We'll go ahead and add just a single blueprint:
 
-  >>> browser.open('http://launchpad.test/big-project/+addspec')
-  >>> browser.getControl('Name', index=0).value = 'blueprint-0'
-  >>> browser.getControl('Title').value = 'Blueprint 0'
-  >>> browser.getControl('Summary').value = 'Blueprint 0'
-  >>> browser.getControl('Register Blueprint').click()
-  >>> browser.url
-  'http://blueprints.launchpad.test/big-project/+spec/blueprint-0'
-  
+    >>> browser.open('http://launchpad.test/big-project/+addspec')
+    >>> browser.getControl('Name', index=0).value = 'blueprint-0'
+    >>> browser.getControl('Title').value = 'Blueprint 0'
+    >>> browser.getControl('Summary').value = 'Blueprint 0'
+    >>> browser.getControl('Register Blueprint').click()
+    >>> browser.url
+    'http://blueprints.launchpad.test/big-project/+spec/blueprint-0'
+
 When we ask for the complete list of blueprints for our project, the new 
 blueprint is listed:
 
-  >>> browser.open("http://blueprints.launchpad.test/big-project";)
-  >>> print(extract_text(first_tag_by_class(browser.contents, 
-  ...                                      'batch-navigation-index')))
-  1...→...1...of...1 result
-  
+    >>> browser.open("http://blueprints.launchpad.test/big-project";)
+    >>> print(extract_text(first_tag_by_class(browser.contents, 
+    ...                                      'batch-navigation-index')))
+    1...→...1...of...1 result
+
 Let's add some more blueprints:
 
-  >>> for index in range(1, 20):
-  ...     browser.open('http://launchpad.test/big-project/+addspec')
-  ...     browser.getControl('Name', index=0).value = 'blueprint-%d' % index
-  ...     browser.getControl('Title').value = 'Blueprint %d' % index
-  ...     browser.getControl('Summary').value = 'Blueprint %d' % index
-  ...     browser.getControl('Register Blueprint').click()
+    >>> for index in range(1, 20):
+    ...     browser.open('http://launchpad.test/big-project/+addspec')
+    ...     browser.getControl('Name', index=0).value = 'blueprint-%d' % index
+    ...     browser.getControl('Title').value = 'Blueprint %d' % index
+    ...     browser.getControl('Summary').value = 'Blueprint %d' % index
+    ...     browser.getControl('Register Blueprint').click()
 
 Observe that now when we ask for the complete list of blueprints, only some of
 the blueprints are listed:
 
-  >>> browser.open("http://blueprints.launchpad.test/big-project";)
-  >>> print(extract_text(first_tag_by_class(browser.contents, 
-  ...                                      'batch-navigation-index')))
-  1...→...5...of...20 results
+    >>> browser.open("http://blueprints.launchpad.test/big-project";)
+    >>> print(extract_text(first_tag_by_class(browser.contents, 
+    ...                                      'batch-navigation-index')))
+    1...→...5...of...20 results
 
 We can go to the next batch of blueprints by following the 'Next' link:
-    
-  >>> browser.getLink('Next').click()
-  >>> print(extract_text(first_tag_by_class(browser.contents, 
-  ...                                      'batch-navigation-index')))
-  6...→...10...of...20 results
+
+    >>> browser.getLink('Next').click()
+    >>> print(extract_text(first_tag_by_class(browser.contents, 
+    ...                                      'batch-navigation-index')))
+    6...→...10...of...20 results
 
 Following the 'Last' link takes us to the last batch of blueprints:
 
-  >>> browser.getLink('Last').click()
-  >>> print(extract_text(first_tag_by_class(browser.contents, 
-  ...                                      'batch-navigation-index')))
-  16...→...20...of...20 results
+    >>> browser.getLink('Last').click()
+    >>> print(extract_text(first_tag_by_class(browser.contents, 
+    ...                                      'batch-navigation-index')))
+    16...→...20...of...20 results
diff --git a/lib/lp/blueprints/stories/standalone/xx-retargeting.txt b/lib/lp/blueprints/stories/standalone/xx-retargeting.txt
index 3f52054..6458f63 100644
--- a/lib/lp/blueprints/stories/standalone/xx-retargeting.txt
+++ b/lib/lp/blueprints/stories/standalone/xx-retargeting.txt
@@ -4,12 +4,12 @@ different product or distribution.
 
 First, load the svg-support spec on Firefox:
 
-  >>> admin_browser.open("http://launchpad.test/firefox/+spec/svg-support";)
+    >>> admin_browser.open("http://launchpad.test/firefox/+spec/svg-support";)
 
 Now, let's make sure we can see the retargeting page for it, as the Foo Bar
 administrator:
 
-  >>> admin_browser.getLink("Re-target blueprint").click()
+    >>> admin_browser.getLink("Re-target blueprint").click()
 
 The page contains a link back to the blueprint, in case we change our
 mind.
@@ -22,44 +22,44 @@ mind.
 
 We can move the blueprint to Evolution.
 
-  >>> admin_browser.getControl("For").value = "evolution"
-  >>> admin_browser.getControl("Retarget Blueprint").click()
-  >>> admin_browser.url
-  'http://blueprints.launchpad.test/evolution/+spec/svg-support'
+    >>> admin_browser.getControl("For").value = "evolution"
+    >>> admin_browser.getControl("Retarget Blueprint").click()
+    >>> admin_browser.url
+    'http://blueprints.launchpad.test/evolution/+spec/svg-support'
 
 OK. Now, it follows that we should be able to retarget it immediately from
 evolution, to a distribution. Let's try redhat.
 
-  >>> admin_browser.getLink("Re-target blueprint").click()
-  >>> admin_browser.getControl("For").value = "redhat"
-  >>> admin_browser.getControl("Retarget Blueprint").click()
-  >>> admin_browser.url
-  'http://blueprints.launchpad.test/redhat/+spec/svg-support'
+    >>> admin_browser.getLink("Re-target blueprint").click()
+    >>> admin_browser.getControl("For").value = "redhat"
+    >>> admin_browser.getControl("Retarget Blueprint").click()
+    >>> admin_browser.url
+    'http://blueprints.launchpad.test/redhat/+spec/svg-support'
 
 And similarly, this should now be on Red Hat, and we should be able to send
 it straight back to firefox. This means that the data set should finish this
 test in the same state that it was when we started.
 
-  >>> admin_browser.getLink("Re-target blueprint").click()
-  >>> admin_browser.getControl("For").value = "firefox"
-  >>> admin_browser.getControl("Retarget Blueprint").click()
-  >>> admin_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/svg-support'
+    >>> admin_browser.getLink("Re-target blueprint").click()
+    >>> admin_browser.getControl("For").value = "firefox"
+    >>> admin_browser.getControl("Retarget Blueprint").click()
+    >>> admin_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/svg-support'
 
 If we try to reassign the spec to a target which doesn't exist, we don't
 blow up:
 
-  >>> admin_browser.getLink("Re-target blueprint").click()
-  >>> admin_browser.getControl("For").value = "foo bar"
-  >>> admin_browser.getControl("Retarget Blueprint").click()
+    >>> admin_browser.getLink("Re-target blueprint").click()
+    >>> admin_browser.getControl("For").value = "foo bar"
+    >>> admin_browser.getControl("Retarget Blueprint").click()
 
 We stay on the same page and get an error message printed out:
 
-  >>> admin_browser.url
-  'http://blueprints.launchpad.test/firefox/+spec/svg-support/+retarget'
+    >>> admin_browser.url
+    'http://blueprints.launchpad.test/firefox/+spec/svg-support/+retarget'
 
-  >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-  ...     print(tag.decode_contents())
-  There is 1 error.
-  <BLANKLINE>
-  There is no project with the name 'foo bar'. Please check that name and try again.
+    >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
+    ...     print(tag.decode_contents())
+    There is 1 error.
+    <BLANKLINE>
+    There is no project with the name 'foo bar'. Please check that name and try again.
diff --git a/lib/lp/bugs/browser/tests/distrosourcepackage-bug-views.txt b/lib/lp/bugs/browser/tests/distrosourcepackage-bug-views.txt
index c3a85c2..a82c205 100644
--- a/lib/lp/bugs/browser/tests/distrosourcepackage-bug-views.txt
+++ b/lib/lp/bugs/browser/tests/distrosourcepackage-bug-views.txt
@@ -6,29 +6,31 @@ Searching
 
 Simple searching is possible on the distro source package bug view page.
 
-  >>> from zope.component import getMultiAdapter, getUtility
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
-  >>> from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
-
-  >>> debian = getUtility(IDistributionSet).get(3)
-  >>> mozilla_firefox = getUtility(ISourcePackageNameSet).get(1)
-  >>> debian_mozilla_firefox = debian.getSourcePackage(mozilla_firefox)
-  >>> request = LaunchpadTestRequest(
-  ...     form={'field.searchtext': 'svg', 'search': 'Search'})
-  >>> dsp_bugs_view = getMultiAdapter(
-  ...     (debian_mozilla_firefox, request), name='+bugs')
-  >>> dsp_bugs_view.initialize()
-
-  >>> [task.bug.id for task in dsp_bugs_view.search().batch]
-  [1]
+    >>> from zope.component import getMultiAdapter, getUtility
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> from lp.registry.interfaces.sourcepackagename import (
+    ...     ISourcePackageNameSet,
+    ...     )
+
+    >>> debian = getUtility(IDistributionSet).get(3)
+    >>> mozilla_firefox = getUtility(ISourcePackageNameSet).get(1)
+    >>> debian_mozilla_firefox = debian.getSourcePackage(mozilla_firefox)
+    >>> request = LaunchpadTestRequest(
+    ...     form={'field.searchtext': 'svg', 'search': 'Search'})
+    >>> dsp_bugs_view = getMultiAdapter(
+    ...     (debian_mozilla_firefox, request), name='+bugs')
+    >>> dsp_bugs_view.initialize()
+
+    >>> [task.bug.id for task in dsp_bugs_view.search().batch]
+    [1]
 
 The "search" parameter is optional, allowing more concise URLs.
 
-  >>> request = LaunchpadTestRequest(form={'field.searchtext': 'svg'})
-  >>> dsp_bugs_view = getMultiAdapter(
-  ...     (debian_mozilla_firefox, request), name='+bugs')
-  >>> dsp_bugs_view.initialize()
+    >>> request = LaunchpadTestRequest(form={'field.searchtext': 'svg'})
+    >>> dsp_bugs_view = getMultiAdapter(
+    ...     (debian_mozilla_firefox, request), name='+bugs')
+    >>> dsp_bugs_view.initialize()
 
-  >>> [task.bug.id for task in dsp_bugs_view.search().batch]
-  [1]
+    >>> [task.bug.id for task in dsp_bugs_view.search().batch]
+    [1]
diff --git a/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt b/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt
index 3e4af4f..4b05b1f 100644
--- a/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt
+++ b/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt
@@ -15,51 +15,51 @@ The icon is dependent on the importance of the IBugTask object.
 
 Let's use a few examples to demonstrate:
 
-  >>> from zope.component import getUtility
-  >>> from lp.services.webapp.interfaces import ILaunchBag
-  >>> from lp.bugs.interfaces.bugtask import BugTaskImportance, IBugTaskSet
-  >>> from lp.testing import (
-  ...     login,
-  ...     test_tales,
-  ...     )
-
-  >>> login("foo.bar@xxxxxxxxxxxxx")
-  >>> bugtaskset = getUtility(IBugTaskSet)
-  >>> test_task = bugtaskset.get(4)
-  >>> ORIGINAL_IMPORTANCE = test_task.importance
-
-  >>> test_task.transitionToImportance(
-  ...   BugTaskImportance.CRITICAL, getUtility(ILaunchBag).user)
-  >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
-  'sprite bug-critical'
-
-  >>> test_task.transitionToImportance(
-  ...   BugTaskImportance.HIGH, getUtility(ILaunchBag).user)
-  >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
-  'sprite bug-high'
-
-  >>> test_task.transitionToImportance(
-  ...   BugTaskImportance.MEDIUM, getUtility(ILaunchBag).user)
-  >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
-  'sprite bug-medium'
-
-  >>> test_task.transitionToImportance(
-  ...   BugTaskImportance.LOW, getUtility(ILaunchBag).user)
-  >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
-  'sprite bug-low'
-
-  >>> test_task.transitionToImportance(
-  ...   BugTaskImportance.WISHLIST, getUtility(ILaunchBag).user)
-  >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
-  'sprite bug-wishlist'
-
-  >>> test_task.transitionToImportance(
-  ...   BugTaskImportance.UNDECIDED, getUtility(ILaunchBag).user)
-  >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
-  'sprite bug-undecided'
-
-  >>> test_task.transitionToImportance(
-  ...   ORIGINAL_IMPORTANCE, getUtility(ILaunchBag).user)
+    >>> from zope.component import getUtility
+    >>> from lp.services.webapp.interfaces import ILaunchBag
+    >>> from lp.bugs.interfaces.bugtask import BugTaskImportance, IBugTaskSet
+    >>> from lp.testing import (
+    ...     login,
+    ...     test_tales,
+    ...     )
+
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> bugtaskset = getUtility(IBugTaskSet)
+    >>> test_task = bugtaskset.get(4)
+    >>> ORIGINAL_IMPORTANCE = test_task.importance
+
+    >>> test_task.transitionToImportance(
+    ...   BugTaskImportance.CRITICAL, getUtility(ILaunchBag).user)
+    >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
+    'sprite bug-critical'
+
+    >>> test_task.transitionToImportance(
+    ...   BugTaskImportance.HIGH, getUtility(ILaunchBag).user)
+    >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
+    'sprite bug-high'
+
+    >>> test_task.transitionToImportance(
+    ...   BugTaskImportance.MEDIUM, getUtility(ILaunchBag).user)
+    >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
+    'sprite bug-medium'
+
+    >>> test_task.transitionToImportance(
+    ...   BugTaskImportance.LOW, getUtility(ILaunchBag).user)
+    >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
+    'sprite bug-low'
+
+    >>> test_task.transitionToImportance(
+    ...   BugTaskImportance.WISHLIST, getUtility(ILaunchBag).user)
+    >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
+    'sprite bug-wishlist'
+
+    >>> test_task.transitionToImportance(
+    ...   BugTaskImportance.UNDECIDED, getUtility(ILaunchBag).user)
+    >>> test_tales("bugtask/image:sprite_css", bugtask=test_task)
+    'sprite bug-undecided'
+
+    >>> test_task.transitionToImportance(
+    ...   ORIGINAL_IMPORTANCE, getUtility(ILaunchBag).user)
 
 
 Displaying Logos for Bug Tasks
@@ -68,24 +68,24 @@ Displaying Logos for Bug Tasks
 The logo for a bug task display the corresponding logo for its
 target.
 
-  >>> from lp.bugs.interfaces.bug import IBugSet
-  >>> bug1 = getUtility(IBugSet).get(1)
-  >>> upstream_task = bug1.bugtasks[0]
-  >>> print(upstream_task.product.name)
-  firefox
-  >>> ubuntu_task = bug1.bugtasks[1]
-  >>> print(ubuntu_task.distribution.name)
-  ubuntu
+    >>> from lp.bugs.interfaces.bug import IBugSet
+    >>> bug1 = getUtility(IBugSet).get(1)
+    >>> upstream_task = bug1.bugtasks[0]
+    >>> print(upstream_task.product.name)
+    firefox
+    >>> ubuntu_task = bug1.bugtasks[1]
+    >>> print(ubuntu_task.distribution.name)
+    ubuntu
 
 So the logo for an upstream bug task shows the project icon:
 
-  >>> test_tales("bugtask/image:logo", bugtask=upstream_task)
-  '<img alt="" width="64" height="64" src="/@@/product-logo" />'
+    >>> test_tales("bugtask/image:logo", bugtask=upstream_task)
+    '<img alt="" width="64" height="64" src="/@@/product-logo" />'
 
 And the logo for a distro bug task shows the source package icon:
 
-  >>> test_tales("bugtask/image:logo", bugtask=ubuntu_task)
-  '<img alt="" width="64" height="64" src="/@@/distribution-logo" />'
+    >>> test_tales("bugtask/image:logo", bugtask=ubuntu_task)
+    '<img alt="" width="64" height="64" src="/@@/distribution-logo" />'
 
 
 Displaying Status
@@ -103,62 +103,62 @@ you might prefer that to read, simply:
 We define a helper that uses the BugTaskListingView class (obtained via
 +listing-view) to render the status:
 
-  >>> from zope.component import getMultiAdapter
-  >>> from lp.services.webapp.interfaces import ILaunchBag
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
+    >>> from zope.component import getMultiAdapter
+    >>> from lp.services.webapp.interfaces import ILaunchBag
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
 
-  >>> def render_bugtask_status(task):
-  ...   view = getMultiAdapter(
-  ...       (task, LaunchpadTestRequest()), name="+listing-view")
-  ...   return view.status
+    >>> def render_bugtask_status(task):
+    ...   view = getMultiAdapter(
+    ...       (task, LaunchpadTestRequest()), name="+listing-view")
+    ...   return view.status
 
 Let's see some examples of how this works:
 
-  >>> login("foo.bar@xxxxxxxxxxxxx", LaunchpadTestRequest())
-  >>> foobar = getUtility(ILaunchBag).user
+    >>> login("foo.bar@xxxxxxxxxxxxx", LaunchpadTestRequest())
+    >>> foobar = getUtility(ILaunchBag).user
 
-  >>> ORIGINAL_STATUS = test_task.status
-  >>> ORIGINAL_ASSIGNEE = test_task.assignee
+    >>> ORIGINAL_STATUS = test_task.status
+    >>> ORIGINAL_ASSIGNEE = test_task.assignee
 
-  >>> test_task.transitionToAssignee(None)
-  >>> render_bugtask_status(test_task)
-  'Confirmed (unassigned)'
+    >>> test_task.transitionToAssignee(None)
+    >>> render_bugtask_status(test_task)
+    'Confirmed (unassigned)'
 
-  >>> test_task.transitionToAssignee(foobar)
-  >>> test_task.transitionToStatus(
-  ...   BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-  >>> print(render_bugtask_status(test_task))
-  New, assigned to ...Foo Bar...
+    >>> test_task.transitionToAssignee(foobar)
+    >>> test_task.transitionToStatus(
+    ...   BugTaskStatus.NEW, getUtility(ILaunchBag).user)
+    >>> print(render_bugtask_status(test_task))
+    New, assigned to ...Foo Bar...
 
-  >>> test_task.transitionToStatus(
-  ...   BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
-  >>> print(render_bugtask_status(test_task))
-  Confirmed, assigned to ...Foo Bar...
+    >>> test_task.transitionToStatus(
+    ...   BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
+    >>> print(render_bugtask_status(test_task))
+    Confirmed, assigned to ...Foo Bar...
 
-  >>> test_task.transitionToStatus(
-  ...   BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
-  >>> print(render_bugtask_status(test_task))
-  Invalid by ...Foo Bar...
+    >>> test_task.transitionToStatus(
+    ...   BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
+    >>> print(render_bugtask_status(test_task))
+    Invalid by ...Foo Bar...
 
-  >>> test_task.transitionToAssignee(None)
-  >>> render_bugtask_status(test_task)
-  'Invalid (unassigned)'
+    >>> test_task.transitionToAssignee(None)
+    >>> render_bugtask_status(test_task)
+    'Invalid (unassigned)'
 
-  >>> test_task.transitionToStatus(
-  ...   BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
-  >>> render_bugtask_status(test_task)
-  'Fix released (unassigned)'
+    >>> test_task.transitionToStatus(
+    ...   BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+    >>> render_bugtask_status(test_task)
+    'Fix released (unassigned)'
 
-  >>> test_task.transitionToAssignee(foobar)
-  >>> print(render_bugtask_status(test_task))
-  Fix released, assigned to ...Foo Bar...
+    >>> test_task.transitionToAssignee(foobar)
+    >>> print(render_bugtask_status(test_task))
+    Fix released, assigned to ...Foo Bar...
 
 Lastly, some cleanup:
 
-  >>> test_task.transitionToStatus(
-  ...   ORIGINAL_STATUS, test_task.distribution.owner)
-  >>> test_task.transitionToAssignee(ORIGINAL_ASSIGNEE)
+    >>> test_task.transitionToStatus(
+    ...   ORIGINAL_STATUS, test_task.distribution.owner)
+    >>> test_task.transitionToAssignee(ORIGINAL_ASSIGNEE)
 
 
 Status Elsewhere
@@ -168,10 +168,10 @@ It's often useful to present information about the status of a bug in
 other contexts. Again, the listing-view holds a method which provides us
 with this information; let's define a helper for it:
 
-  >>> def render_bugtask_status_elsewhere(task):
-  ...   view = getMultiAdapter(
-  ...       (task, LaunchpadTestRequest()), name="+listing-view")
-  ...   return view.status_elsewhere
+    >>> def render_bugtask_status_elsewhere(task):
+    ...   view = getMultiAdapter(
+    ...       (task, LaunchpadTestRequest()), name="+listing-view")
+    ...   return view.status_elsewhere
 
 The main questions of interest, in order, are:
 
@@ -181,22 +181,22 @@ The main questions of interest, in order, are:
 
 Let's see some examples:
 
-  >>> render_bugtask_status_elsewhere(bugtaskset.get(13))
-  'not filed elsewhere'
+    >>> render_bugtask_status_elsewhere(bugtaskset.get(13))
+    'not filed elsewhere'
 
-  >>> render_bugtask_status_elsewhere(bugtaskset.get(2))
-  'filed in 2 other places'
+    >>> render_bugtask_status_elsewhere(bugtaskset.get(2))
+    'filed in 2 other places'
 
 Let's take a random task related to task 2, mark it Fixed, and see how the
 statuselsewhere value is affected:
 
-  >>> related_task = bugtaskset.get(2).related_tasks[0]
-  >>> ORIGINAL_STATUS = related_task.status
-  >>> related_task.transitionToStatus(
-  ...   BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
+    >>> related_task = bugtaskset.get(2).related_tasks[0]
+    >>> ORIGINAL_STATUS = related_task.status
+    >>> related_task.transitionToStatus(
+    ...   BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
 
-  >>> render_bugtask_status_elsewhere(bugtaskset.get(2))
-  'fixed in 1 of 3 places'
+    >>> render_bugtask_status_elsewhere(bugtaskset.get(2))
+    'fixed in 1 of 3 places'
 
-  >>> related_task.transitionToStatus(
-  ...   ORIGINAL_STATUS, getUtility(ILaunchBag).user)
+    >>> related_task.transitionToStatus(
+    ...   ORIGINAL_STATUS, getUtility(ILaunchBag).user)
diff --git a/lib/lp/bugs/doc/externalbugtracker-rt.txt b/lib/lp/bugs/doc/externalbugtracker-rt.txt
index 2feb460..d25c1ed 100644
--- a/lib/lp/bugs/doc/externalbugtracker-rt.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-rt.txt
@@ -83,8 +83,8 @@ There is no obvious mapping from ticket priorities to importances. They
 are all imported as Unknown. No exception is raised, because they are
 all unknown.
 
-   >>> rt.convertRemoteImportance('foo').title
-   'Unknown'
+    >>> rt.convertRemoteImportance('foo').title
+    'Unknown'
 
 
 Initialization
diff --git a/lib/lp/bugs/doc/treelookup.txt b/lib/lp/bugs/doc/treelookup.txt
index 7ef5b4e..c8f2325 100644
--- a/lib/lp/bugs/doc/treelookup.txt
+++ b/lib/lp/bugs/doc/treelookup.txt
@@ -1,8 +1,8 @@
 Doing lookups in a tree
 =======================
 
-  >>> from lp.bugs.adapters.treelookup import (
-  ...     LookupBranch, LookupTree)
+    >>> from lp.bugs.adapters.treelookup import (
+    ...     LookupBranch, LookupTree)
 
 `LookupTree` encapsulates a simple tree structure that can be used to
 do lookups using one or more keys.
@@ -39,15 +39,15 @@ position.
 Creation
 --------
 
-  >>> tree = LookupTree(
-  ...     ('Snack', LookupTree(
-  ...             ('Mars Bar', 'Snickers', 'Bad'),
-  ...             ('Apple', 'Banana', 'Good'))),
-  ...     LookupBranch('Lunch', 'Dinner', LookupTree(
-  ...             ('Fish and chips', "Penne all'arrabbiata", 'Nice'),
-  ...             ('Raw liver', 'Not so nice'))),
-  ...     ('Make up your mind!',),
-  ...     )
+    >>> tree = LookupTree(
+    ...     ('Snack', LookupTree(
+    ...             ('Mars Bar', 'Snickers', 'Bad'),
+    ...             ('Apple', 'Banana', 'Good'))),
+    ...     LookupBranch('Lunch', 'Dinner', LookupTree(
+    ...             ('Fish and chips', "Penne all'arrabbiata", 'Nice'),
+    ...             ('Raw liver', 'Not so nice'))),
+    ...     ('Make up your mind!',),
+    ...     )
 
 Behind the scenes, `LookupTree` promotes plain tuples (or any
 iterable) into `LookupBranch` instances. This means that the last
@@ -60,38 +60,38 @@ position, because it would completely obscure the subsequent branches
 in the tree. Hence, attempting to specify a default branch before the
 last position is treated as an error.
 
-  >>> broken_tree = LookupTree(
-  ...     ('Free agents',),
-  ...     ('Alice', 'Bob', 'Allies of Schneier'))
-  Traceback (most recent call last):
-  ...
-  TypeError: Default branch must be last.
+    >>> broken_tree = LookupTree(
+    ...     ('Free agents',),
+    ...     ('Alice', 'Bob', 'Allies of Schneier'))
+    Traceback (most recent call last):
+    ...
+    TypeError: Default branch must be last.
 
 To help when constructing more complex trees, an existing `LookupTree`
 instance can be passed in when constructing a new one. Its branches
 are copied into the new `LookupTree` at that point.
 
-  >>> breakfast_tree = LookupTree(
-  ...     ('Breakfast', 'Corn flakes'),
-  ...     tree,
-  ...     )
+    >>> breakfast_tree = LookupTree(
+    ...     ('Breakfast', 'Corn flakes'),
+    ...     tree,
+    ...     )
 
-  >>> len(tree.branches)
-  3
-  >>> len(breakfast_tree.branches)
-  4
+    >>> len(tree.branches)
+    3
+    >>> len(breakfast_tree.branches)
+    4
 
 Although it should not happen in regular operation (because
 `LookupTree.__init__` ensures all arguments are `LookupBranch`
 instances), `LookupTree._verify` also checks that every branch is a
 `LookupBranch`.
 
-  >>> invalid_tree = LookupTree(tree)
-  >>> invalid_tree.branches = invalid_tree.branches + ('Greenland',)
-  >>> invalid_tree._verify()
-  Traceback (most recent call last):
-  ...
-  TypeError: Not a LookupBranch: ...'Greenland'
+    >>> invalid_tree = LookupTree(tree)
+    >>> invalid_tree.branches = invalid_tree.branches + ('Greenland',)
+    >>> invalid_tree._verify()
+    Traceback (most recent call last):
+    ...
+    TypeError: Not a LookupBranch: ...'Greenland'
 
 
 Searching
@@ -99,21 +99,21 @@ Searching
 
 Just call `tree.find`.
 
-  >>> print(tree.find('Snack', 'Banana'))
-  Good
+    >>> print(tree.find('Snack', 'Banana'))
+    Good
 
 If you specify more keys than you need to reach a leaf, you still get
 the result.
 
-  >>> print(tree.find('Snack', 'Banana', 'Big', 'Yellow', 'Taxi'))
-  Good
+    >>> print(tree.find('Snack', 'Banana', 'Big', 'Yellow', 'Taxi'))
+    Good
 
 But an exception is raised if it does not reach a leaf.
 
-  >>> tree.find('Snack')
-  Traceback (most recent call last):
-  ...
-  KeyError: ...'Snack'
+    >>> tree.find('Snack')
+    Traceback (most recent call last):
+    ...
+    KeyError: ...'Snack'
 
 
 Development
@@ -122,35 +122,35 @@ Development
 `LookupTree` makes development easy, because `describe` gives a
 complete description of the tree you've created.
 
-  >>> print(tree.describe())
-  tree(
-      branch(Snack => tree(
-          branch('Mars Bar', Snickers => 'Bad')
-          branch(Apple, Banana => 'Good')
-          ))
-      branch(Lunch, Dinner => tree(
-          branch('Fish and chips', "Penne all'arrabbiata" => 'Nice')
-          branch('Raw liver' => 'Not so nice')
-          ))
-      branch(* => 'Make up your mind!')
-      )
+    >>> print(tree.describe())
+    tree(
+        branch(Snack => tree(
+            branch('Mars Bar', Snickers => 'Bad')
+            branch(Apple, Banana => 'Good')
+            ))
+        branch(Lunch, Dinner => tree(
+            branch('Fish and chips', "Penne all'arrabbiata" => 'Nice')
+            branch('Raw liver' => 'Not so nice')
+            ))
+        branch(* => 'Make up your mind!')
+        )
 
 We can also see that the result of constructing a new lookup using an
 existing one is the same as if we had constructed it independently.
 
-  >>> print(breakfast_tree.describe())
-  tree(
-      branch(Breakfast => 'Corn flakes')
-      branch(Snack => tree(
-          branch('Mars Bar', Snickers => 'Bad')
-          branch(Apple, Banana => 'Good')
-          ))
-      branch(Lunch, Dinner => tree(
-          branch('Fish and chips', "Penne all'arrabbiata" => 'Nice')
-          branch('Raw liver' => 'Not so nice')
-          ))
-      branch(* => 'Make up your mind!')
-      )
+    >>> print(breakfast_tree.describe())
+    tree(
+        branch(Breakfast => 'Corn flakes')
+        branch(Snack => tree(
+            branch('Mars Bar', Snickers => 'Bad')
+            branch(Apple, Banana => 'Good')
+            ))
+        branch(Lunch, Dinner => tree(
+            branch('Fish and chips', "Penne all'arrabbiata" => 'Nice')
+            branch('Raw liver' => 'Not so nice')
+            ))
+        branch(* => 'Make up your mind!')
+        )
 
 Simple keys are shown without quotes, to aid readability, and default
 branches are shown with '*' as the key.
@@ -169,20 +169,20 @@ cloned then modified to remove the 'Lunch' key which already appeared
 in the second branch. The default branch is left unchanged; only
 branches with keys are candidates for being discarded.
 
-  >>> pruned_tree = LookupTree(
-  ...     ('Snack', 'Crisps'),
-  ...     ('Lunch', 'Bread'),
-  ...     ('Snack', 'Mars Bar'),
-  ...     ('Lunch', 'Dinner', 'Soup'),
-  ...     ('Eat more fruit and veg',),
-  ...     )
-  >>> print(pruned_tree.describe())
-  tree(
-      branch(Snack => 'Crisps')
-      branch(Lunch => 'Bread')
-      branch(Dinner => 'Soup')
-      branch(* => 'Eat more fruit and veg')
-      )
+    >>> pruned_tree = LookupTree(
+    ...     ('Snack', 'Crisps'),
+    ...     ('Lunch', 'Bread'),
+    ...     ('Snack', 'Mars Bar'),
+    ...     ('Lunch', 'Dinner', 'Soup'),
+    ...     ('Eat more fruit and veg',),
+    ...     )
+    >>> print(pruned_tree.describe())
+    tree(
+        branch(Snack => 'Crisps')
+        branch(Lunch => 'Bread')
+        branch(Dinner => 'Soup')
+        branch(* => 'Eat more fruit and veg')
+        )
 
 
 Documentation
@@ -190,21 +190,21 @@ Documentation
 
 You can discover the minimum and maximum depth of a tree.
 
-  >>> tree.min_depth
-  1
-  >>> tree.max_depth
-  2
+    >>> tree.min_depth
+    1
+    >>> tree.max_depth
+    2
 
 `LookupTree` has a `flatten` method that may be useful when generating
 documentation. It yields tuples of keys that represent paths to
 leaves.
 
-  >>> for elems in tree.flatten():
-  ...     path, result = elems[:-1], elems[-1]
-  ...     print(' => '.join(
-  ...         [pretty(node.keys) for node in path] + [pretty(result)]))
-  ('Snack',) => ('Mars Bar', 'Snickers') => 'Bad'
-  ('Snack',) => ('Apple', 'Banana') => 'Good'
-  ('Lunch', 'Dinner') => ('Fish and chips', "Penne all'arrabbiata") => 'Nice'
-  ('Lunch', 'Dinner') => ('Raw liver',) => 'Not so nice'
-  () => 'Make up your mind!'
+    >>> for elems in tree.flatten():
+    ...     path, result = elems[:-1], elems[-1]
+    ...     print(' => '.join(
+    ...         [pretty(node.keys) for node in path] + [pretty(result)]))
+    ('Snack',) => ('Mars Bar', 'Snickers') => 'Bad'
+    ('Snack',) => ('Apple', 'Banana') => 'Good'
+    ('Lunch', 'Dinner') => ('Fish and chips', "Penne all'arrabbiata") => 'Nice'
+    ('Lunch', 'Dinner') => ('Raw liver',) => 'Not so nice'
+    () => 'Make up your mind!'
diff --git a/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt b/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
index 564d154..caa26a7 100644
--- a/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
+++ b/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
@@ -1,261 +1,262 @@
 We need to login in order to add attachments.
 
-  >>> anon_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.security.interfaces.Unauthorized: ...
+    >>> anon_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.security.interfaces.Unauthorized: ...
 
 When we're logged in we can access the page.
 
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
 
 Let's add an attachment. First create a file-like object.
 
-  >>> from io import BytesIO
-  >>> foo_file = BytesIO(b'Traceback...')
+    >>> from io import BytesIO
+    >>> foo_file = BytesIO(b'Traceback...')
 
 Leading and trailing whitespace are stripped from the description of the
 attachment.
 
-  >>> _ = foo_file.seek(0)
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   foo_file, 'text/plain', 'foo.txt')
-  >>> user_browser.getControl('Description').value = '   Some information   '
-  >>> user_browser.getControl(
-  ...     name="field.comment").value = 'Added some information'
-  >>> user_browser.getControl('Post Comment').click()
+    >>> _ = foo_file.seek(0)
+    >>> user_browser.getControl('Attachment').add_file(
+    ...     foo_file, 'text/plain', 'foo.txt')
+    >>> user_browser.getControl('Description').value = (
+    ...     '   Some information   ')
+    >>> user_browser.getControl(
+    ...     name="field.comment").value = 'Added some information'
+    >>> user_browser.getControl('Post Comment').click()
 
 After we added the attachment, we get redirected to the bug page.
 
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
 
 We can check that the attachment is there
 
-  >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
-  >>> for li_tag in attachments.find_all('li', 'download-attachment'):
-  ...   print(li_tag.a.decode_contents())
-  Some information
+    >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
+    >>> for li_tag in attachments.find_all('li', 'download-attachment'):
+    ...   print(li_tag.a.decode_contents())
+    Some information
 
-  >>> link = user_browser.getLink('Some information')
-  >>> link.url
-  'http://bugs.launchpad.test/firefox/+bug/1/+attachment/.../+files/foo.txt'
+    >>> link = user_browser.getLink('Some information')
+    >>> link.url
+    'http://bugs.launchpad.test/firefox/+bug/1/+attachment/.../+files/foo.txt'
 
-  >>> six.ensure_str('Added some information') in user_browser.contents
-  True
+    >>> six.ensure_str('Added some information') in user_browser.contents
+    True
 
 And that we stripped the leading and trailing whitespace correctly
 
-  >>> six.ensure_str('   Some information   ') in user_browser.contents
-  False
-  >>> six.ensure_str('Some information') in user_browser.contents
-  True
+    >>> six.ensure_str('   Some information   ') in user_browser.contents
+    False
+    >>> six.ensure_str('Some information') in user_browser.contents
+    True
 
 If no description is given it gets set to the attachment filename. It's
 also not necessary to enter a comment in order to add an attachment.
 
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
-  >>> bar_file = BytesIO(b'Traceback...')
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   bar_file, 'text/plain', 'bar.txt')
-  >>> user_browser.getControl('Description').value = ''
-  >>> user_browser.getControl(name="field.comment").value = ''
-  >>> user_browser.getControl('Post Comment').click()
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
-
-  >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
-  >>> for li_tag in attachments.find_all('li', 'download-attachment'):
-  ...   print(li_tag.a.decode_contents())
-  Some information
-  bar.txt
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> bar_file = BytesIO(b'Traceback...')
+    >>> user_browser.getControl('Attachment').add_file(
+    ...   bar_file, 'text/plain', 'bar.txt')
+    >>> user_browser.getControl('Description').value = ''
+    >>> user_browser.getControl(name="field.comment").value = ''
+    >>> user_browser.getControl('Post Comment').click()
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
+
+    >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
+    >>> for li_tag in attachments.find_all('li', 'download-attachment'):
+    ...   print(li_tag.a.decode_contents())
+    Some information
+    bar.txt
 
 We can also declare an attachment to be a patch.
 
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
 
 Leading and trailing whitespace are stripped from the description of the
 attachment.
 
-  >>> _ = foo_file.seek(0)
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   foo_file, 'text/plain', 'foo.diff')
-  >>> user_browser.getControl('Description').value = 'A fix for this bug.'
-  >>> patch_control = user_browser.getControl(
-  ...     'This attachment contains a solution (patch) for this bug')
-  >>> patch_control.selected = True
-  >>> user_browser.getControl(
-  ...     name="field.comment").value = 'Added some information'
-  >>> user_browser.getControl('Post Comment').click()
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> _ = foo_file.seek(0)
+    >>> user_browser.getControl('Attachment').add_file(
+    ...   foo_file, 'text/plain', 'foo.diff')
+    >>> user_browser.getControl('Description').value = 'A fix for this bug.'
+    >>> patch_control = user_browser.getControl(
+    ...     'This attachment contains a solution (patch) for this bug')
+    >>> patch_control.selected = True
+    >>> user_browser.getControl(
+    ...     name="field.comment").value = 'Added some information'
+    >>> user_browser.getControl('Post Comment').click()
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
 
 If we add an attachment that looks like a patch but if we don't set
 the flag "this attachment is a patch"...
 
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
-  >>> _ = foo_file.seek(0)
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   foo_file, 'text/plain', 'foo2.diff')
-  >>> user_browser.getControl('Description').value = 'More data'
-  >>> patch_control = user_browser.getControl(
-  ...     'This attachment contains a solution (patch) for this bug')
-  >>> patch_control.selected = False
-  >>> user_browser.getControl(
-  ...     name="field.comment").value = 'Added even more information'
-  >>> user_browser.getControl('Post Comment').click()
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> _ = foo_file.seek(0)
+    >>> user_browser.getControl('Attachment').add_file(
+    ...   foo_file, 'text/plain', 'foo2.diff')
+    >>> user_browser.getControl('Description').value = 'More data'
+    >>> patch_control = user_browser.getControl(
+    ...     'This attachment contains a solution (patch) for this bug')
+    >>> patch_control.selected = False
+    >>> user_browser.getControl(
+    ...     name="field.comment").value = 'Added even more information'
+    >>> user_browser.getControl('Post Comment').click()
 
 ...we are redirected to a page...
 
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1/+attachment/.../+confirm-is-patch'
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1/+attachment/.../+confirm-is-patch'
 
 ...where we see a message that we should double-check if this file
 is indeed not a patch.
 
-  >>> print(extract_text(find_tags_by_class(
-  ...     user_browser.contents, 'documentDescription')[0]))
-  This file looks like a patch.
-  What is a patch?
+    >>> print(extract_text(find_tags_by_class(
+    ...     user_browser.contents, 'documentDescription')[0]))
+    This file looks like a patch.
+    What is a patch?
 
 Also, we have "yes"/"no" radio buttons to answer the question "Is this a
 patch?". The currently selected radio button is "yes".
 
-  >>> patch_control_yes = user_browser.getControl('yes')
-  >>> patch_control_yes.selected
-  True
-  >>> patch_control_no = user_browser.getControl('no')
-  >>> patch_control_no.selected
-  False
+    >>> patch_control_yes = user_browser.getControl('yes')
+    >>> patch_control_yes.selected
+    True
+    >>> patch_control_no = user_browser.getControl('no')
+    >>> patch_control_no.selected
+    False
 
 We want indeed to declare the file as not being a patch, so we unselect
 the "patch" checkbox again and submit the form.
 
-  >>> patch_control_no.selected = True
-  >>> user_browser.getControl('Change').click()
+    >>> patch_control_no.selected = True
+    >>> user_browser.getControl('Change').click()
 
 Now we are redirected to the main bug page, and the new file is
 listed as an ordinary attachment.
 
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
-  >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
-  >>> for li_tag in attachments.find_all('li', 'download-attachment'):
-  ...   print(li_tag.a.decode_contents())
-  Some information
-  bar.txt
-  More data
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
+    >>> for li_tag in attachments.find_all('li', 'download-attachment'):
+    ...   print(li_tag.a.decode_contents())
+    Some information
+    bar.txt
+    More data
 
 Similary, if we add an attachment that does not look like a patch and
 if we set the "patch" flag for this attachment...
 
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
-  >>> _ = foo_file.seek(0)
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   foo_file, 'text/plain', 'foo.png')
-  >>> user_browser.getControl('Description').value = 'A better icon for foo'
-  >>> patch_control = user_browser.getControl(
-  ...     'This attachment contains a solution (patch) for this bug')
-  >>> patch_control.selected = True
-  >>> user_browser.getControl('Post Comment').click()
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> _ = foo_file.seek(0)
+    >>> user_browser.getControl('Attachment').add_file(
+    ...   foo_file, 'text/plain', 'foo.png')
+    >>> user_browser.getControl('Description').value = 'A better icon for foo'
+    >>> patch_control = user_browser.getControl(
+    ...     'This attachment contains a solution (patch) for this bug')
+    >>> patch_control.selected = True
+    >>> user_browser.getControl('Post Comment').click()
 
 ...we are redirected to the page where we must confirm that this attachment
 is indeed a patch.
 
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1/+attachment/.../+confirm-is-patch'
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1/+attachment/.../+confirm-is-patch'
 
 ...where we see a message asking us if we really ant to declare this file
 as a patch.
 
-  >>> print(extract_text(find_tags_by_class(
-  ...     user_browser.contents, 'documentDescription')[0]))
-  This file does not look like a patch.
-  What is a patch?
+    >>> print(extract_text(find_tags_by_class(
+    ...     user_browser.contents, 'documentDescription')[0]))
+    This file does not look like a patch.
+    What is a patch?
 
 Also, the "patch" flag is not yet set.
 
-  >>> patch_control_yes = user_browser.getControl('yes')
-  >>> patch_control_yes.selected
-  False
-  >>> patch_control_no = user_browser.getControl('no')
-  >>> patch_control_no.selected
-  True
+    >>> patch_control_yes = user_browser.getControl('yes')
+    >>> patch_control_yes.selected
+    False
+    >>> patch_control_no = user_browser.getControl('no')
+    >>> patch_control_no.selected
+    True
 
 Let's pretend that the file contains an improved icon, so we set
 the "patch" flag again and save the changes.
 
-  >>> patch_control_yes.selected = True
-  >>> user_browser.getControl('Change').click()
+    >>> patch_control_yes.selected = True
+    >>> user_browser.getControl('Change').click()
 
 Now we are redirected to the main bug page...
 
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
 
 ...and the new attachment is listed as a patch.
 
-  >>> patches = find_portlet(user_browser.contents, 'Patches')
-  >>> for li_tag in patches.find_all('li', 'download-attachment'):
-  ...   print(li_tag.a.decode_contents())
-  A fix for this bug.
-  A better icon for foo
+    >>> patches = find_portlet(user_browser.contents, 'Patches')
+    >>> for li_tag in patches.find_all('li', 'download-attachment'):
+    ...   print(li_tag.a.decode_contents())
+    A fix for this bug.
+    A better icon for foo
 
 We expect Launchpad to believe us (that is, not ask for confirmation)
 when we tell it that plain text files whose names end in ".diff",
 ".debdiff", or ".patch" are patch attachments:
 
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
-  >>> _ = foo_file.seek(0)
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   foo_file, 'text/plain', 'foo3.diff')
-  >>> user_browser.getControl('Description').value = 'the foo3 patch'
-  >>> patch_control = user_browser.getControl(
-  ...     'This attachment contains a solution (patch) for this bug')
-  >>> patch_control.selected = True
-  >>> user_browser.getControl(
-  ...     name="field.comment").value = 'Add foo3.diff as a patch.'
-  >>> user_browser.getControl('Post Comment').click()
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
-
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
-  >>> _ = foo_file.seek(0)
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   foo_file, 'text/plain', 'foo4.debdiff')
-  >>> user_browser.getControl('Description').value = 'the foo4 patch'
-  >>> patch_control = user_browser.getControl(
-  ...     'This attachment contains a solution (patch) for this bug')
-  >>> patch_control.selected = True
-  >>> user_browser.getControl(
-  ...     name="field.comment").value = 'Add foo4.debdiff as a patch.'
-  >>> user_browser.getControl('Post Comment').click()
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
-
-  >>> user_browser.open(
-  ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
-  >>> _ = foo_file.seek(0)
-  >>> user_browser.getControl('Attachment').add_file(
-  ...   foo_file, 'text/plain', 'foo5.patch')
-  >>> user_browser.getControl('Description').value = 'the foo5 patch'
-  >>> patch_control = user_browser.getControl(
-  ...     'This attachment contains a solution (patch) for this bug')
-  >>> patch_control.selected = True
-  >>> user_browser.getControl(
-  ...     name="field.comment").value = 'Add foo5.patch as a patch.'
-  >>> user_browser.getControl('Post Comment').click()
-  >>> user_browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> _ = foo_file.seek(0)
+    >>> user_browser.getControl('Attachment').add_file(
+    ...   foo_file, 'text/plain', 'foo3.diff')
+    >>> user_browser.getControl('Description').value = 'the foo3 patch'
+    >>> patch_control = user_browser.getControl(
+    ...     'This attachment contains a solution (patch) for this bug')
+    >>> patch_control.selected = True
+    >>> user_browser.getControl(
+    ...     name="field.comment").value = 'Add foo3.diff as a patch.'
+    >>> user_browser.getControl('Post Comment').click()
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
+
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> _ = foo_file.seek(0)
+    >>> user_browser.getControl('Attachment').add_file(
+    ...   foo_file, 'text/plain', 'foo4.debdiff')
+    >>> user_browser.getControl('Description').value = 'the foo4 patch'
+    >>> patch_control = user_browser.getControl(
+    ...     'This attachment contains a solution (patch) for this bug')
+    >>> patch_control.selected = True
+    >>> user_browser.getControl(
+    ...     name="field.comment").value = 'Add foo4.debdiff as a patch.'
+    >>> user_browser.getControl('Post Comment').click()
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
+
+    >>> user_browser.open(
+    ...     'http://bugs.launchpad.test/firefox/+bug/1/+addcomment')
+    >>> _ = foo_file.seek(0)
+    >>> user_browser.getControl('Attachment').add_file(
+    ...   foo_file, 'text/plain', 'foo5.patch')
+    >>> user_browser.getControl('Description').value = 'the foo5 patch'
+    >>> patch_control = user_browser.getControl(
+    ...     'This attachment contains a solution (patch) for this bug')
+    >>> patch_control.selected = True
+    >>> user_browser.getControl(
+    ...     name="field.comment").value = 'Add foo5.patch as a patch.'
+    >>> user_browser.getControl('Post Comment').click()
+    >>> user_browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
 
 We can also edit the attachment details, let's navigate to that page.
 
diff --git a/lib/lp/bugs/stories/bugs/xx-add-comment-with-bugwatch-and-cve.txt b/lib/lp/bugs/stories/bugs/xx-add-comment-with-bugwatch-and-cve.txt
index 147cc45..d08c361 100644
--- a/lib/lp/bugs/stories/bugs/xx-add-comment-with-bugwatch-and-cve.txt
+++ b/lib/lp/bugs/stories/bugs/xx-add-comment-with-bugwatch-and-cve.txt
@@ -5,30 +5,30 @@ When a comment is added to a bug, links to "remote" bug reports and CVEs are
 added to the bugwatches resp CVEs related to this bug
 
 
-  >>> user_browser.open(
-  ...     'http://localhost/debian/+source/mozilla-firefox/+bug/1')
-  >>> user_browser.getControl(name='field.comment').value = (
-  ...     '''This is a test comment. This bug is the same as the one
-  ...        described here http://some.bugzilla/show_bug.cgi?id=9876
-  ...        See also CVE-1991-9911
-  ...     ''')
-  >>> user_browser.getControl('Post Comment', index=-1).click()
-
-  >>> user_browser.url
-  'http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/1'
-
-  >>> added_cve_link = user_browser.getLink('1991-9911')
-  >>> print(added_cve_link)
-  <.../bugs/cve/1991-9911'>
-
-  >>> bugwatch_portlet = find_portlet(user_browser.contents,
-  ...    'Remote bug watches')
-  >>> added_bugwatch_link = bugwatch_portlet('a')[-2]
-  >>> print(added_bugwatch_link['href'])
-  http://some.bugzilla/show_bug.cgi?id=9876
-
-  >>> print(extract_text(added_bugwatch_link))
-  auto-some.bugzilla #9876
+    >>> user_browser.open(
+    ...     'http://localhost/debian/+source/mozilla-firefox/+bug/1')
+    >>> user_browser.getControl(name='field.comment').value = (
+    ...     '''This is a test comment. This bug is the same as the one
+    ...        described here http://some.bugzilla/show_bug.cgi?id=9876
+    ...        See also CVE-1991-9911
+    ...     ''')
+    >>> user_browser.getControl('Post Comment', index=-1).click()
+
+    >>> user_browser.url
+    'http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/1'
+
+    >>> added_cve_link = user_browser.getLink('1991-9911')
+    >>> print(added_cve_link)
+    <.../bugs/cve/1991-9911'>
+
+    >>> bugwatch_portlet = find_portlet(user_browser.contents,
+    ...    'Remote bug watches')
+    >>> added_bugwatch_link = bugwatch_portlet('a')[-2]
+    >>> print(added_bugwatch_link['href'])
+    http://some.bugzilla/show_bug.cgi?id=9876
+
+    >>> print(extract_text(added_bugwatch_link))
+    auto-some.bugzilla #9876
 
 When extracting the remote bug URLs, we can use whatever text we want and
 place the URLs anywhere within this text. Only valid URIs that look like
@@ -37,18 +37,18 @@ a comment, both on the same line, one surrounded by non-ascii characters.
 One of these URLs looks like a remote bugzilla bug, the other is not the
 url of a remote bug.
 
-  >>> user_browser.getControl(name='field.comment').value = (
-  ...     u'\xabhttps://answers.launchpad.net/ubuntu\xbb is not a linked bug '
-  ...     u'but https://bugzilla.example.org/show_bug.cgi?id=1235555 '
-  ...     u'is.'.encode('utf-8'))
-  >>> user_browser.getControl('Post Comment', index=-1).click()
-  >>> user_browser.url
-  'http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/1'
-
-  >>> bugwatch_portlet = find_portlet(user_browser.contents, 
-  ...    'Remote bug watches')
-  >>> added_bugwatch_link = bugwatch_portlet('a')[-2]
-  >>> print(extract_text(added_bugwatch_link))
-  auto-bugzilla.example.org #1235555
-  >>> 'answers.launchpad.net' in extract_text(bugwatch_portlet)
-  False
+    >>> user_browser.getControl(name='field.comment').value = (
+    ...     u'\xabhttps://answers.launchpad.net/ubuntu\xbb is not a linked '
+    ...     u'bug but https://bugzilla.example.org/show_bug.cgi?id=1235555 '
+    ...     u'is.'.encode('utf-8'))
+    >>> user_browser.getControl('Post Comment', index=-1).click()
+    >>> user_browser.url
+    'http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/1'
+
+    >>> bugwatch_portlet = find_portlet(user_browser.contents, 
+    ...    'Remote bug watches')
+    >>> added_bugwatch_link = bugwatch_portlet('a')[-2]
+    >>> print(extract_text(added_bugwatch_link))
+    auto-bugzilla.example.org #1235555
+    >>> 'answers.launchpad.net' in extract_text(bugwatch_portlet)
+    False
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt b/lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt
index 33ce24b..3af8446 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt
@@ -4,67 +4,67 @@ Marking a bug as affecting the user
 Users can mark bugs as affecting them. Let's create a sample bug to
 try this out.
 
-   >>> login(ANONYMOUS)
-   >>> from lp.services.webapp import canonical_url
-   >>> test_bug = factory.makeBug()
-   >>> test_bug_url = canonical_url(test_bug)
-   >>> logout()
+    >>> login(ANONYMOUS)
+    >>> from lp.services.webapp import canonical_url
+    >>> test_bug = factory.makeBug()
+    >>> test_bug_url = canonical_url(test_bug)
+    >>> logout()
 
 The user goes to the bug's index page, and finds a statement that the
 bug affects one other person (in this instance, the person who filed
 the bug).
 
-   >>> user_browser.open(test_bug_url)
-   >>> print(extract_text(find_tag_by_id(
-   ...     user_browser.contents, 'affectsmetoo').find(
-   ...         None, 'static')))
-   This bug affects 1 person. Does this bug affect you?
+    >>> user_browser.open(test_bug_url)
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'affectsmetoo').find(
+    ...         None, 'static')))
+    This bug affects 1 person. Does this bug affect you?
 
 Next to the statement is a link containing an edit icon.
 
-   >>> edit_link = find_tag_by_id(
-   ...     user_browser.contents, 'affectsmetoo').a
-   >>> print(edit_link['href'])
-   +affectsmetoo
-   >>> print(edit_link.img['src'])
-   /@@/edit
+    >>> edit_link = find_tag_by_id(
+    ...     user_browser.contents, 'affectsmetoo').a
+    >>> print(edit_link['href'])
+    +affectsmetoo
+    >>> print(edit_link.img['src'])
+    /@@/edit
 
 The user is affected by this bug, so clicks the link.
 
-   >>> user_browser.getLink(url='+affectsmetoo').click()
-   >>> print(user_browser.url)
-   http://bugs.launchpad.test/.../+bug/.../+affectsmetoo
-   >>> user_browser.getControl(name='field.affects').value
-   ['YES']
+    >>> user_browser.getLink(url='+affectsmetoo').click()
+    >>> print(user_browser.url)
+    http://bugs.launchpad.test/.../+bug/.../+affectsmetoo
+    >>> user_browser.getControl(name='field.affects').value
+    ['YES']
 
 The form defaults to 'Yes', and the user submits the form.
 
-   >>> user_browser.getControl('Change').click()
+    >>> user_browser.getControl('Change').click()
 
 The bug page loads again, and now the text is changed, to make it
 clear to the user that they have marked this bug as affecting them.
 
-   >>> print(extract_text(find_tag_by_id(
-   ...     user_browser.contents, 'affectsmetoo').find(
-   ...         None, 'static')))
-   This bug affects you and 1 other person
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'affectsmetoo').find(
+    ...         None, 'static')))
+    This bug affects you and 1 other person
 
 On second thoughts, the user realises that this bug does not affect
 them, so they click on the edit link once more.
 
-   >>> user_browser.getLink(url='+affectsmetoo').click()
+    >>> user_browser.getLink(url='+affectsmetoo').click()
 
 The user changes their selection to 'No' and submits the form.
 
-   >>> user_browser.getControl(name='field.affects').value = ['NO']
-   >>> user_browser.getControl('Change').click()
+    >>> user_browser.getControl(name='field.affects').value = ['NO']
+    >>> user_browser.getControl('Change').click()
 
 Back at the bug page, the text changes once again.
 
-   >>> print(extract_text(find_tag_by_id(
-   ...     user_browser.contents, 'affectsmetoo').find(
-   ...         None, 'static')))
-   This bug affects 1 person, but not you
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'affectsmetoo').find(
+    ...         None, 'static')))
+    This bug affects 1 person, but not you
 
 
 Anonymous users
@@ -72,21 +72,21 @@ Anonymous users
 
 Anonymous users just see the number of affected users.
 
-   >>> anon_browser.open(test_bug_url)
-   >>> print(extract_text(find_tag_by_id(
-   ...     anon_browser.contents, 'affectsmetoo')))
-   This bug affects 1 person
+    >>> anon_browser.open(test_bug_url)
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'affectsmetoo')))
+    This bug affects 1 person
 
 If no one is marked as affected by the bug, the message does not
 appear at all to anonymous users.
 
-   >>> login('test@xxxxxxxxxxxxx')
-   >>> test_bug.markUserAffected(test_bug.owner, False)
-   >>> logout()
+    >>> login('test@xxxxxxxxxxxxx')
+    >>> test_bug.markUserAffected(test_bug.owner, False)
+    >>> logout()
 
-   >>> anon_browser.open(test_bug_url)
-   >>> print(find_tag_by_id(anon_browser.contents, 'affectsmetoo'))
-   None
+    >>> anon_browser.open(test_bug_url)
+    >>> print(find_tag_by_id(anon_browser.contents, 'affectsmetoo'))
+    None
 
 
 Static and dynamic support
@@ -95,33 +95,33 @@ Static and dynamic support
 A bug page contains markup to support both static (no Javascript) and
 dynamic (Javascript enabled) scenarios.
 
-   >>> def class_filter(css_class):
-   ...     def test(node):
-   ...         return css_class in node.get('class', [])
-   ...     return test
+    >>> def class_filter(css_class):
+    ...     def test(node):
+    ...         return css_class in node.get('class', [])
+    ...     return test
 
-   >>> static_content = find_tag_by_id(
-   ...     user_browser.contents, 'affectsmetoo').find(
-   ...         class_filter('static'))
+    >>> static_content = find_tag_by_id(
+    ...     user_browser.contents, 'affectsmetoo').find(
+    ...         class_filter('static'))
 
-   >>> static_content is not None
-   True
+    >>> static_content is not None
+    True
 
-   >>> dynamic_content = find_tag_by_id(
-   ...     user_browser.contents, 'affectsmetoo').find(
-   ...         class_filter('dynamic'))
+    >>> dynamic_content = find_tag_by_id(
+    ...     user_browser.contents, 'affectsmetoo').find(
+    ...         class_filter('dynamic'))
 
-   >>> dynamic_content is not None
-   True
+    >>> dynamic_content is not None
+    True
 
 The dynamic content is hidden by the presence of the "hidden" CSS
 class.
 
-   >>> print(' '.join(static_content.get('class')))
-   static
+    >>> print(' '.join(static_content.get('class')))
+    static
 
-   >>> print(' '.join(dynamic_content.get('class')))
-   dynamic hidden
+    >>> print(' '.join(dynamic_content.get('class')))
+    dynamic hidden
 
 It is the responsibilty of Javascript running in the page to unhide
 the dynamic content and hide the static content.
diff --git a/lib/lp/bugs/stories/bugs/xx-bugs.txt b/lib/lp/bugs/stories/bugs/xx-bugs.txt
index 0dd58c7..e07dec7 100644
--- a/lib/lp/bugs/stories/bugs/xx-bugs.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bugs.txt
@@ -19,26 +19,26 @@ comment to a bug.
 In this case, let's add a simple comment to bug #2 as user Foo
 Bar. First, let's clear out the notification table:
 
-  >>> from lp.bugs.model.bugnotification import BugNotification
-  >>> from lp.services.database.interfaces import IStore
-  >>> store = IStore(BugNotification)
-  >>> store.execute("DELETE FROM BugNotification", noresult=True)
+    >>> from lp.bugs.model.bugnotification import BugNotification
+    >>> from lp.services.database.interfaces import IStore
+    >>> store = IStore(BugNotification)
+    >>> store.execute("DELETE FROM BugNotification", noresult=True)
 
-  >>> user_browser.open(
-  ...     'http://localhost/debian/+source/mozilla-firefox/+bug/2')
-  >>> user_browser.getControl(name='field.comment').value = (
-  ...     'This is a test comment.')
-  >>> user_browser.getControl('Post Comment', index=-1).click()
+    >>> user_browser.open(
+    ...     'http://localhost/debian/+source/mozilla-firefox/+bug/2')
+    >>> user_browser.getControl(name='field.comment').value = (
+    ...     'This is a test comment.')
+    >>> user_browser.getControl('Post Comment', index=-1).click()
 
-  >>> user_browser.url
-  'http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/2'
+    >>> user_browser.url
+    'http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/2'
 
-  >>> print(user_browser.contents)
-  <...
-  ...This is a test comment...
+    >>> print(user_browser.contents)
+    <...
+    ...This is a test comment...
 
 
 After the comment has been submitted, a notification is added:
 
-  >>> IStore(BugNotification).find(BugNotification).count()
-  1
+    >>> IStore(BugNotification).find(BugNotification).count()
+    1
diff --git a/lib/lp/bugs/stories/bugs/xx-bugtask-assignee-widget.txt b/lib/lp/bugs/stories/bugs/xx-bugtask-assignee-widget.txt
index 5001cee..cff92cf 100644
--- a/lib/lp/bugs/stories/bugs/xx-bugtask-assignee-widget.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bugtask-assignee-widget.txt
@@ -1,233 +1,233 @@
 The bug task edit page now features a new and improved assignee
 widget, which makes it easier to "take" a task.
 
-  >>> print(http(br"""
-  ... GET /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...Assigned to...
-  ...nobody...
-  ...me...
+    >>> print(http(br"""
+    ... GET /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...Assigned to...
+    ...nobody...
+    ...me...
 
 So, taking the task is now as simple as selecting the "me" radio
 button:
 
-  >>> print(http(br"""
-  ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Referer: https://launchpad.test/
-  ... Content-Type: multipart/form-data; boundary=---------------------------19759086281403130373932339922
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status"
-  ... 
-  ... Confirmed
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance"
-  ... 
-  ... Low
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.sourcepackagename"
-  ... 
-  ... mozilla-firefox
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee.option"
-  ... 
-  ... ubuntu_mozilla-firefox.assignee.assign_to_me
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.actions.save"
-  ... 
-  ... Save Changes
-  ... -----------------------------19759086281403130373932339922--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
+    >>> print(http(br"""
+    ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Referer: https://launchpad.test/
+    ... Content-Type: multipart/form-data; boundary=---------------------------19759086281403130373932339922
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status"
+    ... 
+    ... Confirmed
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance"
+    ... 
+    ... Low
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.sourcepackagename"
+    ... 
+    ... mozilla-firefox
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee.option"
+    ... 
+    ... ubuntu_mozilla-firefox.assignee.assign_to_me
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.actions.save"
+    ... 
+    ... Save Changes
+    ... -----------------------------19759086281403130373932339922--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
 
 In this example, we were logged in as Foo Bar, so the task is now
 automagically assigned to Foo Bar.
 
-  >>> print(http(br"""
-  ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...mozilla-firefox (Ubuntu)...Foo Bar...
-  ...
+    >>> print(http(br"""
+    ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...mozilla-firefox (Ubuntu)...Foo Bar...
+    ...
 
 But, you can also assign the task to another person, of course:
 
-  >>> print(http(br"""
-  ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Referer: https://launchpad.test/
-  ... Content-Length: 1999
-  ... Content-Type: multipart/form-data; boundary=---------------------------19759086281403130373932339922
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status"
-  ... 
-  ... Confirmed
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance"
-  ... 
-  ... Low
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.sourcepackagename"
-  ... 
-  ... mozilla-firefox
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee.option"
-  ... 
-  ... ubuntu_mozilla-firefox.assignee.assign_to
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee"
-  ... 
-  ... name12
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.actions.save"
-  ... 
-  ... Save Changes
-  ... -----------------------------19759086281403130373932339922--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
+    >>> print(http(br"""
+    ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Referer: https://launchpad.test/
+    ... Content-Length: 1999
+    ... Content-Type: multipart/form-data; boundary=---------------------------19759086281403130373932339922
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status"
+    ... 
+    ... Confirmed
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance"
+    ... 
+    ... Low
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.sourcepackagename"
+    ... 
+    ... mozilla-firefox
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee.option"
+    ... 
+    ... ubuntu_mozilla-firefox.assignee.assign_to
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee"
+    ... 
+    ... name12
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.actions.save"
+    ... 
+    ... Save Changes
+    ... -----------------------------19759086281403130373932339922--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
 
 In this case, we assigned the task to Sample Person:
 
-  >>> print(http(br"""
-  ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...mozilla-firefox (Ubuntu)...Sample Person...
-  ...
+    >>> print(http(br"""
+    ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...mozilla-firefox (Ubuntu)...Sample Person...
+    ...
 
 Lastly, the widget also allows you to simply assign the task to nobody
 (to, "give up" the task, you might say)
 
-  >>> print(http(br"""
-  ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Referer: https://launchpad.test/
-  ... Content-Length: 1999
-  ... Content-Type: multipart/form-data; boundary=---------------------------19759086281403130373932339922
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status"
-  ... 
-  ... Confirmed
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance"
-  ... 
-  ... Low
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.sourcepackagename"
-  ... 
-  ... mozilla-firefox
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee.option"
-  ... 
-  ... ubuntu_mozilla-firefox.assignee.assign_to_nobody
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch"
-  ... 
-  ... 
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------19759086281403130373932339922
-  ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.actions.save"
-  ... 
-  ... Save Changes
-  ... -----------------------------19759086281403130373932339922--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
+    >>> print(http(br"""
+    ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Referer: https://launchpad.test/
+    ... Content-Length: 1999
+    ... Content-Type: multipart/form-data; boundary=---------------------------19759086281403130373932339922
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status"
+    ... 
+    ... Confirmed
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.status-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance"
+    ... 
+    ... Low
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.importance-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.milestone-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.sourcepackagename"
+    ... 
+    ... mozilla-firefox
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee.option"
+    ... 
+    ... ubuntu_mozilla-firefox.assignee.assign_to_nobody
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.assignee"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch"
+    ... 
+    ... 
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.bugwatch-empty-marker"
+    ... 
+    ... 1
+    ... -----------------------------19759086281403130373932339922
+    ... Content-Disposition: form-data; name="ubuntu_mozilla-firefox.actions.save"
+    ... 
+    ... Save Changes
+    ... -----------------------------19759086281403130373932339922--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
 
 And now the bug task is unassigned:
 
-  >>> print(http(br"""
-  ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...mozilla-firefox (Ubuntu)...
-  ...
+    >>> print(http(br"""
+    ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...mozilla-firefox (Ubuntu)...
+    ...
diff --git a/lib/lp/bugs/stories/bugs/xx-portlets-bug-series.txt b/lib/lp/bugs/stories/bugs/xx-portlets-bug-series.txt
index 207adb8..9c16dab 100644
--- a/lib/lp/bugs/stories/bugs/xx-portlets-bug-series.txt
+++ b/lib/lp/bugs/stories/bugs/xx-portlets-bug-series.txt
@@ -4,59 +4,59 @@ been accepted as targeting a specific series of a distribution:
 This portlet is not available from a distribution's bug page if it
 does not use Launchpad for tracking bugs.
 
-  >>> anon_browser.open("http://bugs.launchpad.test/debian/+bugs";)
-  >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print(portlet)
-  None
+    >>> anon_browser.open("http://bugs.launchpad.test/debian/+bugs";)
+    >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
+    >>> print(portlet)
+    None
 
 Change debian to track bugs in Launchpad and the portlet becomes visible.
 
-  >>> from lp.testing.service_usage_helpers import set_service_usage
-  >>> set_service_usage('debian', bug_tracking_usage='LAUNCHPAD')
-
-  >>> anon_browser.open("http://bugs.launchpad.test/debian/+bugs";)
-  >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print(extract_text(portlet))
-  Series-targeted bugs
-  1
-  sarge
-  2
-  woody
-
-  >>> anon_browser.open("http://bugs.launchpad.test/debian/sarge/+bugs";)
-  >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print(extract_text(portlet))
-  Series-targeted bugs
-  1
-  sarge
-  2
-  woody
-
-  >>> anon_browser.open("http://bugs.launchpad.test/ubuntu/+bugs";)
-  >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print(extract_text(portlet))
-  Series-targeted bugs
-  1
-  hoary
-  1
-  warty
-
-  >>> print(anon_browser.getLink("hoary").url)
-  http://bugs.launchpad.test/ubuntu/hoary/+bugs
+    >>> from lp.testing.service_usage_helpers import set_service_usage
+    >>> set_service_usage('debian', bug_tracking_usage='LAUNCHPAD')
+
+    >>> anon_browser.open("http://bugs.launchpad.test/debian/+bugs";)
+    >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
+    >>> print(extract_text(portlet))
+    Series-targeted bugs
+    1
+    sarge
+    2
+    woody
+
+    >>> anon_browser.open("http://bugs.launchpad.test/debian/sarge/+bugs";)
+    >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
+    >>> print(extract_text(portlet))
+    Series-targeted bugs
+    1
+    sarge
+    2
+    woody
+
+    >>> anon_browser.open("http://bugs.launchpad.test/ubuntu/+bugs";)
+    >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
+    >>> print(extract_text(portlet))
+    Series-targeted bugs
+    1
+    hoary
+    1
+    warty
+
+    >>> print(anon_browser.getLink("hoary").url)
+    http://bugs.launchpad.test/ubuntu/hoary/+bugs
 
 The same portlet is also available for project and project series
 listings and homepages:
 
-  >>> anon_browser.open("http://bugs.launchpad.test/firefox/+bugs";)
-  >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print(extract_text(portlet))
-  Series-targeted bugs
-  1
-  1.0
-
-  >>> anon_browser.open("http://bugs.launchpad.test/firefox/1.0/+bugs";)
-  >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print(extract_text(portlet))
-  Series-targeted bugs
-  1
-  1.0
+    >>> anon_browser.open("http://bugs.launchpad.test/firefox/+bugs";)
+    >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
+    >>> print(extract_text(portlet))
+    Series-targeted bugs
+    1
+    1.0
+
+    >>> anon_browser.open("http://bugs.launchpad.test/firefox/1.0/+bugs";)
+    >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
+    >>> print(extract_text(portlet))
+    Series-targeted bugs
+    1
+    1.0
diff --git a/lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt b/lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt
index c641b85..0e9753f 100644
--- a/lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt
+++ b/lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt
@@ -11,40 +11,40 @@ use a URL of the form /bugs/bugtrackers/$bugtrackername/$remotebug.
 If there are multiple Launchpad bugs watching a particular remote bug,
 then a list of the relevant Launchpad bugs:
 
-  >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/42')
-
-  >>> print_location(browser.contents)
-  Hierarchy: Bug trackers > The Mozilla.org Bug Tracker
-  Tabs:
-  * Launchpad Home - http://launchpad.test/
-  * Code - http://code.launchpad.test/
-  * Bugs (selected) - http://bugs.launchpad.test/
-  * Blueprints - http://blueprints.launchpad.test/
-  * Translations - http://translations.launchpad.test/
-  * Answers - http://answers.launchpad.test/
-  Main heading: Remote Bug #42 in The Mozilla.org Bug Tracker
-
-  >>> print(extract_text(find_tag_by_id(browser.contents, 'watchedbugs')))
-  Bug #1: Firefox does not support SVG
-  Bug #2: Blackhole Trash folder
+    >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/42')
+
+    >>> print_location(browser.contents)
+    Hierarchy: Bug trackers > The Mozilla.org Bug Tracker
+    Tabs:
+    * Launchpad Home - http://launchpad.test/
+    * Code - http://code.launchpad.test/
+    * Bugs (selected) - http://bugs.launchpad.test/
+    * Blueprints - http://blueprints.launchpad.test/
+    * Translations - http://translations.launchpad.test/
+    * Answers - http://answers.launchpad.test/
+    Main heading: Remote Bug #42 in The Mozilla.org Bug Tracker
+
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'watchedbugs')))
+    Bug #1: Firefox does not support SVG
+    Bug #2: Blackhole Trash folder
 
 If there is only a single bug watching the remote bug, then we skip
 the list page and redirect the user directly to that bug's page:
 
-  >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/2000')
-  >>> print(browser.url)
-  http://bugs.launchpad.test/firefox/+bug/1
+    >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/2000')
+    >>> print(browser.url)
+    http://bugs.launchpad.test/firefox/+bug/1
 
 If there are no bug watches for a particular remote bug, then a Not
 Found page is generated:
 
-  >>> browser.handleErrors = True
-  >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/99999')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  urllib.error.HTTPError: HTTP Error 404: Not Found
-  >>> browser.handleErrors = False
+    >>> browser.handleErrors = True
+    >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/99999')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    urllib.error.HTTPError: HTTP Error 404: Not Found
+    >>> browser.handleErrors = False
 
 
 Private Bugs
@@ -55,55 +55,56 @@ particular remote bug, we do not expose the title of the remote bug.
 
 Mark bug 1 as private:
 
-  >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
-  >>> browser.open('http://bugs.launchpad.test/firefox/+bug/1/+secrecy')
-  >>> browser.getControl('Private', index=1).selected = True
-  >>> browser.getControl('Change').click()
-  >>> browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
+    >>> browser.open('http://bugs.launchpad.test/firefox/+bug/1/+secrecy')
+    >>> browser.getControl('Private', index=1).selected = True
+    >>> browser.getControl('Change').click()
+    >>> browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
 
 List Launchpad bugs watching Mozilla bug 42:
 
-  >>> anon_browser.open(
-  ...     'http://launchpad.test/bugs/bugtrackers/mozilla.org/42')
-
-  >>> print_location(anon_browser.contents)
-  Hierarchy: Bug trackers > The Mozilla.org Bug Tracker
-  Tabs:
-  * Launchpad Home - http://launchpad.test/
-  * Code - http://code.launchpad.test/
-  * Bugs (selected) - http://bugs.launchpad.test/
-  * Blueprints - http://blueprints.launchpad.test/
-  * Translations - http://translations.launchpad.test/
-  * Answers - http://answers.launchpad.test/
-  Main heading: Remote Bug #42 in The Mozilla.org Bug Tracker
-
-  >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'watchedbugs')))
-  Bug #1: (Private)
-  Bug #2: Blackhole Trash folder
+    >>> anon_browser.open(
+    ...     'http://launchpad.test/bugs/bugtrackers/mozilla.org/42')
+
+    >>> print_location(anon_browser.contents)
+    Hierarchy: Bug trackers > The Mozilla.org Bug Tracker
+    Tabs:
+    * Launchpad Home - http://launchpad.test/
+    * Code - http://code.launchpad.test/
+    * Bugs (selected) - http://bugs.launchpad.test/
+    * Blueprints - http://blueprints.launchpad.test/
+    * Translations - http://translations.launchpad.test/
+    * Answers - http://answers.launchpad.test/
+    Main heading: Remote Bug #42 in The Mozilla.org Bug Tracker
+
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'watchedbugs')))
+    Bug #1: (Private)
+    Bug #2: Blackhole Trash folder
 
 The bug title is still provided if the user can view the private bug:
 
-  >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/42')
-  >>> print(extract_text(find_tag_by_id(browser.contents, 'watchedbugs')))
-  Bug #1: Firefox does not support SVG
-  Bug #2: Blackhole Trash folder
+    >>> browser.open('http://launchpad.test/bugs/bugtrackers/mozilla.org/42')
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'watchedbugs')))
+    Bug #1: Firefox does not support SVG
+    Bug #2: Blackhole Trash folder
 
 For the case where the private bug is the only one watching the given
 remote bug, we don't perform the redirect ahead of time (i.e. before the
 user logs in):
 
-  >>> anon_browser.open(
-  ...     'http://bugs.launchpad.test/bugs/bugtrackers/mozilla.org/2000')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.publisher.interfaces.NotFound: ...
+    >>> anon_browser.open(
+    ...     'http://bugs.launchpad.test/bugs/bugtrackers/mozilla.org/2000')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.publisher.interfaces.NotFound: ...
 
 Set the bug back to public:
 
-  >>> browser.open('http://bugs.launchpad.test/firefox/+bug/1/+secrecy')
-  >>> browser.getControl('Public', index=1).selected = True
-  >>> browser.getControl('Change').click()
-  >>> browser.url
-  'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> browser.open('http://bugs.launchpad.test/firefox/+bug/1/+secrecy')
+    >>> browser.getControl('Public', index=1).selected = True
+    >>> browser.getControl('Change').click()
+    >>> browser.url
+    'http://bugs.launchpad.test/firefox/+bug/1'
diff --git a/lib/lp/bugs/stories/standalone/xx-nonexistent-bugid-raises-404.txt b/lib/lp/bugs/stories/standalone/xx-nonexistent-bugid-raises-404.txt
index 69820f0..2b6df66 100644
--- a/lib/lp/bugs/stories/standalone/xx-nonexistent-bugid-raises-404.txt
+++ b/lib/lp/bugs/stories/standalone/xx-nonexistent-bugid-raises-404.txt
@@ -1,14 +1,14 @@
 When the user attempts to access a bug that doesn't exist, a 404 is
 raised.
 
-  >>> print(http(br"""
-  ... GET http://localhost:8085/bugs/123456 HTTP/1.1
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(br"""
+    ... GET http://localhost:8085/bugs/123456 HTTP/1.1
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
 
-  >>> print(http(br"""
-  ... GET http://localhost:8085/bugs/doesntexist HTTP/1.1
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(br"""
+    ... GET http://localhost:8085/bugs/doesntexist HTTP/1.1
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
diff --git a/lib/lp/bugs/stories/standalone/xx-obsolete-bug-and-task-urls.txt b/lib/lp/bugs/stories/standalone/xx-obsolete-bug-and-task-urls.txt
index c4c80d0..fd04457 100644
--- a/lib/lp/bugs/stories/standalone/xx-obsolete-bug-and-task-urls.txt
+++ b/lib/lp/bugs/stories/standalone/xx-obsolete-bug-and-task-urls.txt
@@ -2,24 +2,24 @@ We recently made obsolete two URLs in Launchpad:
 
 1. The Anorak bug listing URL:
 
-  >>> print(http(br"""
-  ... GET http://localhost:8085/bugs/bugs HTTP/1.1
-  ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(br"""
+    ... GET http://localhost:8085/bugs/bugs HTTP/1.1
+    ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
 
 There is currently no replacement for this URL, but
 /bugs/$bug.id will continue to work, for the time being.
 
 2. The tasks namespace:
 
-  >>> print(http(br"""
-  ... GET http://localhost:8085/malone/tasks HTTP/1.1
-  ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(br"""
+    ... GET http://localhost:8085/malone/tasks HTTP/1.1
+    ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
 
 Tasks are now accessed "contextually", like
 /firefox/+bugs/$bug.id. The entire /bugs/tasks namespace
diff --git a/lib/lp/bugs/stories/standalone/xx-slash-malone-slash-assigned.txt b/lib/lp/bugs/stories/standalone/xx-slash-malone-slash-assigned.txt
index d2eeb3f..6684eff 100644
--- a/lib/lp/bugs/stories/standalone/xx-slash-malone-slash-assigned.txt
+++ b/lib/lp/bugs/stories/standalone/xx-slash-malone-slash-assigned.txt
@@ -1,23 +1,23 @@
 To access /malone/assigned we have to be logged in.
 
-  >>> print(http(br"""
-  ... GET /bugs/assigned HTTP/1.1
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://localhost/bugs/assigned/+login
-  ...
+    >>> print(http(br"""
+    ... GET /bugs/assigned HTTP/1.1
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://localhost/bugs/assigned/+login
+    ...
 
 
 When we're logged in as Foo Bar we can see our own bugs. Note that
 /malone/assigned has been deprecated, in favour of the equivalent
 report (at least by intent, if not by design) in FOAF.
 
-  >>> print(http(br"""
-  ... GET /bugs/assigned HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://localhost/~name16/+assignedbugs
-  ...
+    >>> print(http(br"""
+    ... GET /bugs/assigned HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://localhost/~name16/+assignedbugs
+    ...
diff --git a/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt b/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt
index 00bea23..a0ae7e0 100644
--- a/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt
+++ b/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt
@@ -107,116 +107,116 @@ They now access the task page of a task on a private bug; also permitted.
 View the bug task listing page as an anonymous user. Note that the
 private bug just filed by Sample Person is not visible.
 
-  >>> print(http(br"""
-  ... GET /firefox/+bugs HTTP/1.1
-  ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
-  ... """))
-  HTTP/1.1 200 Ok
-  ...3 results...
-  ...<span class="bugnumber">#5</span>...
-  ...<span class="bugnumber">#4</span>...
-  ...<span class="bugnumber">#1</span>...
-  ...
+    >>> print(http(br"""
+    ... GET /firefox/+bugs HTTP/1.1
+    ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
+    ... """))
+    HTTP/1.1 200 Ok
+    ...3 results...
+    ...<span class="bugnumber">#5</span>...
+    ...<span class="bugnumber">#4</span>...
+    ...<span class="bugnumber">#1</span>...
+    ...
 
 Trying to access a private upstream bug as an anonymous user results
 in a page not found error.
 
-  >>> print(http(br"""
-  ... GET /firefox/+bug/6 HTTP/1.1
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
+    >>> print(http(br"""
+    ... GET /firefox/+bug/6 HTTP/1.1
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
 
-  >>> print(http(br"""
-  ... GET /firefox/+bug/14 HTTP/1.1
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(br"""
+    ... GET /firefox/+bug/14 HTTP/1.1
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
 
 View the upstream Firefox bug listing as user Foo Bar. Note that Foo
 Bar cannot see in this listing the private bug that Sample Person
 submitted earlier.
 
-  >>> print(http(br"""
-  ... GET /firefox/+bugs HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...Mozilla Firefox...
-  ...<span class="bugnumber">#5</span>...
-  ...Firefox install instructions should be complete...
-  ...<span class="bugnumber">#4</span>...
-  ...Reflow problems with complex page layouts...
-  ...<span class="bugnumber">#1</span>...
-  ...Firefox does not support SVG...
-  ...
+    >>> print(http(br"""
+    ... GET /firefox/+bugs HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...Mozilla Firefox...
+    ...<span class="bugnumber">#5</span>...
+    ...Firefox install instructions should be complete...
+    ...<span class="bugnumber">#4</span>...
+    ...Reflow problems with complex page layouts...
+    ...<span class="bugnumber">#1</span>...
+    ...Firefox does not support SVG...
+    ...
 
 
 View bugs on Mozilla Firefox as the no-privs user:
 
-  >>> print(http(br"""
-  ... GET /firefox/+bugs HTTP/1.1
-  ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-      Mozilla Firefox
-  ...
+    >>> print(http(br"""
+    ... GET /firefox/+bugs HTTP/1.1
+    ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+        Mozilla Firefox
+    ...
 
 Note that the no-privs user doesn't have the permissions to see bug #13.
 
-  >>> print(http(br"""
-  ... GET /firefox/+bug/14 HTTP/1.1
-  ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(br"""
+    ... GET /firefox/+bug/14 HTTP/1.1
+    ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
 
 This is also true if no-privs tries to access the bug from another
 context.
 
-  >>> print(http(br"""
-  ... GET /tomcat/+bug/14 HTTP/1.1
-  ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(br"""
+    ... GET /tomcat/+bug/14 HTTP/1.1
+    ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
 
 Sample Person views a bug, which they're about to set private:
 
-  >>> print(http(br"""
-  ... GET /firefox/+bug/4/+edit HTTP/1.1
-  ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...Reflow problems with complex page layouts...
-  ...
+    >>> print(http(br"""
+    ... GET /firefox/+bug/4/+edit HTTP/1.1
+    ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...Reflow problems with complex page layouts...
+    ...
 
 Sample Person sets the bug private and is made an explicit subscriber
 in the process.
 
-  ... POST /firefox/+bug/4/+secrecy HTTP/1.1
-  ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Content-Length: 429
-  ... Content-Type: multipart/form-data; boundary=---------------------------10389799518848978361196772104
-  ... 
-  ... -----------------------------10389799518848978361196772104
-  ... Content-Disposition: form-data; name="field.private.used"
-  ... 
-  ... 
-  ... -----------------------------10389799518848978361196772104
-  ... Content-Disposition: form-data; name="field.private"
-  ... 
-  ... on
-  ... -----------------------------10389799518848978361196772104
-  ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
-  ... 
-  ... Change
-  ... -----------------------------10389799518848978361196772104--
-  ... """)
-  HTTP/1.1 200 Ok
-  ...
-  ...Cc:...
-  ...Sample Person...
-  ...
+    ... POST /firefox/+bug/4/+secrecy HTTP/1.1
+    ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Content-Length: 429
+    ... Content-Type: multipart/form-data; boundary=---------------------------10389799518848978361196772104
+    ... 
+    ... -----------------------------10389799518848978361196772104
+    ... Content-Disposition: form-data; name="field.private.used"
+    ... 
+    ... 
+    ... -----------------------------10389799518848978361196772104
+    ... Content-Disposition: form-data; name="field.private"
+    ... 
+    ... on
+    ... -----------------------------10389799518848978361196772104
+    ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+    ... 
+    ... Change
+    ... -----------------------------10389799518848978361196772104--
+    ... """)
+    HTTP/1.1 200 Ok
+    ...
+    ...Cc:...
+    ...Sample Person...
+    ...
diff --git a/lib/lp/code/stories/branches/xx-branch-reference.txt b/lib/lp/code/stories/branches/xx-branch-reference.txt
index 3748402..31fba80 100644
--- a/lib/lp/code/stories/branches/xx-branch-reference.txt
+++ b/lib/lp/code/stories/branches/xx-branch-reference.txt
@@ -17,26 +17,26 @@ before finding the real branch location.
 
 The first request made is to find the bzrdir format:
 
-  >>> branchurl = ('http://launchpad.test/~name12/+branch/'
-  ...              'gnome-terminal/main')
-  >>> anon_browser.open(branchurl + '/.bzr/branch-format')
-  >>> anon_browser.contents
-  'Bazaar-NG meta directory, format 1\n'
+    >>> branchurl = ('http://launchpad.test/~name12/+branch/'
+    ...              'gnome-terminal/main')
+    >>> anon_browser.open(branchurl + '/.bzr/branch-format')
+    >>> anon_browser.contents
+    'Bazaar-NG meta directory, format 1\n'
 
 Once it has been determined that we have a meta-dir format bzrdir, the
 branch format is checked:
 
-  >>> anon_browser.open(branchurl + '/.bzr/branch/format')
-  >>> anon_browser.contents
-  'Bazaar-NG Branch Reference Format 1\n'
+    >>> anon_browser.open(branchurl + '/.bzr/branch/format')
+    >>> anon_browser.contents
+    'Bazaar-NG Branch Reference Format 1\n'
 
 Now Bazaar knows that it has a branch reference.  The final request is
 to find the real branch location.  We return Launchpad's HTTP URL for
 the branch:
 
-  >>> anon_browser.open(branchurl + '/.bzr/branch/location')
-  >>> anon_browser.contents
-  'http://bazaar.launchpad.test/~name12/gnome-terminal/main'
+    >>> anon_browser.open(branchurl + '/.bzr/branch/location')
+    >>> anon_browser.contents
+    'http://bazaar.launchpad.test/~name12/gnome-terminal/main'
 
 
 Product Series Branch
@@ -45,21 +45,21 @@ Product Series Branch
 A branch can be nominated as representing a product series.  A branch
 reference is provided for product series too:
 
-  >>> anon_browser.open('http://launchpad.test/'
-  ...                   'evolution/trunk/.bzr/branch/location')
-  >>> anon_browser.contents
-  'http://bazaar.launchpad.test/~vcs-imports/evolution/main'
+    >>> anon_browser.open('http://launchpad.test/'
+    ...                   'evolution/trunk/.bzr/branch/location')
+    >>> anon_browser.contents
+    'http://bazaar.launchpad.test/~vcs-imports/evolution/main'
 
 
 However, if a product series has no branch associated with it, we get
 a 404 error:
 
-  >>> anon_browser.open('http://launchpad.test/'
-  ...                   'firefox/1.0/.bzr/branch/location')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  zope.publisher.interfaces.NotFound: ... '.bzr'
+    >>> anon_browser.open('http://launchpad.test/'
+    ...                   'firefox/1.0/.bzr/branch/location')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    zope.publisher.interfaces.NotFound: ... '.bzr'
 
 
 Product Branch
@@ -69,18 +69,18 @@ Each product has a series set as the development focus.  A branch
 reference is provided for a product, providing the branch for the
 development focus:
 
-  >>> anon_browser.open('http://launchpad.test/'
-  ...                   'evolution/.bzr/branch/location')
-  >>> anon_browser.contents
-  'http://bazaar.launchpad.test/~vcs-imports/evolution/main'
+    >>> anon_browser.open('http://launchpad.test/'
+    ...                   'evolution/.bzr/branch/location')
+    >>> anon_browser.contents
+    'http://bazaar.launchpad.test/~vcs-imports/evolution/main'
 
 
 However, if the development focus of a product has no branch
 associated with it, we get a 404 error:
 
-  >>> anon_browser.open('http://launchpad.test/'
-  ...                   'firefox/.bzr/branch/location')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  zope.publisher.interfaces.NotFound: ... '.bzr'
+    >>> anon_browser.open('http://launchpad.test/'
+    ...                   'firefox/.bzr/branch/location')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    zope.publisher.interfaces.NotFound: ... '.bzr'
diff --git a/lib/lp/code/stories/webservice/xx-branch-links.txt b/lib/lp/code/stories/webservice/xx-branch-links.txt
index 81a5a15..dfeec6e 100644
--- a/lib/lp/code/stories/webservice/xx-branch-links.txt
+++ b/lib/lp/code/stories/webservice/xx-branch-links.txt
@@ -1,7 +1,7 @@
 Branch links
 ============
 
-  >>> from lazr.restful.testing.webservice import pprint_entry
+    >>> from lazr.restful.testing.webservice import pprint_entry
 
 
 Set up a branch and a bug to link to.
diff --git a/lib/lp/coop/answersbugs/stories/question-makebug.txt b/lib/lp/coop/answersbugs/stories/question-makebug.txt
index ba2c23c..34bf516 100644
--- a/lib/lp/coop/answersbugs/stories/question-makebug.txt
+++ b/lib/lp/coop/answersbugs/stories/question-makebug.txt
@@ -3,82 +3,83 @@ Turning a Question into a Bug
 
 The question page shows a link to make the question into a bug.
 
-  >>> browser.open('http://launchpad.test/firefox/+question/2')
-  >>> createLink = browser.getLink('Create bug report')
-  >>> createLink is not None
-  True
+    >>> browser.open('http://launchpad.test/firefox/+question/2')
+    >>> createLink = browser.getLink('Create bug report')
+    >>> createLink is not None
+    True
 
 This link brings the user to page proposing to create a new bug based
 on the content of the question. The bug description is set to the
 the question's description and the title is empty.
 
-  >>> browser.addHeader('Authorization', 'Basic foo.bar@xxxxxxxxxxxxx:test')
-  >>> createLink.click()
-  >>> print(browser.title)
-  Create bug report based on question #2...
-  >>> browser.getControl('Summary').value
-  ''
-  >>> browser.getControl('Description').value
-  "...I'm trying to learn about SVG..."
+    >>> browser.addHeader('Authorization', 'Basic foo.bar@xxxxxxxxxxxxx:test')
+    >>> createLink.click()
+    >>> print(browser.title)
+    Create bug report based on question #2...
+    >>> browser.getControl('Summary').value
+    ''
+    >>> browser.getControl('Description').value
+    "...I'm trying to learn about SVG..."
 
 The user must enter a valid title and description before creating the
 bug.
 
-  >>> browser.getControl('Description').value= ''
-  >>> browser.getControl('Create').click()
-  >>> soup = find_main_content(browser.contents)
-  >>> for tag in soup('div', 'message'):
-  ...   print(tag.string)
-  Required input is missing.
-  Required input is missing.
+    >>> browser.getControl('Description').value= ''
+    >>> browser.getControl('Create').click()
+    >>> soup = find_main_content(browser.contents)
+    >>> for tag in soup('div', 'message'):
+    ...   print(tag.string)
+    Required input is missing.
+    Required input is missing.
 
 Clicking the 'Create' button creates the bug with the user-specified title
 and description and redirects the user to the bug page.
 
-  >>> browser.getControl('Summary').value = (
-  ...     "W3C SVG demo doesn't work in Firefox")
-  >>> browser.getControl('Description').value = (
-  ...     "Browsing to the W3C SVG demo results in a blank page.")
-  >>> browser.getControl('Create').click()
-  >>> browser.url
-  '.../firefox/+bug/...'
-  >>> soup = find_main_content(browser.contents)
-  >>> for tag in soup('h1'):
-  ...     print(extract_text(tag))
-  W3C SVG demo doesn't work in Firefox Edit
-  >>> print(extract_text(find_tag_by_id(browser.contents, 'edit-description')))
-  Edit Bug Description
-  Browsing to the W3C SVG demo results in a blank page.
+    >>> browser.getControl('Summary').value = (
+    ...     "W3C SVG demo doesn't work in Firefox")
+    >>> browser.getControl('Description').value = (
+    ...     "Browsing to the W3C SVG demo results in a blank page.")
+    >>> browser.getControl('Create').click()
+    >>> browser.url
+    '.../firefox/+bug/...'
+    >>> soup = find_main_content(browser.contents)
+    >>> for tag in soup('h1'):
+    ...     print(extract_text(tag))
+    W3C SVG demo doesn't work in Firefox Edit
+    >>> print(extract_text(find_tag_by_id(
+    ...     browser.contents, 'edit-description')))
+    Edit Bug Description
+    Browsing to the W3C SVG demo results in a blank page.
 
 The bug page will display a link to the originating question in the 'Related
 questions' portlet:
 
-  >>> portlet = find_portlet(browser.contents, 'Related questions')
-  >>> for question in portlet.find_all('li', 'question-row'):
-  ...     print(question.decode_contents())
-  <span class="sprite question">Mozilla Firefox</span>: ...<a href=".../firefox/+question/2">Problem...
+    >>> portlet = find_portlet(browser.contents, 'Related questions')
+    >>> for question in portlet.find_all('li', 'question-row'):
+    ...     print(question.decode_contents())
+    <span class="sprite question">Mozilla Firefox</span>: ...<a href=".../firefox/+question/2">Problem...
 
 A user can't create a bug report when a question has already a bug linked
 to it.
 
-  >>> browser.open('http://launchpad.test/firefox/+question/2')
-  >>> browser.contents
-  '...<h3>Related bugs</h3>...'
-  >>> browser.getLink('Create bug report')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ..
-  zope.testbrowser.browser.LinkNotFoundError
+    >>> browser.open('http://launchpad.test/firefox/+question/2')
+    >>> browser.contents
+    '...<h3>Related bugs</h3>...'
+    >>> browser.getLink('Create bug report')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ..
+    zope.testbrowser.browser.LinkNotFoundError
 
 It works with distribution questions as well.
 
-  >>> browser.open('http://launchpad.test/ubuntu/+question/5/+makebug')
-  >>> browser.getControl('Summary').value = (
-  ...     "Ubuntu Installer can't find CDROM")
-  >>> browser.getControl('Create Bug Report').click()
-  >>> browser.url
-  '.../ubuntu/+bug/...'
-  >>> soup = find_main_content(browser.contents)
-  >>> for tag in soup('div', 'informational message'):
-  ...   print(tag.string)
-  Thank you! Bug...created.
+    >>> browser.open('http://launchpad.test/ubuntu/+question/5/+makebug')
+    >>> browser.getControl('Summary').value = (
+    ...     "Ubuntu Installer can't find CDROM")
+    >>> browser.getControl('Create Bug Report').click()
+    >>> browser.url
+    '.../ubuntu/+bug/...'
+    >>> soup = find_main_content(browser.contents)
+    >>> for tag in soup('div', 'informational message'):
+    ...   print(tag.string)
+    Thank you! Bug...created.
diff --git a/lib/lp/registry/browser/tests/gpg-views.txt b/lib/lp/registry/browser/tests/gpg-views.txt
index 8a99a8f..7682722 100644
--- a/lib/lp/registry/browser/tests/gpg-views.txt
+++ b/lib/lp/registry/browser/tests/gpg-views.txt
@@ -7,9 +7,9 @@ This tests GPG-related pages of an IPerson.
 
 Set up the key server:
 
-  >>> from lp.testing.keyserver import KeyServerTac
-  >>> tac = KeyServerTac()
-  >>> tac.setUp()
+    >>> from lp.testing.keyserver import KeyServerTac
+    >>> tac = KeyServerTac()
+    >>> tac.setUp()
 
 Grab the sample user:
 
diff --git a/lib/lp/registry/browser/tests/poll-views_0.txt b/lib/lp/registry/browser/tests/poll-views_0.txt
index 83282dc..487a960 100644
--- a/lib/lp/registry/browser/tests/poll-views_0.txt
+++ b/lib/lp/registry/browser/tests/poll-views_0.txt
@@ -3,14 +3,14 @@ Poll Pages
 
 First import some stuff and setup some things we'll use in this test.
 
-  >>> from zope.component import getUtility, getMultiAdapter
-  >>> from zope.publisher.browser import TestRequest
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.registry.interfaces.person import IPersonSet
-  >>> from lp.registry.interfaces.poll import IPollSet
-  >>> from datetime import datetime, timedelta
-  >>> login("test@xxxxxxxxxxxxx")
-  >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
+    >>> from zope.component import getUtility, getMultiAdapter
+    >>> from zope.publisher.browser import TestRequest
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.registry.interfaces.poll import IPollSet
+    >>> from datetime import datetime, timedelta
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
 
 
 Creating new polls
@@ -22,58 +22,58 @@ created.
 First we attempt to create a poll which starts 11h from now.  That will fail
 with a proper explanation of why it failed.
 
-  >>> eleven_hours_from_now = datetime.now() + timedelta(hours=11)
-  >>> eleven_hours_from_now = eleven_hours_from_now.strftime(
-  ...     '%Y-%m-%d %H:%M:%S')
-  >>> form = {
-  ...     'field.name': 'test-poll',
-  ...     'field.title': 'test-poll',
-  ...     'field.proposition': 'test-poll',
-  ...     'field.allowspoilt': '1',
-  ...     'field.secrecy': 'SECRET',
-  ...     'field.dateopens': eleven_hours_from_now,
-  ...     'field.datecloses': '2025-06-04',
-  ...     'field.actions.continue': 'Continue'}
-  >>> request = LaunchpadTestRequest(method='POST', form=form)
-  >>> new_poll = getMultiAdapter((ubuntu_team, request), name="+newpoll")
-  >>> new_poll.initialize()
-  >>> print("\n".join(new_poll.errors))
-  A poll cannot open less than 12 hours after it&#x27;s created.
+    >>> eleven_hours_from_now = datetime.now() + timedelta(hours=11)
+    >>> eleven_hours_from_now = eleven_hours_from_now.strftime(
+    ...     '%Y-%m-%d %H:%M:%S')
+    >>> form = {
+    ...     'field.name': 'test-poll',
+    ...     'field.title': 'test-poll',
+    ...     'field.proposition': 'test-poll',
+    ...     'field.allowspoilt': '1',
+    ...     'field.secrecy': 'SECRET',
+    ...     'field.dateopens': eleven_hours_from_now,
+    ...     'field.datecloses': '2025-06-04',
+    ...     'field.actions.continue': 'Continue'}
+    >>> request = LaunchpadTestRequest(method='POST', form=form)
+    >>> new_poll = getMultiAdapter((ubuntu_team, request), name="+newpoll")
+    >>> new_poll.initialize()
+    >>> print("\n".join(new_poll.errors))
+    A poll cannot open less than 12 hours after it&#x27;s created.
 
 Now we successfully create a poll which starts 12h from now.
 
-  >>> twelve_hours_from_now = datetime.now() + timedelta(hours=12)
-  >>> twelve_hours_from_now = twelve_hours_from_now.strftime(
-  ...     '%Y-%m-%d %H:%M:%S')
-  >>> form['field.dateopens'] = twelve_hours_from_now
-  >>> request = LaunchpadTestRequest(method='POST', form=form)
-  >>> new_poll = getMultiAdapter((ubuntu_team, request), name="+newpoll")
-  >>> new_poll.initialize()
-  >>> new_poll.errors
-  []
+    >>> twelve_hours_from_now = datetime.now() + timedelta(hours=12)
+    >>> twelve_hours_from_now = twelve_hours_from_now.strftime(
+    ...     '%Y-%m-%d %H:%M:%S')
+    >>> form['field.dateopens'] = twelve_hours_from_now
+    >>> request = LaunchpadTestRequest(method='POST', form=form)
+    >>> new_poll = getMultiAdapter((ubuntu_team, request), name="+newpoll")
+    >>> new_poll.initialize()
+    >>> new_poll.errors
+    []
 
 
 Displaying results of condorcet polls
 -------------------------------------
 
-  >>> poll = getUtility(IPollSet).getByTeamAndName(
-  ...     ubuntu_team, u'director-2004')
-  >>> poll.type.title
-  'Condorcet Voting'
+    >>> poll = getUtility(IPollSet).getByTeamAndName(
+    ...     ubuntu_team, u'director-2004')
+    >>> poll.type.title
+    'Condorcet Voting'
 
 Although condorcet polls are disabled now, everything is implemented and we're
 using a pairwise matrix to display the results. It's very trick to create this
 matrix on page templates, so the view provides a method wich return this
 matrix as a python list, with the necessary headers (the option's names).
 
-  >>> poll_results = getMultiAdapter((poll, TestRequest()), name="+index")
-  >>> for row in poll_results.getPairwiseMatrixWithHeaders():
-  ...     print(pretty(row))
-  [None, 'A', 'B', 'C', 'D']
-  ['A', None, 2, 2, 2]
-  ['B', 2, None, 2, 2]
-  ['C', 1, 1, None, 1]
-  ['D', 2, 1, 2, None]
+    >>> poll_results = getMultiAdapter((poll, TestRequest()), name="+index")
+    >>> for row in poll_results.getPairwiseMatrixWithHeaders():
+    ...     print(pretty(row))
+    [None, 'A', 'B', 'C', 'D']
+    ['A', None, 2, 2, 2]
+    ['B', 2, None, 2, 2]
+    ['C', 1, 1, None, 1]
+    ['D', 2, 1, 2, None]
 
 Voting on closed polls
 ----------------------
diff --git a/lib/lp/registry/doc/distribution-mirror.txt b/lib/lp/registry/doc/distribution-mirror.txt
index 3a69e47..7b94163 100644
--- a/lib/lp/registry/doc/distribution-mirror.txt
+++ b/lib/lp/registry/doc/distribution-mirror.txt
@@ -851,34 +851,34 @@ functions that achieve that.
 
 First we import the classes required to test the view:
 
-  >>> from zope.component import getMultiAdapter
-  >>> from lp.registry.browser.distribution import (
-  ...     DistributionMirrorsView)
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from zope.component import getMultiAdapter
+    >>> from lp.registry.browser.distribution import (
+    ...     DistributionMirrorsView)
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
 Create a view to test:
 
-  >>> request = LaunchpadTestRequest()
-  >>> view = getMultiAdapter((ubuntu, request), name='+archivemirrors')
+    >>> request = LaunchpadTestRequest()
+    >>> view = getMultiAdapter((ubuntu, request), name='+archivemirrors')
 
 Verify that the view is a DistributionMirrorsView:
 
-  >>> isinstance(view, DistributionMirrorsView)
-  True
+    >>> isinstance(view, DistributionMirrorsView)
+    True
 
 We want to make sure that the view._sum_throughput method knows about all
 the possible mirror speeds.
 
-  >>> from lp.registry.interfaces.distributionmirror import MirrorSpeed
-  >>> class MockMirror:
-  ...     speed = None
-  >>> mirrors = []
-  >>> for speed in MirrorSpeed.items:
-  ...     a = MockMirror()
-  ...     a.speed = speed
-  ...     mirrors.append(a)
-  >>> print(view._sum_throughput(mirrors))
-  37 Gbps
+    >>> from lp.registry.interfaces.distributionmirror import MirrorSpeed
+    >>> class MockMirror:
+    ...     speed = None
+    >>> mirrors = []
+    >>> for speed in MirrorSpeed.items:
+    ...     a = MockMirror()
+    ...     a.speed = speed
+    ...     mirrors.append(a)
+    >>> print(view._sum_throughput(mirrors))
+    37 Gbps
 
 
 Changing mirror owners
diff --git a/lib/lp/registry/doc/poll-preconditions.txt b/lib/lp/registry/doc/poll-preconditions.txt
index b6d5dba..7f22497 100644
--- a/lib/lp/registry/doc/poll-preconditions.txt
+++ b/lib/lp/registry/doc/poll-preconditions.txt
@@ -5,68 +5,69 @@ There's some preconditions that we need to meet to vote in polls and remove
 options from them, Not meeting these preconditions is a programming error and
 should be threated as so.
 
-  >>> from zope.component import getUtility
-  >>> from datetime import timedelta
-  >>> from lp.registry.interfaces.person import IPersonSet
-  >>> from lp.registry.interfaces.poll import IPollSet
-
-  >>> ubuntu_team = getUtility(IPersonSet).get(17)
-  >>> ubuntu_team_member = getUtility(IPersonSet).get(1)
-  >>> ubuntu_team_nonmember = getUtility(IPersonSet).get(12)
-
-  >>> pollset = getUtility(IPollSet)
-  >>> director_election = pollset.getByTeamAndName(ubuntu_team,
-  ...                                              u'director-2004')
-  >>> director_options = director_election.getActiveOptions()
-  >>> leader_election = pollset.getByTeamAndName(ubuntu_team, u'leader-2004')
-  >>> leader_options = leader_election.getActiveOptions()
-  >>> opendate = leader_election.dateopens
-  >>> onesec = timedelta(seconds=1)
+    >>> from zope.component import getUtility
+    >>> from datetime import timedelta
+    >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.registry.interfaces.poll import IPollSet
+
+    >>> ubuntu_team = getUtility(IPersonSet).get(17)
+    >>> ubuntu_team_member = getUtility(IPersonSet).get(1)
+    >>> ubuntu_team_nonmember = getUtility(IPersonSet).get(12)
+
+    >>> pollset = getUtility(IPollSet)
+    >>> director_election = pollset.getByTeamAndName(ubuntu_team,
+    ...                                              u'director-2004')
+    >>> director_options = director_election.getActiveOptions()
+    >>> leader_election = pollset.getByTeamAndName(
+    ...     ubuntu_team, u'leader-2004')
+    >>> leader_options = leader_election.getActiveOptions()
+    >>> opendate = leader_election.dateopens
+    >>> onesec = timedelta(seconds=1)
 
 
 If the poll is already opened, it's impossible to remove an option.
 
-  >>> leader_election.removeOption(leader_options[0], when=opendate)
-  Traceback (most recent call last):
-  ...
-  AssertionError
+    >>> leader_election.removeOption(leader_options[0], when=opendate)
+    Traceback (most recent call last):
+    ...
+    AssertionError
 
 
 Trying to vote two times is a programming error.
-  
-  >>> votes = leader_election.storeSimpleVote(
-  ...     ubuntu_team_member, leader_options[0], when=opendate)
 
-  >>> votes = leader_election.storeSimpleVote(
-  ...     ubuntu_team_member, leader_options[0], when=opendate)
-  Traceback (most recent call last):
-  ...
-  AssertionError: Can't vote twice in one poll
+    >>> votes = leader_election.storeSimpleVote(
+    ...     ubuntu_team_member, leader_options[0], when=opendate)
+
+    >>> votes = leader_election.storeSimpleVote(
+    ...     ubuntu_team_member, leader_options[0], when=opendate)
+    Traceback (most recent call last):
+    ...
+    AssertionError: Can't vote twice in one poll
 
 
 It's not possible for a non-member to vote, neither to vote when the poll is
 not open.
 
-  >>> votes = leader_election.storeSimpleVote(
-  ...     ubuntu_team_nonmember, leader_options[0], when=opendate)
-  Traceback (most recent call last):
-  ...
-  AssertionError: Person ... is not a member of this poll's team.
+    >>> votes = leader_election.storeSimpleVote(
+    ...     ubuntu_team_nonmember, leader_options[0], when=opendate)
+    Traceback (most recent call last):
+    ...
+    AssertionError: Person ... is not a member of this poll's team.
 
-  >>> votes = leader_election.storeSimpleVote(
-  ...     ubuntu_team_member, leader_options[0], when=opendate - onesec)
-  Traceback (most recent call last):
-  ...
-  AssertionError: This poll is not open
+    >>> votes = leader_election.storeSimpleVote(
+    ...     ubuntu_team_member, leader_options[0], when=opendate - onesec)
+    Traceback (most recent call last):
+    ...
+    AssertionError: This poll is not open
 
 
 It's not possible to vote on an option that doesn't belong to the poll you're
 voting in.
 
-  >>> options = {leader_options[0]: 1}
-  >>> votes = director_election.storeCondorcetVote(
-  ...     ubuntu_team_member, options, when=opendate)
-  Traceback (most recent call last):
-  ...
-  AssertionError: The option ... doesn't belong to this poll
+    >>> options = {leader_options[0]: 1}
+    >>> votes = director_election.storeCondorcetVote(
+    ...     ubuntu_team_member, options, when=opendate)
+    Traceback (most recent call last):
+    ...
+    AssertionError: The option ... doesn't belong to this poll
 
diff --git a/lib/lp/registry/doc/poll.txt b/lib/lp/registry/doc/poll.txt
index dc29e06..de5ed77 100644
--- a/lib/lp/registry/doc/poll.txt
+++ b/lib/lp/registry/doc/poll.txt
@@ -8,149 +8,151 @@ like the 'Gnome Team' and the 'Ubuntu Team'. These teams often have leaders
 whose ellection depends on the vote of all members, and this is one of the
 reasons why we teams can have polls attached to them.
 
-  >>> import pytz
-  >>> from datetime import datetime, timedelta
-  >>> from zope.component import getUtility
-  >>> from lp.services.database.sqlbase import flush_database_updates
-  >>> from lp.testing import login
-  >>> from lp.registry.interfaces.person import IPersonSet
-  >>> from lp.registry.interfaces.poll import (
-  ...     IPollSubset,
-  ...     PollAlgorithm,
-  ...     PollSecrecy,
-  ...     )
-
-  >>> team = getUtility(IPersonSet).getByName('ubuntu-team')
-  >>> member = getUtility(IPersonSet).getByName('stevea')
-  >>> member2 = getUtility(IPersonSet).getByName('jdub')
-  >>> member3 = getUtility(IPersonSet).getByName('kamion')
-  >>> member4 = getUtility(IPersonSet).getByName('name16')
-  >>> member5 = getUtility(IPersonSet).getByName('limi')
-  >>> nonmember = getUtility(IPersonSet).getByName('justdave')
-  >>> now = datetime.now(pytz.timezone('UTC'))
-  >>> onesec = timedelta(seconds=1)
+    >>> import pytz
+    >>> from datetime import datetime, timedelta
+    >>> from zope.component import getUtility
+    >>> from lp.services.database.sqlbase import flush_database_updates
+    >>> from lp.testing import login
+    >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.registry.interfaces.poll import (
+    ...     IPollSubset,
+    ...     PollAlgorithm,
+    ...     PollSecrecy,
+    ...     )
+
+    >>> team = getUtility(IPersonSet).getByName('ubuntu-team')
+    >>> member = getUtility(IPersonSet).getByName('stevea')
+    >>> member2 = getUtility(IPersonSet).getByName('jdub')
+    >>> member3 = getUtility(IPersonSet).getByName('kamion')
+    >>> member4 = getUtility(IPersonSet).getByName('name16')
+    >>> member5 = getUtility(IPersonSet).getByName('limi')
+    >>> nonmember = getUtility(IPersonSet).getByName('justdave')
+    >>> now = datetime.now(pytz.timezone('UTC'))
+    >>> onesec = timedelta(seconds=1)
 
 We need to login with one of the administrators of the team named 
 'ubuntu-team' to be able to create/edit polls.
-  >>> login('colin.watson@xxxxxxxxxxxxxxx')
+    >>> login('colin.watson@xxxxxxxxxxxxxxx')
 
 First we get an object implementing IPollSubset, which is the set of polls for
 a given team (in our case, the 'Ubuntu Team')
-  >>> pollsubset = IPollSubset(team)
+    >>> pollsubset = IPollSubset(team)
 
 Now we create a new poll on this team.
-  >>> opendate = datetime(2005, 1, 1, tzinfo=pytz.timezone('UTC'))
-  >>> closedate = opendate + timedelta(weeks=2)
-  >>> title = u"2005 Leader's Elections"
-  >>> proposition = u"Who's going to be the next leader?"
-  >>> type = PollAlgorithm.SIMPLE
-  >>> secrecy = PollSecrecy.SECRET
-  >>> allowspoilt = True
-  >>> poll = pollsubset.new(u"leader-election", title, proposition, opendate,
-  ...                       closedate, secrecy, allowspoilt, type)
+    >>> opendate = datetime(2005, 1, 1, tzinfo=pytz.timezone('UTC'))
+    >>> closedate = opendate + timedelta(weeks=2)
+    >>> title = u"2005 Leader's Elections"
+    >>> proposition = u"Who's going to be the next leader?"
+    >>> type = PollAlgorithm.SIMPLE
+    >>> secrecy = PollSecrecy.SECRET
+    >>> allowspoilt = True
+    >>> poll = pollsubset.new(
+    ...     u"leader-election", title, proposition, opendate,
+    ...     closedate, secrecy, allowspoilt, type)
 
 Now we test the if the poll is open or closed in some specific dates.
-  >>> poll.isOpen(when=opendate)
-  True
-  >>> poll.isOpen(when=opendate - onesec)
-  False
-  >>> poll.isOpen(when=closedate)
-  True
-  >>> poll.isOpen(when=closedate + onesec)
-  False
+    >>> poll.isOpen(when=opendate)
+    True
+    >>> poll.isOpen(when=opendate - onesec)
+    False
+    >>> poll.isOpen(when=closedate)
+    True
+    >>> poll.isOpen(when=closedate + onesec)
+    False
 
 To know what polls are open/closed/not-yet-opened in a team, you can use the
 methods of PollSubset.
 Here we'll query using three different dates:
 
 Query for open polls in the exact second the poll is opening.
-  >>> for p in pollsubset.getOpenPolls(when=opendate):
-  ...     print(p.name)
-  leader-election
-  never-closes
-  never-closes2
-  never-closes3
-  never-closes4
+    >>> for p in pollsubset.getOpenPolls(when=opendate):
+    ...     print(p.name)
+    leader-election
+    never-closes
+    never-closes2
+    never-closes3
+    never-closes4
 
 Query for closed polls, one second after the poll closes.
-  >>> for p in pollsubset.getClosedPolls(when=closedate + onesec):
-  ...     print(p.name)
-  director-2004
-  leader-2004
-  leader-election
+    >>> for p in pollsubset.getClosedPolls(when=closedate + onesec):
+    ...     print(p.name)
+    director-2004
+    leader-2004
+    leader-election
 
 Query for not-yet-opened polls, one second before the poll opens.
-  >>> for p in pollsubset.getNotYetOpenedPolls(when=opendate - onesec):
-  ...     print(p.name)
-  leader-election
-  not-yet-opened
+    >>> for p in pollsubset.getNotYetOpenedPolls(when=opendate - onesec):
+    ...     print(p.name)
+    leader-election
+    not-yet-opened
 
 All polls must have a set of options for people to choose, and they'll always
 start with zero options. We're responsible for adding new ones.
-  >>> poll.getAllOptions().count()
-  0
+    >>> poll.getAllOptions().count()
+    0
 
 Let's add some options to this poll, so people can start voting. :)
-  >>> will = poll.newOption(u'wgraham', u'Will Graham')
-  >>> jack = poll.newOption(u'jcrawford', u'Jack Crawford')
-  >>> francis = poll.newOption(u'fd', u'Francis Dolarhyde')
-  >>> for o in poll.getActiveOptions():
-  ...     print(o.title)
-  Francis Dolarhyde
-  Jack Crawford
-  Will Graham
+    >>> will = poll.newOption(u'wgraham', u'Will Graham')
+    >>> jack = poll.newOption(u'jcrawford', u'Jack Crawford')
+    >>> francis = poll.newOption(u'fd', u'Francis Dolarhyde')
+    >>> for o in poll.getActiveOptions():
+    ...     print(o.title)
+    Francis Dolarhyde
+    Jack Crawford
+    Will Graham
 
 Now, what happens if the poll is already open and, let's say, Francis Dolarhyde
 is convicted and thus becomes ineligible? We'll have to mark that option as
 inactive, so people can't vote on it.
-  >>> francis.active = False
-  >>> flush_database_updates()
-  >>> for o in poll.getActiveOptions():
-  ...     print(o.title)
-  Jack Crawford
-  Will Graham
+    >>> francis.active = False
+    >>> flush_database_updates()
+    >>> for o in poll.getActiveOptions():
+    ...     print(o.title)
+    Jack Crawford
+    Will Graham
 
 If the poll is not yet opened, it's possible to simply remove a given option.
-  >>> poll.removeOption(will, when=opendate - onesec)
-  >>> for o in poll.getAllOptions():
-  ...     print(o.title)
-  Francis Dolarhyde
-  Jack Crawford
+    >>> poll.removeOption(will, when=opendate - onesec)
+    >>> for o in poll.getAllOptions():
+    ...     print(o.title)
+    Francis Dolarhyde
+    Jack Crawford
 
 Any member of the team this poll refers to is eligible to vote, if the poll is
 still open.
 
-  >>> vote1 = poll.storeSimpleVote(member, jack, when=opendate)
-  >>> vote2 = poll.storeSimpleVote(member2, None, when=opendate)
+    >>> vote1 = poll.storeSimpleVote(member, jack, when=opendate)
+    >>> vote2 = poll.storeSimpleVote(member2, None, when=opendate)
 
 
 Now we create a Condorcet poll on this team and add some options to it, so
 people can start voting.
 
-  >>> title = u"2005 Director's Elections"
-  >>> proposition = u"Who's going to be the next director?"
-  >>> type = PollAlgorithm.CONDORCET
-  >>> secrecy = PollSecrecy.SECRET
-  >>> allowspoilt = True
-  >>> poll2 = pollsubset.new(u"director-election", title, proposition,
-  ...                        opendate, closedate, secrecy, allowspoilt, type)
-  >>> a = poll2.newOption(u'A', u'Option A')
-  >>> b = poll2.newOption(u'B', u'Option B')
-  >>> c = poll2.newOption(u'C', u'Option C')
-  >>> d = poll2.newOption(u'D', u'Option D')
-
-  >>> options = {b: 1, d: 2, c: 3}
-  >>> votes = poll2.storeCondorcetVote(member, options, when=opendate)
-  >>> options = {d: 1, b: 2}
-  >>> votes = poll2.storeCondorcetVote(member2, options, when=opendate)
-  >>> options = {a: 1, c: 2, b: 3}
-  >>> votes = poll2.storeCondorcetVote(member3, options, when=opendate)
-  >>> options = {a: 1}
-  >>> votes = poll2.storeCondorcetVote(member4, options, when=opendate)
-  >>> from zope.security.proxy import removeSecurityProxy
-  >>> for row in poll2.getPairwiseMatrix():
-  ...     print(pretty(removeSecurityProxy(row)))
-  [None, 2, 2, 2]
-  [2, None, 2, 2]
-  [1, 1, None, 1]
-  [2, 1, 2, None]
+    >>> title = u"2005 Director's Elections"
+    >>> proposition = u"Who's going to be the next director?"
+    >>> type = PollAlgorithm.CONDORCET
+    >>> secrecy = PollSecrecy.SECRET
+    >>> allowspoilt = True
+    >>> poll2 = pollsubset.new(
+    ...     u"director-election", title, proposition,
+    ...     opendate, closedate, secrecy, allowspoilt, type)
+    >>> a = poll2.newOption(u'A', u'Option A')
+    >>> b = poll2.newOption(u'B', u'Option B')
+    >>> c = poll2.newOption(u'C', u'Option C')
+    >>> d = poll2.newOption(u'D', u'Option D')
+
+    >>> options = {b: 1, d: 2, c: 3}
+    >>> votes = poll2.storeCondorcetVote(member, options, when=opendate)
+    >>> options = {d: 1, b: 2}
+    >>> votes = poll2.storeCondorcetVote(member2, options, when=opendate)
+    >>> options = {a: 1, c: 2, b: 3}
+    >>> votes = poll2.storeCondorcetVote(member3, options, when=opendate)
+    >>> options = {a: 1}
+    >>> votes = poll2.storeCondorcetVote(member4, options, when=opendate)
+    >>> from zope.security.proxy import removeSecurityProxy
+    >>> for row in poll2.getPairwiseMatrix():
+    ...     print(pretty(removeSecurityProxy(row)))
+    [None, 2, 2, 2]
+    [2, None, 2, 2]
+    [1, 1, None, 1]
+    [2, 1, 2, None]
diff --git a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
index 3839e1d..b28373f 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
@@ -554,43 +554,43 @@ And now we can see the key listed as one of Sample Person's active keys.
 This test verifies that we correctly handle keys which are in some way
 special: either invalid, broken, revoked, expired, or already imported.
 
-  >>> from lp.testing.keyserver import KeyServerTac
-  >>> from lp.services.mail import stub
+    >>> from lp.testing.keyserver import KeyServerTac
+    >>> from lp.services.mail import stub
 
-  >>> tac = KeyServerTac()
-  >>> tac.setUp()
+    >>> tac = KeyServerTac()
+    >>> tac.setUp()
 
-  >>> sign_only   = "447D BF38 C4F9 C4ED 7522  46B7 7D88 9137 17B0 5A8F"
-  >>> preimported = "A419AE861E88BC9E04B9C26FBA2B9389DFD20543"
+    >>> sign_only   = "447D BF38 C4F9 C4ED 7522  46B7 7D88 9137 17B0 5A8F"
+    >>> preimported = "A419AE861E88BC9E04B9C26FBA2B9389DFD20543"
 
 Try to import a key which is already imported:
 
-  >>> del stub.test_emails[:]
-  >>> browser.open('http://launchpad.test/~name12/+editpgpkeys')
-  >>> browser.getControl(name='fingerprint').value = preimported
-  >>> browser.getControl(name='import').click()
-  >>> "A message has been sent" in browser.contents
-  False
-  >>> stub.test_emails
-  []
-  >>> print(browser.contents)
-  <BLANKLINE>
-  ...
-  ...has already been imported...
+    >>> del stub.test_emails[:]
+    >>> browser.open('http://launchpad.test/~name12/+editpgpkeys')
+    >>> browser.getControl(name='fingerprint').value = preimported
+    >>> browser.getControl(name='import').click()
+    >>> "A message has been sent" in browser.contents
+    False
+    >>> stub.test_emails
+    []
+    >>> print(browser.contents)
+    <BLANKLINE>
+    ...
+    ...has already been imported...
 
-  >>> tac.tearDown()
+    >>> tac.tearDown()
 
 
 
 Ensure we are raising 404 error instead of System Error
 
-  >>> print(http(r"""
-  ... POST /codeofconduct/donkey HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Referer: https://launchpad.test/
-  ... """))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(http(r"""
+    ... POST /codeofconduct/donkey HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Referer: https://launchpad.test/
+    ... """))
+    HTTP/1.1 404 Not Found
+    ...
 
 Check to see no CoC signature is registered for Mark:
 
@@ -636,47 +636,47 @@ Test if the advertisement email was sent:
 
   Let's login with an Launchpad Admin
 
-  >>> browser.addHeader(
-  ...   'Authorization', 'Basic guilherme.salgado@xxxxxxxxxxxxx:test')
+    >>> browser.addHeader(
+    ...   'Authorization', 'Basic guilherme.salgado@xxxxxxxxxxxxx:test')
 
   Check if we can see the Code of conduct page
 
-  >>> browser.open('http://localhost:9000/codeofconduct')
-  >>> 'Ubuntu Codes of Conduct' in browser.contents
-  True
+    >>> browser.open('http://localhost:9000/codeofconduct')
+    >>> 'Ubuntu Codes of Conduct' in browser.contents
+    True
 
   The link to the Administrator console
 
-  >>> admin_console_link = browser.getLink('Administration console')
-  >>> admin_console_link.url
-  'http://localhost:9000/codeofconduct/console'
+    >>> admin_console_link = browser.getLink('Administration console')
+    >>> admin_console_link.url
+    'http://localhost:9000/codeofconduct/console'
 
   Let's follow the link
 
-  >>> admin_console_link.click()
+    >>> admin_console_link.click()
 
   We are in the Administration page 
 
-  >>> browser.url
-  'http://localhost:9000/codeofconduct/console'
+    >>> browser.url
+    'http://localhost:9000/codeofconduct/console'
 
-  >>> 'Administer code of conduct signatures' in browser.contents
-  True
+    >>> 'Administer code of conduct signatures' in browser.contents
+    True
 
-  >>> browser.getLink("register signatures").url
-  'http://localhost:9000/codeofconduct/console/+new'
+    >>> browser.getLink("register signatures").url
+    'http://localhost:9000/codeofconduct/console/+new'
 
 
   Back to the CoC front page let's see the current version of the CoC
 
-  >>> browser.open('http://localhost:9000/codeofconduct')
-  >>> browser.getLink('current version').click()
+    >>> browser.open('http://localhost:9000/codeofconduct')
+    >>> browser.getLink('current version').click()
 
-  >>> 'Ubuntu Code of Conduct - 2.0' in browser.contents
-  True
+    >>> 'Ubuntu Code of Conduct - 2.0' in browser.contents
+    True
 
-  >>> browser.getLink('Sign it').url
-  'http://localhost:9000/codeofconduct/2.0/+sign'
+    >>> browser.getLink('Sign it').url
+    'http://localhost:9000/codeofconduct/2.0/+sign'
 
-  >>> browser.getLink('Download this version').url
-  'http://localhost:9000/codeofconduct/2.0/+download'
+    >>> browser.getLink('Download this version').url
+    'http://localhost:9000/codeofconduct/2.0/+download'
diff --git a/lib/lp/registry/stories/person/xx-people-index.txt b/lib/lp/registry/stories/person/xx-people-index.txt
index ea6f71d..cd6e00c 100644
--- a/lib/lp/registry/stories/person/xx-people-index.txt
+++ b/lib/lp/registry/stories/person/xx-people-index.txt
@@ -1,13 +1,13 @@
 
   Test /people.
 
-  >>> print(http(r"""
-  ... GET /people HTTP/1.1
-  ... """))
-  HTTP/1.1 200 Ok
-  Content-Length: ...
-  Content-Type: text/html;charset=utf-8
-  <BLANKLINE>
-  ...
-  ...<h1>People and teams</h1>...
-  ...
+    >>> print(http(r"""
+    ... GET /people HTTP/1.1
+    ... """))
+    HTTP/1.1 200 Ok
+    Content-Length: ...
+    Content-Type: text/html;charset=utf-8
+    <BLANKLINE>
+    ...
+    ...<h1>People and teams</h1>...
+    ...
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-review.txt b/lib/lp/registry/stories/productseries/xx-productseries-review.txt
index fcc2db8..0bdea46 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-review.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-review.txt
@@ -1,32 +1,32 @@
 Foo Bar changes the productseries named 'failedbranch' from the product a52dec
 to bazaar. Also changes the name of the productseries to 'newname'.
-  >>> print(http(r"""
-  ... POST /a52dec/failedbranch/+review HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... Referer: https://launchpad.test/
-  ... Content-Type: multipart/form-data; boundary=---------------------------10572808480422220968425074
-  ...
-  ... -----------------------------10572808480422220968425074
-  ... Content-Disposition: form-data; name="field.product"
-  ...
-  ... bazaar
-  ... -----------------------------10572808480422220968425074
-  ... Content-Disposition: form-data; name="field.name"
-  ...
-  ... newname
-  ... -----------------------------10572808480422220968425074
-  ... Content-Disposition: form-data; name="field.actions.change"
-  ...
-  ... Change
-  ... -----------------------------10572808480422220968425074--
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://localhost/bazaar/newname...
+    >>> print(http(r"""
+    ... POST /a52dec/failedbranch/+review HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... Referer: https://launchpad.test/
+    ... Content-Type: multipart/form-data; boundary=---------------------------10572808480422220968425074
+    ...
+    ... -----------------------------10572808480422220968425074
+    ... Content-Disposition: form-data; name="field.product"
+    ...
+    ... bazaar
+    ... -----------------------------10572808480422220968425074
+    ... Content-Disposition: form-data; name="field.name"
+    ...
+    ... newname
+    ... -----------------------------10572808480422220968425074
+    ... Content-Disposition: form-data; name="field.actions.change"
+    ...
+    ... Change
+    ... -----------------------------10572808480422220968425074--
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://localhost/bazaar/newname...
 
-  >>> print(http(r"""
-  ... GET /bazaar/newname HTTP/1.1
-  ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
+    >>> print(http(r"""
+    ... GET /bazaar/newname HTTP/1.1
+    ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
diff --git a/lib/lp/registry/stories/project/xx-project-add.txt b/lib/lp/registry/stories/project/xx-project-add.txt
index e33b039..4e6a32a 100644
--- a/lib/lp/registry/stories/project/xx-project-add.txt
+++ b/lib/lp/registry/stories/project/xx-project-add.txt
@@ -3,60 +3,64 @@ Adding new projects
 
 Normal users should not be able to do this:
 
-  >>> user_browser.open("http://launchpad.test/projectgroups/+new";)
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  zope.security.interfaces.Unauthorized: ...
+    >>> user_browser.open("http://launchpad.test/projectgroups/+new";)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    zope.security.interfaces.Unauthorized: ...
 
 But an admin user should be able to do it:
 
-  >>> admin_browser.open('http://launchpad.test/projectgroups')
-  >>> admin_browser.getLink('Register a project group').click()
-  >>> admin_browser.url
-  'http://launchpad.test/projectgroups/+new'
+    >>> admin_browser.open('http://launchpad.test/projectgroups')
+    >>> admin_browser.getLink('Register a project group').click()
+    >>> admin_browser.url
+    'http://launchpad.test/projectgroups/+new'
 
 Testing if the validator is working for the URL field.
 Add a new project without the http://
 
-  >>> admin_browser.getControl('Name', index=0).value = 'kde'
-  >>> admin_browser.getControl('Display Name').value = 'K Desktop Environment'
-  >>> admin_browser.getControl('Project Group Summary').value = 'KDE'
-  >>> admin_browser.getControl('Description').value = 'K Desktop Environment'
-  >>> admin_browser.getControl('Maintainer').value = 'cprov'
-  >>> admin_browser.getControl('Homepage URL').value = 'www.kde.org'
-  >>> admin_browser.getControl('Add').click()
-  >>> print_feedback_messages(admin_browser.contents)
-  There is 1 error.
-  "www.kde.org" is not a valid URI
+    >>> admin_browser.getControl('Name', index=0).value = 'kde'
+    >>> admin_browser.getControl('Display Name').value = (
+    ...     'K Desktop Environment')
+    >>> admin_browser.getControl('Project Group Summary').value = 'KDE'
+    >>> admin_browser.getControl('Description').value = (
+    ...     'K Desktop Environment')
+    >>> admin_browser.getControl('Maintainer').value = 'cprov'
+    >>> admin_browser.getControl('Homepage URL').value = 'www.kde.org'
+    >>> admin_browser.getControl('Add').click()
+    >>> print_feedback_messages(admin_browser.contents)
+    There is 1 error.
+    "www.kde.org" is not a valid URI
 
 Testing if the validator is working for the name field.
 
-  >>> admin_browser.open('http://launchpad.test/projectgroups/+new')
-  >>> admin_browser.getControl('Name', index=0).value = 'kde!'
-  >>> admin_browser.getControl('Display Name').value = 'K Desktop Environment'
-  >>> admin_browser.getControl('Project Group Summary').value = 'KDE'
-  >>> admin_browser.getControl('Description').value = 'K Desktop Environment'
-  >>> admin_browser.getControl('Maintainer').value = 'cprov'
-  >>> admin_browser.getControl('Homepage URL').value = 'http://kde.org/'
-  >>> admin_browser.getControl('Add').click()
-  >>> print_feedback_messages(admin_browser.contents)
-  There is 1 error.
-  Invalid name 'kde!'. Names must...
-
-  >>> admin_browser.getControl('Name', index=0).value = 'apache'
-  >>> admin_browser.getControl('Add').click()
-  >>> print_feedback_messages(admin_browser.contents)
-  There is 1 error.
-  apache is already used by another project
+    >>> admin_browser.open('http://launchpad.test/projectgroups/+new')
+    >>> admin_browser.getControl('Name', index=0).value = 'kde!'
+    >>> admin_browser.getControl('Display Name').value = (
+    ...     'K Desktop Environment')
+    >>> admin_browser.getControl('Project Group Summary').value = 'KDE'
+    >>> admin_browser.getControl('Description').value = (
+    ...     'K Desktop Environment')
+    >>> admin_browser.getControl('Maintainer').value = 'cprov'
+    >>> admin_browser.getControl('Homepage URL').value = 'http://kde.org/'
+    >>> admin_browser.getControl('Add').click()
+    >>> print_feedback_messages(admin_browser.contents)
+    There is 1 error.
+    Invalid name 'kde!'. Names must...
+
+    >>> admin_browser.getControl('Name', index=0).value = 'apache'
+    >>> admin_browser.getControl('Add').click()
+    >>> print_feedback_messages(admin_browser.contents)
+    There is 1 error.
+    apache is already used by another project
 
 Now we add a new project.
 
-  >>> admin_browser.getControl('Name', index=0).value = 'kde'
-  >>> admin_browser.getControl('Add').click()
-  >>> admin_browser.url
-  'http://launchpad.test/kde'
+    >>> admin_browser.getControl('Name', index=0).value = 'kde'
+    >>> admin_browser.getControl('Add').click()
+    >>> admin_browser.url
+    'http://launchpad.test/kde'
 
-  >>> anon_browser.open(admin_browser.url)
-  >>> print(anon_browser.title)
-  K Desktop Environment in Launchpad
+    >>> anon_browser.open(admin_browser.url)
+    >>> print(anon_browser.title)
+    K Desktop Environment in Launchpad
diff --git a/lib/lp/registry/stories/project/xx-reassign-project.txt b/lib/lp/registry/stories/project/xx-reassign-project.txt
index c643d82..d561210 100644
--- a/lib/lp/registry/stories/project/xx-reassign-project.txt
+++ b/lib/lp/registry/stories/project/xx-reassign-project.txt
@@ -4,56 +4,56 @@
   Logged in as no-priv@xxxxxxxxxxxxx we can't do that, because they're not the
   owner of the project nor a member of admins.
 
-  >>> print(http(r"""
-  ... GET /mozilla/+reassign HTTP/1.1
-  ... Authorization: Basic no-priv@xxxxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 403 Forbidden
-  ...
+    >>> print(http(r"""
+    ... GET /mozilla/+reassign HTTP/1.1
+    ... Authorization: Basic no-priv@xxxxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 403 Forbidden
+    ...
 
 
   Now we're logged in as mark@xxxxxxxxxxx and he's the owner of the admins team,
   so he can do everything.
 
-  >>> print(http(r"""
-  ... GET /mozilla/+reassign HTTP/1.1
-  ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...Current:...
-  ...
-  ...New:...
-  ...
+    >>> print(http(r"""
+    ... GET /mozilla/+reassign HTTP/1.1
+    ... Authorization: Basic mark@xxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...Current:...
+    ...
+    ...New:...
+    ...
 
 
   Here he changes the owner to himself.
 
-  >>> print(http(r"""
-  ... POST /mozilla/+reassign HTTP/1.1
-  ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... Referer: https://launchpad.test/
-  ...
-  ... field.owner=mark&field.existing=existing"""
-  ... r"""&field.actions.change=Change"""))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://localhost/mozilla
-  ...
+    >>> print(http(r"""
+    ... POST /mozilla/+reassign HTTP/1.1
+    ... Authorization: Basic mark@xxxxxxxxxxx:test
+    ... Referer: https://launchpad.test/
+    ...
+    ... field.owner=mark&field.existing=existing"""
+    ... r"""&field.actions.change=Change"""))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://localhost/mozilla
+    ...
 
 
 
   Here we see the new owner: Mark Shuttleworth
 
-  >>> print(http(r"""
-  ... GET /mozilla/ HTTP/1.1
-  ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 200 Ok
-  Content-Length: ...
-  Content-Type: text/html;charset=utf-8
-  ...
-  ...Maintainer:...
-  ...
-  ...Mark Shuttleworth...
-  ...
+    >>> print(http(r"""
+    ... GET /mozilla/ HTTP/1.1
+    ... Authorization: Basic mark@xxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 200 Ok
+    Content-Length: ...
+    Content-Type: text/html;charset=utf-8
+    ...
+    ...Maintainer:...
+    ...
+    ...Mark Shuttleworth...
+    ...
diff --git a/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt b/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt
index 1bf2413..f587fcb 100644
--- a/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt
+++ b/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt
@@ -4,229 +4,229 @@
   Go to a condorcet-style poll (which is still open) and check that apart
   from seeing our vote we can also change it.
 
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes2 HTTP/1.1
-  ... Accept-Language: en-us,en;q=0.5
-  ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://localhost/~ubuntu-team/+poll/never-closes2/+vote
-  ...
-
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
-  ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...You must enter your vote key...
-  ...This is a secret poll...
-  ...your vote is identified only by the key you...
-  ...were given when you voted. To view or change your vote you must enter...
-  ...your key:...
-  ...
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/never-closes2 HTTP/1.1
+    ... Accept-Language: en-us,en;q=0.5
+    ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://localhost/~ubuntu-team/+poll/never-closes2/+vote
+    ...
+
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
+    ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...You must enter your vote key...
+    ...This is a secret poll...
+    ...your vote is identified only by the key you...
+    ...were given when you voted. To view or change your vote you must enter...
+    ...your key:...
+    ...
 
 
   If a non-member (Sample Person) guesses the voting URL and tries to vote,
   they won't be allowed.
 
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
-  ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """))
-  HTTP/1.1 200 Ok
-  ...You can&#8217;t vote in this poll because you&#8217;re not...
-  ...a member of Ubuntu Team...
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
+    ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
+    ... """))
+    HTTP/1.1 200 Ok
+    ...You can&#8217;t vote in this poll because you&#8217;re not...
+    ...a member of Ubuntu Team...
 
 
   By providing the token we will be able to see our current vote.
 
-  >>> print(http(r"""
-  ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
-  ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... Content-Type: application/x-www-form-urlencoded
-  ... Referer: https://launchpad.test/
-  ...
-  ... token=xn9FDCTp4m&showvote=Show+My+Vote&option_12=&option_13=&option_14=&option_15="""))
-  HTTP/1.1 200 Ok
-  ...
-                  <p>Your current vote is as follows:</p>
-                  <p>
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>1</b>.
-                      Option 1
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>2</b>.
-                      Option 2
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>3</b>.
-                      Option 4
-  <BLANKLINE>
-                  </p>
-  ...
+    >>> print(http(r"""
+    ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
+    ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
+    ... Content-Type: application/x-www-form-urlencoded
+    ... Referer: https://launchpad.test/
+    ...
+    ... token=xn9FDCTp4m&showvote=Show+My+Vote&option_12=&option_13=&option_14=&option_15="""))
+    HTTP/1.1 200 Ok
+    ...
+                    <p>Your current vote is as follows:</p>
+                    <p>
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>1</b>.
+                        Option 1
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>2</b>.
+                        Option 2
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>3</b>.
+                        Option 4
+    <BLANKLINE>
+                    </p>
+    ...
 
 
   It's also possible to change the vote, if wanted.
 
-  >>> print(http(r"""
-  ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
-  ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... Content-Type: application/x-www-form-urlencoded
-  ... Referer: https://launchpad.test/
-  ...
-  ... token=xn9FDCTp4m&option_12=2&option_13=3&option_14=4&option_15=1&changevote=Change+Vote"""))
-  HTTP/1.1 200 Ok
-  ...
-  ...Your vote was changed successfully.</p>
-  ...
-                  <p>Your current vote is as follows:</p>
-                  <p>
-  <BLANKLINE>
-                      <b>1</b>.
-                      Option 4
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>2</b>.
-                      Option 1
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>3</b>.
-                      Option 2
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>4</b>.
-                      Option 3
-  <BLANKLINE>
-                  </p>
-  ...
+    >>> print(http(r"""
+    ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
+    ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
+    ... Content-Type: application/x-www-form-urlencoded
+    ... Referer: https://launchpad.test/
+    ...
+    ... token=xn9FDCTp4m&option_12=2&option_13=3&option_14=4&option_15=1&changevote=Change+Vote"""))
+    HTTP/1.1 200 Ok
+    ...
+    ...Your vote was changed successfully.</p>
+    ...
+                    <p>Your current vote is as follows:</p>
+                    <p>
+    <BLANKLINE>
+                        <b>1</b>.
+                        Option 4
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>2</b>.
+                        Option 1
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>3</b>.
+                        Option 2
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>4</b>.
+                        Option 3
+    <BLANKLINE>
+                    </p>
+    ...
 
 
   Now we go to another poll in which name16 voted. But this time it's a public
   one, so there's no need to provide the token to see the current vote.
 
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
-  ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
-  ...
-
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
-  ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-                  <p>Your current vote is as follows:</p>
-                  <p>
-  <BLANKLINE>
-                      <b>1</b>.
-                      Option 1
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>2</b>.
-                      Option 2
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>3</b>.
-                      Option 3
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>4</b>.
-                      Option 4
-  <BLANKLINE>
-                  </p>
-  ...
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
+    ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
+    ...
+
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
+    ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+                    <p>Your current vote is as follows:</p>
+                    <p>
+    <BLANKLINE>
+                        <b>1</b>.
+                        Option 1
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>2</b>.
+                        Option 2
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>3</b>.
+                        Option 3
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>4</b>.
+                        Option 4
+    <BLANKLINE>
+                    </p>
+    ...
 
 
   Now we change the vote and we see the new vote displayed as our current
   vote.
 
-  >>> print(http(r"""
-  ... POST /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
-  ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... Content-Type: application/x-www-form-urlencoded
-  ... Referer: https://launchpad.test/
-  ...
-  ... option_16=4&option_17=2&option_18=1&option_19=3&changevote=Change+Vote"""))
-  HTTP/1.1 200 Ok
-  ...
-                  <p>Your current vote is as follows:</p>
-                  <p>
-  <BLANKLINE>
-                      <b>1</b>.
-                      Option 3
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>2</b>.
-                      Option 2
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>3</b>.
-                      Option 4
-  <BLANKLINE>
-                  </p>
-                  <p>
-  <BLANKLINE>
-                      <b>4</b>.
-                      Option 1
-  <BLANKLINE>
-                  </p>
-  ...
+    >>> print(http(r"""
+    ... POST /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
+    ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
+    ... Content-Type: application/x-www-form-urlencoded
+    ... Referer: https://launchpad.test/
+    ...
+    ... option_16=4&option_17=2&option_18=1&option_19=3&changevote=Change+Vote"""))
+    HTTP/1.1 200 Ok
+    ...
+                    <p>Your current vote is as follows:</p>
+                    <p>
+    <BLANKLINE>
+                        <b>1</b>.
+                        Option 3
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>2</b>.
+                        Option 2
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>3</b>.
+                        Option 4
+    <BLANKLINE>
+                    </p>
+                    <p>
+    <BLANKLINE>
+                        <b>4</b>.
+                        Option 1
+    <BLANKLINE>
+                    </p>
+    ...
 
 
   Logged in as mark@xxxxxxxxxxx (which is a member of ubuntu-team), go to a public
   condorcet-style poll that's still open and get redirected to a page where
   it's possible to vote (and see the current vote).
 
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
-  ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
-  ...
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
+    ... Authorization: Basic mark@xxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
+    ...
 
 
   And here we'll see the form which says you haven't voted yet and allows you
   to vote.
 
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
-  ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """))
-  HTTP/1.1 200 Ok
-  ...
-  ...Your current vote...
-  ...You have not yet voted in this poll...
-  ...Rank options in order...
-  ...
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
+    ... Authorization: Basic mark@xxxxxxxxxxx:test
+    ... """))
+    HTTP/1.1 200 Ok
+    ...
+    ...Your current vote...
+    ...You have not yet voted in this poll...
+    ...Rank options in order...
+    ...
diff --git a/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt b/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt
index 0b4628a..28a5ac1 100644
--- a/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt
+++ b/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt
@@ -1,94 +1,94 @@
   Logged in as 'jdub' (which voted in the director-2004 poll), let's see the
   results of the director-2004 poll.
 
-  >>> import base64
-  >>> jdub_auth = base64.b64encode(
-  ...     b'jeff.waugh@xxxxxxxxxxxxxxx:test').decode('ASCII')
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/director-2004 HTTP/1.1
-  ... Authorization: Basic %s
-  ... """ % jdub_auth))
-  HTTP/1.1 200 Ok
-  ...
-  ...2004 Director's Elections...
-  ...
-  ...This was a secret poll: your vote is identified only by the key...
-  ...you were given when you voted. To view your vote you must enter...
-  ...your key:...
-  ...Results...
-  ...This is the pairwise matrix for this poll...
-  ...
+    >>> import base64
+    >>> jdub_auth = base64.b64encode(
+    ...     b'jeff.waugh@xxxxxxxxxxxxxxx:test').decode('ASCII')
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/director-2004 HTTP/1.1
+    ... Authorization: Basic %s
+    ... """ % jdub_auth))
+    HTTP/1.1 200 Ok
+    ...
+    ...2004 Director's Elections...
+    ...
+    ...This was a secret poll: your vote is identified only by the key...
+    ...you were given when you voted. To view your vote you must enter...
+    ...your key:...
+    ...Results...
+    ...This is the pairwise matrix for this poll...
+    ...
 
 
   Now let's see if jdub's vote was stored correctly, by entering the token he
   got when voting.
 
-  >>> print(http(r"""
-  ... POST /~ubuntu-team/+poll/director-2004 HTTP/1.1
-  ... Authorization: Basic %s
-  ... Referer: https://launchpad.test/
-  ... Content-Type: application/x-www-form-urlencoded
-  ... 
-  ... token=9WjxQq2V9p&showvote=Show+My+Vote""" % jdub_auth))
-  HTTP/1.1 200 Ok
-  ...
-                <p>Your vote was as follows:</p>
-                <p>
-  <BLANKLINE>
-                    <b>1</b>. 
-                    D
-  <BLANKLINE>
-                </p>
-                <p>
-  <BLANKLINE>
-                    <b>2</b>. 
-                    B
-  <BLANKLINE>
-                </p>
-                <p>
-  <BLANKLINE>
-                    <b>3</b>. 
-                    A
-  <BLANKLINE>
-                </p>
-                <p>
-  <BLANKLINE>
-                    <b>3</b>. 
-                    C
-  <BLANKLINE>
-                </p>
-  ...
+    >>> print(http(r"""
+    ... POST /~ubuntu-team/+poll/director-2004 HTTP/1.1
+    ... Authorization: Basic %s
+    ... Referer: https://launchpad.test/
+    ... Content-Type: application/x-www-form-urlencoded
+    ... 
+    ... token=9WjxQq2V9p&showvote=Show+My+Vote""" % jdub_auth))
+    HTTP/1.1 200 Ok
+    ...
+                  <p>Your vote was as follows:</p>
+                  <p>
+    <BLANKLINE>
+                      <b>1</b>. 
+                      D
+    <BLANKLINE>
+                  </p>
+                  <p>
+    <BLANKLINE>
+                      <b>2</b>. 
+                      B
+    <BLANKLINE>
+                  </p>
+                  <p>
+    <BLANKLINE>
+                      <b>3</b>. 
+                      A
+    <BLANKLINE>
+                  </p>
+                  <p>
+    <BLANKLINE>
+                      <b>3</b>. 
+                      C
+    <BLANKLINE>
+                  </p>
+    ...
 
 
   Now we'll see the results of the leader-2004 poll, in which jdub also
   voted.
 
-  >>> print(http(r"""
-  ... GET /~ubuntu-team/+poll/leader-2004 HTTP/1.1
-  ... Authorization: Basic %s
-  ... """ % jdub_auth))
-  HTTP/1.1 200 Ok
-  ...
-  ...2004 Leader's Elections...
-  ...
-  ...This was a secret poll: your vote is identified only by the key...
-  ...you were given when you voted. To view your vote you must enter...
-  ...your key:...
-  ...
+    >>> print(http(r"""
+    ... GET /~ubuntu-team/+poll/leader-2004 HTTP/1.1
+    ... Authorization: Basic %s
+    ... """ % jdub_auth))
+    HTTP/1.1 200 Ok
+    ...
+    ...2004 Leader's Elections...
+    ...
+    ...This was a secret poll: your vote is identified only by the key...
+    ...you were given when you voted. To view your vote you must enter...
+    ...your key:...
+    ...
 
 
   And now we confirm his vote on this poll too.
 
-  >>> print(http(r"""
-  ... POST /~ubuntu-team/+poll/leader-2004 HTTP/1.1
-  ... Authorization: Basic %s
-  ... Referer: https://launchpad.test/
-  ... Content-Type: application/x-www-form-urlencoded
-  ... 
-  ... token=W7gR5mjNrX&showvote=Show+My+Vote""" % jdub_auth))
-  HTTP/1.1 200 Ok
-  ...
-              <p>Your vote was for
-  <BLANKLINE>
-                <b>Jack Crawford</b></p>
-  ...
+    >>> print(http(r"""
+    ... POST /~ubuntu-team/+poll/leader-2004 HTTP/1.1
+    ... Authorization: Basic %s
+    ... Referer: https://launchpad.test/
+    ... Content-Type: application/x-www-form-urlencoded
+    ... 
+    ... token=W7gR5mjNrX&showvote=Show+My+Vote""" % jdub_auth))
+    HTTP/1.1 200 Ok
+    ...
+                <p>Your vote was for
+    <BLANKLINE>
+                  <b>Jack Crawford</b></p>
+    ...
diff --git a/lib/lp/registry/stories/team-polls/xx-poll-results.txt b/lib/lp/registry/stories/team-polls/xx-poll-results.txt
index 286eeaa..219cc26 100644
--- a/lib/lp/registry/stories/team-polls/xx-poll-results.txt
+++ b/lib/lp/registry/stories/team-polls/xx-poll-results.txt
@@ -1,69 +1,70 @@
 First we check all polls of 'ubuntu-team'.
 
-  >>> anon_browser.open("http://launchpad.test/~ubuntu-team";)
-  >>> anon_browser.getLink('Show polls').click()
-  >>> print(find_main_content(anon_browser.contents))
-  <...
-  ...Current polls...
-  ...A random poll that never closes...
-  ...A second random poll that never closes...
-  ...A third random poll that never closes...
-  ...Closed polls...
-  ...2004 Director's Elections...
-  ...2004 Leader's Elections...
+    >>> anon_browser.open("http://launchpad.test/~ubuntu-team";)
+    >>> anon_browser.getLink('Show polls').click()
+    >>> print(find_main_content(anon_browser.contents))
+    <...
+    ...Current polls...
+    ...A random poll that never closes...
+    ...A second random poll that never closes...
+    ...A third random poll that never closes...
+    ...Closed polls...
+    ...2004 Director's Elections...
+    ...2004 Leader's Elections...
 
 
   Check the results of a closed simple-style poll.
 
-  >>> anon_browser.open("http://launchpad.test/~ubuntu-team/+poll/leader-2004";)
-  >>> print(find_main_content(anon_browser.contents))
-  <...
-  ...Who's going to be the next leader?...
-  ...Results...
-  ...
-              <td>
-                Francis Dolarhyde
-  <BLANKLINE>
-              </td>
-              <td>1</td>
-  ...
-              <td>
-                Jack Crawford
-  <BLANKLINE>
-              </td>
-              <td>1</td>
-  ...
-              <td>
-                Will Graham
-  <BLANKLINE>
-              </td>
-              <td>2</td>
-  ...
+    >>> anon_browser.open(
+    ...     "http://launchpad.test/~ubuntu-team/+poll/leader-2004";)
+    >>> print(find_main_content(anon_browser.contents))
+    <...
+    ...Who's going to be the next leader?...
+    ...Results...
+    ...
+                <td>
+                  Francis Dolarhyde
+    <BLANKLINE>
+                </td>
+                <td>1</td>
+    ...
+                <td>
+                  Jack Crawford
+    <BLANKLINE>
+                </td>
+                <td>1</td>
+    ...
+                <td>
+                  Will Graham
+    <BLANKLINE>
+                </td>
+                <td>2</td>
+    ...
 
 
   Check the results of a closed condorcet-style poll.
 
-  >>> anon_browser.open("http://launchpad.test/~ubuntu-team/+poll/director-2004";)
-  >>> print(find_main_content(anon_browser.contents))
-  <...
-  ...Who's going to be the next director?...
-  ...Results...
-  ...
-  ...A...
-  ...2...
-  ...2...
-  ...2...
-  ...B...
-  ...2...
-  ...2...
-  ...2...
-  ...C...
-  ...1...
-  ...1...
-  ...1...
-  ...D...
-  ...2...
-  ...1...
-  ...2...
-  ...
-
+    >>> anon_browser.open(
+    ...     "http://launchpad.test/~ubuntu-team/+poll/director-2004";)
+    >>> print(find_main_content(anon_browser.contents))
+    <...
+    ...Who's going to be the next director?...
+    ...Results...
+    ...
+    ...A...
+    ...2...
+    ...2...
+    ...2...
+    ...B...
+    ...2...
+    ...2...
+    ...2...
+    ...C...
+    ...1...
+    ...1...
+    ...1...
+    ...D...
+    ...2...
+    ...1...
+    ...2...
+    ...
diff --git a/lib/lp/services/feeds/stories/xx-navigation.txt b/lib/lp/services/feeds/stories/xx-navigation.txt
index 316d759..185a9fe 100644
--- a/lib/lp/services/feeds/stories/xx-navigation.txt
+++ b/lib/lp/services/feeds/stories/xx-navigation.txt
@@ -42,24 +42,24 @@ a predictable pattern. If someone enters a feed URL and it has
 uppercase letters in it, we redirect to the canonical form of the URL
 before serving the page.
 
-   >>> browser.open('http://feeds.launchpad.test/jOkOshEr/latest-bugs.html')
-   >>> browser.url
-   'http://feeds.launchpad.test/jokosher/latest-bugs.html'
+    >>> browser.open('http://feeds.launchpad.test/jOkOshEr/latest-bugs.html')
+    >>> browser.url
+    'http://feeds.launchpad.test/jokosher/latest-bugs.html'
 
 The +index view should redirect to https://help.launchpad.net/Feeds
 
 XXX Edwin Grubbs 2007-12-10 bug=98482: zope.testbrowser does not handle
 redirects to remote sites, but http() can be used instead.
 
-   >>> response = http(r"""
-   ... GET / HTTP/1.0
-   ... Host: feeds.launchpad.test
-   ... """)
-   >>> print(six.text_type(response))
-   HTTP/1.0 301 Moved Permanently
-   ...
-   Location: https://help.launchpad.net/Feeds
-   ...
+    >>> response = http(r"""
+    ... GET / HTTP/1.0
+    ... Host: feeds.launchpad.test
+    ... """)
+    >>> print(six.text_type(response))
+    HTTP/1.0 301 Moved Permanently
+    ...
+    Location: https://help.launchpad.net/Feeds
+    ...
 
 
 Query String Normalization
diff --git a/lib/lp/services/fields/doc/uri-field.txt b/lib/lp/services/fields/doc/uri-field.txt
index 65b65f4..5b60710 100644
--- a/lib/lp/services/fields/doc/uri-field.txt
+++ b/lib/lp/services/fields/doc/uri-field.txt
@@ -14,17 +14,17 @@ the following features:
 
 To demonstrate, we'll create a sample interface:
 
-  >>> from zope.interface import Interface, implementer
-  >>> from lp.services.fields import URIField
-  >>> class IURIFieldTest(Interface):
-  ...     field = URIField()
-  ...     sftp_only = URIField(allowed_schemes=['sftp'])
-  ...     no_userinfo = URIField(allow_userinfo=False)
-  ...     no_port = URIField(allow_port=False)
-  ...     no_query = URIField(allow_query=False)
-  ...     no_fragment = URIField(allow_fragment=False)
-  ...     with_slash = URIField(trailing_slash=True)
-  ...     without_slash = URIField(trailing_slash=False)
+    >>> from zope.interface import Interface, implementer
+    >>> from lp.services.fields import URIField
+    >>> class IURIFieldTest(Interface):
+    ...     field = URIField()
+    ...     sftp_only = URIField(allowed_schemes=['sftp'])
+    ...     no_userinfo = URIField(allow_userinfo=False)
+    ...     no_port = URIField(allow_port=False)
+    ...     no_query = URIField(allow_query=False)
+    ...     no_fragment = URIField(allow_fragment=False)
+    ...     with_slash = URIField(trailing_slash=True)
+    ...     without_slash = URIField(trailing_slash=False)
 
 
 Validation
@@ -33,13 +33,13 @@ Validation
 In its most basic form, the field validator makes sure the value is a
 valid URI:
 
-  >>> field = IURIFieldTest['field']
-  >>> field.validate(u'http://launchpad.net/')
-  >>> field.validate(u'not-a-uri')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.validators.LaunchpadValidationError: &quot;not-a-uri&quot; is not a valid URI
+    >>> field = IURIFieldTest['field']
+    >>> field.validate(u'http://launchpad.net/')
+    >>> field.validate(u'not-a-uri')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.validators.LaunchpadValidationError: &quot;not-a-uri&quot; is not a valid URI
 
 
 Scheme Restrictions
@@ -49,13 +49,13 @@ If the allowed_schemes argument is specified for the field, then only
 URIs matching one of those schemes will be accepted.  Other schemes
 will result in a validation error:
 
-  >>> sftp_only = IURIFieldTest['sftp_only']
-  >>> sftp_only.validate(u'sFtp://launchpad.net/')
-  >>> sftp_only.validate(u'http://launchpad.net/')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.validators.LaunchpadValidationError: The URI scheme &quot;http&quot; is not allowed. Only URIs with the following schemes may be used: sftp
+    >>> sftp_only = IURIFieldTest['sftp_only']
+    >>> sftp_only.validate(u'sFtp://launchpad.net/')
+    >>> sftp_only.validate(u'http://launchpad.net/')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.validators.LaunchpadValidationError: The URI scheme &quot;http&quot; is not allowed. Only URIs with the following schemes may be used: sftp
 
 
 Disallowing Userinfo
@@ -65,18 +65,18 @@ The field can be configured to reject URIs with a userinfo portion.
 This can be useful to catch possible phishing attempts for URIs like a
 product home page, where authentication is not generally required:
 
-  >>> no_userinfo = IURIFieldTest['no_userinfo']
-  >>> no_userinfo.validate(u'http://launchpad.net:80@127.0.0.1/ubuntu')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.validators.LaunchpadValidationError: A username may not be specified in the URI.
+    >>> no_userinfo = IURIFieldTest['no_userinfo']
+    >>> no_userinfo.validate(u'http://launchpad.net:80@127.0.0.1/ubuntu')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.validators.LaunchpadValidationError: A username may not be specified in the URI.
 
-  >>> no_userinfo.validate(u'http://launchpad.net@127.0.0.1/ubuntu')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.validators.LaunchpadValidationError: A username may not be specified in the URI.
+    >>> no_userinfo.validate(u'http://launchpad.net@127.0.0.1/ubuntu')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.validators.LaunchpadValidationError: A username may not be specified in the URI.
 
 
 Disallowing Non-default Ports
@@ -85,17 +85,17 @@ Disallowing Non-default Ports
 For some URIs we will want to disallow using non-default ports in
 URIs.  This can be done with the allow_port option:
 
-  >>> no_port = IURIFieldTest['no_port']
-  >>> no_port.validate(u'http://launchpad.net:21')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.validators.LaunchpadValidationError: Non-default ports are not allowed.
+    >>> no_port = IURIFieldTest['no_port']
+    >>> no_port.validate(u'http://launchpad.net:21')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.validators.LaunchpadValidationError: Non-default ports are not allowed.
 
 Note that an error is not raised if the URI specifies a port but it is
 known to be the default for that scheme:
 
-  >>> no_port.validate(u'http://launchpad.net:80/')
+    >>> no_port.validate(u'http://launchpad.net:80/')
 
 
 Disallowing the Query Component
@@ -105,12 +105,12 @@ For some URIs (such as Bazaar branch URLs), it doesn't make sense to
 include a query component.  The allow_query argument can be used to
 reject those URIs:
 
-  >>> no_query = IURIFieldTest['no_query']
-  >>> no_query.validate(u'http://launchpad.net/?key=value')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.validators.LaunchpadValidationError: URIs with query strings are not allowed.
+    >>> no_query = IURIFieldTest['no_query']
+    >>> no_query.validate(u'http://launchpad.net/?key=value')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.validators.LaunchpadValidationError: URIs with query strings are not allowed.
 
 
 Disallowing the Fragment Component
@@ -118,12 +118,12 @@ Disallowing the Fragment Component
 
 The fragment component can also be disallowed:
 
-  >>> no_fragment = IURIFieldTest['no_fragment']
-  >>> no_fragment.validate(u'http://launchpad.net/#fragment')
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.validators.LaunchpadValidationError: URIs with fragment identifiers are not allowed.
+    >>> no_fragment = IURIFieldTest['no_fragment']
+    >>> no_fragment.validate(u'http://launchpad.net/#fragment')
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.validators.LaunchpadValidationError: URIs with fragment identifiers are not allowed.
 
 
 == Requiring or Forbidding a Trailing Slash ===
@@ -135,41 +135,43 @@ in a normalised form.
 
 The default behaviour is to allow both cases:
 
-  >>> with_slash = IURIFieldTest['with_slash']
-  >>> print(with_slash.normalize(u'http://launchpad.net/ubuntu/'))
-  http://launchpad.net/ubuntu/
-  >>> print(with_slash.normalize(
-  ...     u'http://launchpad.net/ubuntu/?query#fragment'))
-  http://launchpad.net/ubuntu/?query#fragment
-  >>> print(with_slash.normalize(u'http://launchpad.net/ubuntu'))
-  http://launchpad.net/ubuntu/
-  >>> print(with_slash.normalize(u'http://launchpad.net'))
-  http://launchpad.net/
+    >>> with_slash = IURIFieldTest['with_slash']
+    >>> print(with_slash.normalize(u'http://launchpad.net/ubuntu/'))
+    http://launchpad.net/ubuntu/
+    >>> print(with_slash.normalize(
+    ...     u'http://launchpad.net/ubuntu/?query#fragment'))
+    http://launchpad.net/ubuntu/?query#fragment
+    >>> print(with_slash.normalize(u'http://launchpad.net/ubuntu'))
+    http://launchpad.net/ubuntu/
+    >>> print(with_slash.normalize(u'http://launchpad.net'))
+    http://launchpad.net/
 
 Similarly, we can require that the URI path does not end in a slash:
 
-  >>> without_slash = IURIFieldTest['without_slash']
-  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu'))
-  http://launchpad.net/ubuntu
-  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu/#fragment'))
-  http://launchpad.net/ubuntu#fragment
-  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu#fragment/'))
-  http://launchpad.net/ubuntu#fragment/
-  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu/'))
-  http://launchpad.net/ubuntu
+    >>> without_slash = IURIFieldTest['without_slash']
+    >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu'))
+    http://launchpad.net/ubuntu
+    >>> print(without_slash.normalize(
+    ...     u'http://launchpad.net/ubuntu/#fragment'))
+    http://launchpad.net/ubuntu#fragment
+    >>> print(without_slash.normalize(
+    ...     u'http://launchpad.net/ubuntu#fragment/'))
+    http://launchpad.net/ubuntu#fragment/
+    >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu/'))
+    http://launchpad.net/ubuntu
 
 URIs with an authority but a blank path get canonicalised to a path of
 "/", which is not affected by the without_slash setting.
 
-  >>> print(with_slash.normalize(u'http://launchpad.net/'))
-  http://launchpad.net/
-  >>> print(with_slash.normalize(u'http://launchpad.net'))
-  http://launchpad.net/
+    >>> print(with_slash.normalize(u'http://launchpad.net/'))
+    http://launchpad.net/
+    >>> print(with_slash.normalize(u'http://launchpad.net'))
+    http://launchpad.net/
 
-  >>> print(without_slash.normalize(u'http://launchpad.net/'))
-  http://launchpad.net/
-  >>> print(without_slash.normalize(u'http://launchpad.net'))
-  http://launchpad.net/
+    >>> print(without_slash.normalize(u'http://launchpad.net/'))
+    http://launchpad.net/
+    >>> print(without_slash.normalize(u'http://launchpad.net'))
+    http://launchpad.net/
 
 
 Null values
@@ -177,9 +179,9 @@ Null values
 
 None is an acceptable value for a URI field.
 
-  >>> field = URIField(__name__='foo', title=u'Foo')
-  >>> print(field.normalize(None))
-  None
+    >>> field = URIField(__name__='foo', title=u'Foo')
+    >>> print(field.normalize(None))
+    None
 
 
 URIWidget
@@ -192,30 +194,30 @@ standard text widget with the following differences:
 
 This widget is registered as an input widget:
 
-  >>> from zope.formlib.interfaces import IInputWidget
-  >>> from zope.component import getMultiAdapter
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from zope.formlib.interfaces import IInputWidget
+    >>> from zope.component import getMultiAdapter
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
-  >>> @implementer(IURIFieldTest)
-  ... class URIFieldTest(object):
-  ...     field = None
+    >>> @implementer(IURIFieldTest)
+    ... class URIFieldTest(object):
+    ...     field = None
 
-  >>> context = URIFieldTest()
-  >>> field = IURIFieldTest['field'].bind(context)
-  >>> request = LaunchpadTestRequest()
-  >>> widget = getMultiAdapter((field, request), IInputWidget)
-  >>> print(widget)
-  <lp.app.widgets.textwidgets.URIWidget object at ...>
+    >>> context = URIFieldTest()
+    >>> field = IURIFieldTest['field'].bind(context)
+    >>> request = LaunchpadTestRequest()
+    >>> widget = getMultiAdapter((field, request), IInputWidget)
+    >>> print(widget)
+    <lp.app.widgets.textwidgets.URIWidget object at ...>
 
 Multiple values will cause an UnexpectedFormData exception:
 
-  >>> widget._toFieldValue(['http://launchpad.net', 'http://ubuntu.com'])
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-    ...
-  lp.app.errors.UnexpectedFormData: Only a single value is expected
+    >>> widget._toFieldValue(['http://launchpad.net', 'http://ubuntu.com'])
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+      ...
+    lp.app.errors.UnexpectedFormData: Only a single value is expected
 
 Values with leading and trailing whitespace are stripped.
 
-   >>> print(widget._toFieldValue('  http://www.ubuntu.com/   '))
-   http://www.ubuntu.com/
+    >>> print(widget._toFieldValue('  http://www.ubuntu.com/   '))
+    http://www.ubuntu.com/
diff --git a/lib/lp/services/gpg/doc/gpg-signatures.txt b/lib/lp/services/gpg/doc/gpg-signatures.txt
index fe03593..2c46970 100644
--- a/lib/lp/services/gpg/doc/gpg-signatures.txt
+++ b/lib/lp/services/gpg/doc/gpg-signatures.txt
@@ -67,8 +67,8 @@ The text below was "clear signed" by a 0x02BA5EF6, a subkey of 0xDFD20543
     A419AE861E88BC9E04B9C26FBA2B9389DFD20543
 
 
-   >>> master_sig.fingerprint == subkey_sig.fingerprint
-   True
+    >>> master_sig.fingerprint == subkey_sig.fingerprint
+    True
 
 The text below was "clear signed" by 0xDFD20543 master key but tampered with:
 
diff --git a/lib/lp/services/webapp/doc/launchbag.txt b/lib/lp/services/webapp/doc/launchbag.txt
index ce05eba..db16e8c 100644
--- a/lib/lp/services/webapp/doc/launchbag.txt
+++ b/lib/lp/services/webapp/doc/launchbag.txt
@@ -4,72 +4,72 @@ filter or otherwise specialize views or behaviour.
 
 First, we'll set up various imports and stub objects.
 
->>> from zope.component import getUtility
->>> from lp.services.webapp.interfaces import ILaunchBag
->>> from lp.services.webapp.interfaces import BasicAuthLoggedInEvent
->>> from lp.services.webapp.interfaces import LoggedOutEvent
->>> from lp.services.webapp.interfaces import \
-...     CookieAuthPrincipalIdentifiedEvent
-
->>> class Principal(object):
-...     id = 23
-
->>> principal = Principal()
->>> class Participation(object):
-...     principal = principal
-...     interaction = None
-
->>> class Response(object):
-...     def getCookie(self, name):
-...         return None
-
->>> class Request(object):
-...     principal = principal
-...     response = Response()
-...     cookies = {}
-...     def setPrincipal(self, principal):
-...         pass
-
->>> request = Request()
+    >>> from zope.component import getUtility
+    >>> from lp.services.webapp.interfaces import ILaunchBag
+    >>> from lp.services.webapp.interfaces import BasicAuthLoggedInEvent
+    >>> from lp.services.webapp.interfaces import LoggedOutEvent
+    >>> from lp.services.webapp.interfaces import \
+    ...     CookieAuthPrincipalIdentifiedEvent
+
+    >>> class Principal(object):
+    ...     id = 23
+
+    >>> principal = Principal()
+    >>> class Participation(object):
+    ...     principal = principal
+    ...     interaction = None
+
+    >>> class Response(object):
+    ...     def getCookie(self, name):
+    ...         return None
+
+    >>> class Request(object):
+    ...     principal = principal
+    ...     response = Response()
+    ...     cookies = {}
+    ...     def setPrincipal(self, principal):
+    ...         pass
+
+    >>> request = Request()
 
 There have been no logins, so launchbag.login will be None.
 
->>> launchbag = getUtility(ILaunchBag)
->>> print(launchbag.login)
-None
+    >>> launchbag = getUtility(ILaunchBag)
+    >>> print(launchbag.login)
+    None
 
 Let's send a basic auth login event.
 
->>> login = "foo.bar@xxxxxxxxxxxxx"
->>> event = BasicAuthLoggedInEvent(request, login, principal)
->>> from zope.event import notify
->>> notify(event)
+    >>> login = "foo.bar@xxxxxxxxxxxxx"
+    >>> event = BasicAuthLoggedInEvent(request, login, principal)
+    >>> from zope.event import notify
+    >>> notify(event)
 
 Now, launchbag.login will be 'foo.bar@xxxxxxxxxxxxx'.
 
->>> print(launchbag.login)
-foo.bar@xxxxxxxxxxxxx
+    >>> print(launchbag.login)
+    foo.bar@xxxxxxxxxxxxx
 
 Login should be set back to None on a logout.
 
->>> event = LoggedOutEvent(request)
->>> notify(event)
->>> print(launchbag.login)
-None
+    >>> event = LoggedOutEvent(request)
+    >>> notify(event)
+    >>> print(launchbag.login)
+    None
 
 'user' will also be set to None:
 
->>> print(launchbag.user)
-None
+    >>> print(launchbag.user)
+    None
 
 Let's do a cookie auth principal identification.  In this case, the login
 will be cookie@xxxxxxxxxxx.
 
->>> event = CookieAuthPrincipalIdentifiedEvent(
-...     principal, request, 'cookie@xxxxxxxxxxx')
->>> notify(event)
->>> print(launchbag.login)
-cookie@xxxxxxxxxxx
+    >>> event = CookieAuthPrincipalIdentifiedEvent(
+    ...     principal, request, 'cookie@xxxxxxxxxxx')
+    >>> notify(event)
+    >>> print(launchbag.login)
+    cookie@xxxxxxxxxxx
 
 
 time_zone
diff --git a/lib/lp/services/webapp/doc/uri.txt b/lib/lp/services/webapp/doc/uri.txt
index b0ab76d..00a1930 100644
--- a/lib/lp/services/webapp/doc/uri.txt
+++ b/lib/lp/services/webapp/doc/uri.txt
@@ -4,84 +4,84 @@ Security Proxied URI Objects
 URI objects can be compared for equality even in the presence of Zope
 security proxies.
 
-  >>> from zope.security.proxy import ProxyFactory
-  >>> from lazr.uri import URI
+    >>> from zope.security.proxy import ProxyFactory
+    >>> from lazr.uri import URI
 
-  >>> uri1 = URI('http://a/b/c/d;p?q')
-  >>> uri2 = URI('http://a/b/c/d;p?q')
-  >>> uri3 = URI('https://launchpad.net')
-  >>> proxied_uri1 = ProxyFactory(uri1)
-  >>> proxied_uri2 = ProxyFactory(uri2)
-  >>> proxied_uri3 = ProxyFactory(uri3)
+    >>> uri1 = URI('http://a/b/c/d;p?q')
+    >>> uri2 = URI('http://a/b/c/d;p?q')
+    >>> uri3 = URI('https://launchpad.net')
+    >>> proxied_uri1 = ProxyFactory(uri1)
+    >>> proxied_uri2 = ProxyFactory(uri2)
+    >>> proxied_uri3 = ProxyFactory(uri3)
 
 We can access the various URI components:
 
-  >>> print(proxied_uri1.scheme)
-  http
-  >>> print(proxied_uri1.userinfo)
-  None
-  >>> print(proxied_uri1.host)
-  a
-  >>> print(proxied_uri1.port)
-  None
-  >>> print(proxied_uri1.path)
-  /b/c/d;p
-  >>> print(proxied_uri1.query)
-  q
-  >>> print(proxied_uri1.fragment)
-  None
-  >>> print(proxied_uri1.authority)
-  a
-  >>> print(proxied_uri1.hier_part)
-  //a/b/c/d;p
+    >>> print(proxied_uri1.scheme)
+    http
+    >>> print(proxied_uri1.userinfo)
+    None
+    >>> print(proxied_uri1.host)
+    a
+    >>> print(proxied_uri1.port)
+    None
+    >>> print(proxied_uri1.path)
+    /b/c/d;p
+    >>> print(proxied_uri1.query)
+    q
+    >>> print(proxied_uri1.fragment)
+    None
+    >>> print(proxied_uri1.authority)
+    a
+    >>> print(proxied_uri1.hier_part)
+    //a/b/c/d;p
 
 We can test for equality:
 
-  >>> uri1 == uri2
-  True
-  >>> uri1 == proxied_uri2
-  True
-  >>> proxied_uri1 == uri2
-  True
-  >>> proxied_uri1 == proxied_uri2
-  True
+    >>> uri1 == uri2
+    True
+    >>> uri1 == proxied_uri2
+    True
+    >>> proxied_uri1 == uri2
+    True
+    >>> proxied_uri1 == proxied_uri2
+    True
 
-  >>> proxied_uri1 == proxied_uri3
-  False
+    >>> proxied_uri1 == proxied_uri3
+    False
 
 Similarly, inequality can be checked:
 
-  >>> proxied_uri1 != proxied_uri3
-  True
+    >>> proxied_uri1 != proxied_uri3
+    True
 
 We can get the string value and representation of a URI:
 
-  >>> print(str(proxied_uri1))
-  http://a/b/c/d;p?q
-  >>> print(repr(proxied_uri1))
-  URI('http://a/b/c/d;p?q')
+    >>> print(str(proxied_uri1))
+    http://a/b/c/d;p?q
+    >>> print(repr(proxied_uri1))
+    URI('http://a/b/c/d;p?q')
 
 We can replace components:
 
-  >>> print(proxied_uri1.replace(scheme='https'))
-  https://a/b/c/d;p?q
+    >>> print(proxied_uri1.replace(scheme='https'))
+    https://a/b/c/d;p?q
 
 We can append a component:
 
-  >>> print(proxied_uri1.append('e/f'))
-  http://a/b/c/d;p/e/f
+    >>> print(proxied_uri1.append('e/f'))
+    http://a/b/c/d;p/e/f
 
 We can check for containment:
 
-  >>> proxied_uri1.contains(proxied_uri2)
-  True
-  >>> proxied_uri1.contains(proxied_uri3)
-  False
+    >>> proxied_uri1.contains(proxied_uri2)
+    True
+    >>> proxied_uri1.contains(proxied_uri3)
+    False
 
 We can create a URI that ensures it has or does not have a trailing
 slash:
 
-  >>> print(proxied_uri1.ensureSlash())
-  http://a/b/c/d;p/?q
-  >>> print(proxied_uri1.ensureNoSlash())
-  http://a/b/c/d;p?q
+    >>> print(proxied_uri1.ensureSlash())
+    http://a/b/c/d;p/?q
+    >>> print(proxied_uri1.ensureNoSlash())
+    http://a/b/c/d;p?q
diff --git a/lib/lp/services/webservice/doc/webservice-error.txt b/lib/lp/services/webservice/doc/webservice-error.txt
index 34f7ef9..b96dfbc 100644
--- a/lib/lp/services/webservice/doc/webservice-error.txt
+++ b/lib/lp/services/webservice/doc/webservice-error.txt
@@ -36,4 +36,4 @@ IRequestExpired exceptions have a 503 status code.
 
 Cleanup.
 
-   >>> clear_request_started()
+    >>> clear_request_started()
diff --git a/lib/lp/services/webservice/stories/conditional-write.txt b/lib/lp/services/webservice/stories/conditional-write.txt
index 44502fd..8ed192f 100644
--- a/lib/lp/services/webservice/stories/conditional-write.txt
+++ b/lib/lp/services/webservice/stories/conditional-write.txt
@@ -9,37 +9,37 @@ the problem crops up again.
 Here's a bug: it has an ETag and values for fields like
 'date_last_message'.
 
-  >>> url = '/bugs/1'
-  >>> bug = webservice.get(url).jsonBody()
-  >>> old_etag = bug['http_etag']
-  >>> old_date_last_message = bug['date_last_message']
+    >>> url = '/bugs/1'
+    >>> bug = webservice.get(url).jsonBody()
+    >>> old_etag = bug['http_etag']
+    >>> old_date_last_message = bug['date_last_message']
 
 When we add a message to a bug, 'date_last_message' is changed as a
 side effect.
 
-  >>> print(webservice.named_post(
-  ...     url, 'newMessage', subject="subject", content="content"))
-  HTTP/1.1 201 Created
-  ...
+    >>> print(webservice.named_post(
+    ...     url, 'newMessage', subject="subject", content="content"))
+    HTTP/1.1 201 Created
+    ...
 
-  >>> new_bug = webservice.get(url).jsonBody()
-  >>> new_date_last_message = new_bug['date_last_message']
-  >>> old_date_last_message == new_date_last_message
-  False
+    >>> new_bug = webservice.get(url).jsonBody()
+    >>> new_date_last_message = new_bug['date_last_message']
+    >>> old_date_last_message == new_date_last_message
+    False
 
 Because 'date_last_message' changed, the bug resource's ETag also
 changed:
 
-  >>> new_etag = new_bug['http_etag']
-  >>> old_etag == new_etag
-  False
+    >>> new_etag = new_bug['http_etag']
+    >>> old_etag == new_etag
+    False
 
 A conditional GET request using the old ETag will fail, and the client
 will hear about the new value for 'date_last_message'.
 
-  >>> print(webservice.get(url, headers={'If-None-Match' : old_etag}))
-  HTTP/1.1 200 Ok
-  ...
+    >>> print(webservice.get(url, headers={'If-None-Match' : old_etag}))
+    HTTP/1.1 200 Ok
+    ...
 
 But what if we want to PATCH the bug object after adding a message?
 Logically speaking, the PATCH should go through. 'date_last_message' has
@@ -53,49 +53,51 @@ lazr.restful resolves this by splitting the ETag into two parts. The
 first part changes only on changes to fields that clients cannot
 modify directly, like 'date_last_message':
 
-  >>> old_read, old_write = old_etag.rsplit('-', 1)
-  >>> new_read, new_write = new_etag.rsplit('-', 1)
-  >>> old_read == new_read
-  False
+    >>> old_read, old_write = old_etag.rsplit('-', 1)
+    >>> new_read, new_write = new_etag.rsplit('-', 1)
+    >>> old_read == new_read
+    False
 
 The second part changes only on changes to fields that a client could
 modify directly.
 
-  >>> old_write == new_write
-  True
+    >>> old_write == new_write
+    True
 
 So long as the second part of the submitted ETag matches, a
 conditional write will succeed.
 
-  >>> import simplejson
-  >>> data = simplejson.dumps({'title' : 'New title'})
-  >>> headers = {'If-Match': old_etag}
-  >>> print(webservice.patch(url, 'application/json', data, headers=headers))
-  HTTP/1.1 209 Content Returned
-  ...
+    >>> import simplejson
+    >>> data = simplejson.dumps({'title' : 'New title'})
+    >>> headers = {'If-Match': old_etag}
+    >>> print(webservice.patch(
+    ...     url, 'application/json', data, headers=headers))
+    HTTP/1.1 209 Content Returned
+    ...
 
 Of course, now the resource has been modified by a client, and the
 ETag has changed.
 
-  >>> newer_etag = webservice.get(url).jsonBody()['http_etag']
-  >>> newer_read, newer_write = newer_etag.rsplit('-', 1)
+    >>> newer_etag = webservice.get(url).jsonBody()['http_etag']
+    >>> newer_read, newer_write = newer_etag.rsplit('-', 1)
 
 Both portions of the ETag has changed: the write portion because we
 just changed 'description', and the read portion because
 'date_last_updated' changed as a side effect.
 
-  >>> new_read == newer_read
-  False
-  >>> new_write == newer_write
-  False
+    >>> new_read == newer_read
+    False
+    >>> new_write == newer_write
+    False
 
 A conditional write will fail when the write portion of the submitted
 ETag doesn't match, even if the read portion matches.
 
-  >>> headers = {'If-Match': new_etag}
-  >>> print(webservice.patch(url, 'application/json', data, headers=headers))
-  HTTP/1.1 412 Precondition Failed
-  ...
+    >>> headers = {'If-Match': new_etag}
+    >>> print(webservice.patch(
+    ...     url, 'application/json', data, headers=headers))
+    HTTP/1.1 412 Precondition Failed
+    ...
 
 When two clients attempt overlapping modifications of the same
 resource, the later one still gets a 412 error. If an unwritable field
@@ -111,29 +113,29 @@ same. Apache's mod_compress modifies outgoing ETags when it compresses
 the representations. Launchpad's web service will treat an ETag
 modified by mod_compress as though it were the original ETag.
 
-  >>> etag = webservice.get(url).jsonBody()['http_etag']
+    >>> etag = webservice.get(url).jsonBody()['http_etag']
 
-  >>> headers = {'If-None-Match': etag}
-  >>> print(webservice.get(url, headers=headers))
-  HTTP/1.1 304 Not Modified
-  ...
+    >>> headers = {'If-None-Match': etag}
+    >>> print(webservice.get(url, headers=headers))
+    HTTP/1.1 304 Not Modified
+    ...
 
 Some versions of mod_compress turn '"foo"' into '"foo"-gzip', and some
 versions turn it into '"foo-gzip"'. We treat all three forms the same.
 
-  >>> headers = {'If-None-Match': etag + "-gzip"}
-  >>> print(webservice.get(url, headers=headers))
-  HTTP/1.1 304 Not Modified
-  ...
+    >>> headers = {'If-None-Match': etag + "-gzip"}
+    >>> print(webservice.get(url, headers=headers))
+    HTTP/1.1 304 Not Modified
+    ...
 
-  >>> headers = {'If-None-Match': etag[:-1] + "-gzip" + etag[-1]}
-  >>> print(webservice.get(url, headers=headers))
-  HTTP/1.1 304 Not Modified
-  ...
+    >>> headers = {'If-None-Match': etag[:-1] + "-gzip" + etag[-1]}
+    >>> print(webservice.get(url, headers=headers))
+    HTTP/1.1 304 Not Modified
+    ...
 
 Any other modification to the ETag is treated as a distinct ETag.
 
-  >>> headers = {'If-None-Match': etag + "-not-gzip"}
-  >>> print(webservice.get(url, headers=headers))
-  HTTP/1.1 200 Ok
-  ...
+    >>> headers = {'If-None-Match': etag + "-not-gzip"}
+    >>> print(webservice.get(url, headers=headers))
+    HTTP/1.1 200 Ok
+    ...
diff --git a/lib/lp/services/webservice/stories/datamodel.txt b/lib/lp/services/webservice/stories/datamodel.txt
index 5ca2388..cfda663 100644
--- a/lib/lp/services/webservice/stories/datamodel.txt
+++ b/lib/lp/services/webservice/stories/datamodel.txt
@@ -7,50 +7,50 @@ uses Storm backed by a database. This means it's nice to have some
 end-to-end tests of code paths that, on the surface, look like they're
 already tested in lazr.restful.
 
-  >>> def get_collection(version="devel", start=0, size=2):
-  ...     collection = webservice.get(
-  ...         ("/people?ws.op=find&text=s&ws.start=%s&ws.size=%s" %
-  ...          (start, size)),
-  ...         api_version=version)
-  ...     return collection.jsonBody()
+    >>> def get_collection(version="devel", start=0, size=2):
+    ...     collection = webservice.get(
+    ...         ("/people?ws.op=find&text=s&ws.start=%s&ws.size=%s" %
+    ...          (start, size)),
+    ...         api_version=version)
+    ...     return collection.jsonBody()
 
 
 Normally, the total size of a collection is not served along with the
 collection; it's available by following the total_size_link.
 
-  >>> collection = get_collection()
-  >>> for key in sorted(collection.keys()):
-  ...     print(key)
-  entries
-  next_collection_link
-  start
-  total_size_link
-  >>> print(webservice.get(collection['total_size_link']).jsonBody())
-  9
+    >>> collection = get_collection()
+    >>> for key in sorted(collection.keys()):
+    ...     print(key)
+    entries
+    next_collection_link
+    start
+    total_size_link
+    >>> print(webservice.get(collection['total_size_link']).jsonBody())
+    9
 
 If an entire collection fits on one page (making the size of the
 collection obvious), 'total_size' is served instead of
 'total_size_link'.
 
-  >>> collection = get_collection(size=100)
-  >>> for key in sorted(collection.keys()):
-  ...     print(key)
-  entries
-  start
-  total_size
-  >>> print(collection['total_size'])
-  9
+    >>> collection = get_collection(size=100)
+    >>> for key in sorted(collection.keys()):
+    ...     print(key)
+    entries
+    start
+    total_size
+    >>> print(collection['total_size'])
+    9
 
 If the last page of the collection is fetched (making the total size
 of the collection semi-obvious), 'total_size' is served instead of
 'total_size_link'.
 
-  >>> collection = get_collection(start=8)
-  >>> for key in sorted(collection.keys()):
-  ...     print(key)
-  entries
-  prev_collection_link
-  start
-  total_size
-  >>> print(collection['total_size'])
-  9
+    >>> collection = get_collection(start=8)
+    >>> for key in sorted(collection.keys()):
+    ...     print(key)
+    entries
+    prev_collection_link
+    start
+    total_size
+    >>> print(collection['total_size'])
+    9
diff --git a/lib/lp/services/webservice/stories/multiversion.txt b/lib/lp/services/webservice/stories/multiversion.txt
index 683325f..e628434 100644
--- a/lib/lp/services/webservice/stories/multiversion.txt
+++ b/lib/lp/services/webservice/stories/multiversion.txt
@@ -9,35 +9,35 @@ In the 'devel' version of the web service, named operations that
 return collections will return a 'total_size_link' pointing to the
 total size of the collection.
 
-  >>> def get_collection(version, start=0, size=2):
-  ...     collection = webservice.get(
-  ...         ("/people?ws.op=find&text=s&ws.start=%s&ws.size=%s" %
-  ...          (start, size)),
-  ...         api_version=version)
-  ...     return collection.jsonBody()
+    >>> def get_collection(version, start=0, size=2):
+    ...     collection = webservice.get(
+    ...         ("/people?ws.op=find&text=s&ws.start=%s&ws.size=%s" %
+    ...          (start, size)),
+    ...         api_version=version)
+    ...     return collection.jsonBody()
 
-  >>> collection = get_collection("devel")
-  >>> for key in sorted(collection.keys()):
-  ...     print(key)
-  entries
-  next_collection_link
-  start
-  total_size_link
-  >>> print(webservice.get(collection['total_size_link']).jsonBody())
-  9
+    >>> collection = get_collection("devel")
+    >>> for key in sorted(collection.keys()):
+    ...     print(key)
+    entries
+    next_collection_link
+    start
+    total_size_link
+    >>> print(webservice.get(collection['total_size_link']).jsonBody())
+    9
 
 In previous versions, the same named operations will return a
 'total_size' containing the actual size of the collection.
 
-  >>> collection = get_collection("1.0")
-  >>> for key in sorted(collection.keys()):
-  ...     print(key)
-  entries
-  next_collection_link
-  start
-  total_size
-  >>> print(collection['total_size'])
-  9
+    >>> collection = get_collection("1.0")
+    >>> for key in sorted(collection.keys()):
+    ...     print(key)
+    entries
+    next_collection_link
+    start
+    total_size
+    >>> print(collection['total_size'])
+    9
 
 Mutator operations
 ==================
@@ -46,31 +46,32 @@ In the 'beta' version of the web service, mutator methods like
 IBugTask.transitionToStatus are published as named operations. In
 subsequent versions, those named operations are not published.
 
-  >>> from operator import itemgetter
+    >>> from operator import itemgetter
 
-  >>> def get_bugtask_path(version):
-  ...     bug_one = webservice.get("/bugs/1", api_version=version).jsonBody()
-  ...     bug_one_bugtasks_url = bug_one['bug_tasks_collection_link']
-  ...     bug_one_bugtasks = sorted(webservice.get(
-  ...         bug_one_bugtasks_url).jsonBody()['entries'],
-  ...         key=itemgetter('self_link'))
-  ...     bugtask_path = bug_one_bugtasks[0]['self_link']
-  ...     return bugtask_path
+    >>> def get_bugtask_path(version):
+    ...     bug_one = webservice.get(
+    ...         "/bugs/1", api_version=version).jsonBody()
+    ...     bug_one_bugtasks_url = bug_one['bug_tasks_collection_link']
+    ...     bug_one_bugtasks = sorted(webservice.get(
+    ...         bug_one_bugtasks_url).jsonBody()['entries'],
+    ...         key=itemgetter('self_link'))
+    ...     bugtask_path = bug_one_bugtasks[0]['self_link']
+    ...     return bugtask_path
 
 Here's the 'beta' version, where the named operation works.
 
-  >>> url = get_bugtask_path('beta')
-  >>> print(webservice.named_post(
-  ...     url, 'transitionToImportance', importance='Low',
-  ...     api_version='beta'))
-  HTTP/1.1 200 Ok
-  ...
+    >>> url = get_bugtask_path('beta')
+    >>> print(webservice.named_post(
+    ...     url, 'transitionToImportance', importance='Low',
+    ...     api_version='beta'))
+    HTTP/1.1 200 Ok
+    ...
 
 Now let's try the same thing in the '1.0' version, where it fails.
 
-  >>> url = get_bugtask_path('1.0')
-  >>> print(webservice.named_post(
-  ...     url, 'transitionToImportance', importance='Low',
-  ...     api_version='devel'))
-  HTTP/1.1 405 Method Not Allowed
-  ...
+    >>> url = get_bugtask_path('1.0')
+    >>> print(webservice.named_post(
+    ...     url, 'transitionToImportance', importance='Low',
+    ...     api_version='devel'))
+    HTTP/1.1 405 Method Not Allowed
+    ...
diff --git a/lib/lp/services/webservice/stories/root.txt b/lib/lp/services/webservice/stories/root.txt
index cc8ab14..3a2ebb4 100644
--- a/lib/lp/services/webservice/stories/root.txt
+++ b/lib/lp/services/webservice/stories/root.txt
@@ -6,8 +6,8 @@ special entry: the user account of the authenticated user. To avoid
 confusion when one program runs as different users, this is
 implemented as a redirect to that person's canonical URL.
 
-  >>> print(webservice.get('/people/+me'))
-  HTTP/1.1 303 See Other
-  ...
-  Location: http://.../~salgado
-  ...
+    >>> print(webservice.get('/people/+me'))
+    HTTP/1.1 303 See Other
+    ...
+    Location: http://.../~salgado
+    ...
diff --git a/lib/lp/services/webservice/stories/security.txt b/lib/lp/services/webservice/stories/security.txt
index 77c09ea..86ee057 100644
--- a/lib/lp/services/webservice/stories/security.txt
+++ b/lib/lp/services/webservice/stories/security.txt
@@ -12,38 +12,38 @@ A user without permission to see items in a collection will, of
 course, not see those items. The 'salgado' user can see all bugs in the
 Jokosher project.
 
-  >>> search = "/jokosher?ws.op=searchTasks"
-  >>> salgado_output = webservice.get(search).jsonBody()
-  >>> salgado_output['total_size']
-  3
-  >>> len(salgado_output['entries'])
-  3
+    >>> search = "/jokosher?ws.op=searchTasks"
+    >>> salgado_output = webservice.get(search).jsonBody()
+    >>> salgado_output['total_size']
+    3
+    >>> len(salgado_output['entries'])
+    3
 
 But the 'no-priv' user can't see bug number 14, which is private.
 
-  >>> print(user_webservice.get("/bugs/14"))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(user_webservice.get("/bugs/14"))
+    HTTP/1.1 404 Not Found
+    ...
 
-  >>> nopriv_output = user_webservice.get(search).jsonBody()
-  >>> nopriv_output['total_size']
-  2
-  >>> len(nopriv_output['entries'])
-  2
+    >>> nopriv_output = user_webservice.get(search).jsonBody()
+    >>> nopriv_output['total_size']
+    2
+    >>> len(nopriv_output['entries'])
+    2
 
 Things are a little different for a user who has permission to see
 private data, but is using an OAuth key that restricts the client to
 operating on public data.
 
-  >>> print(public_webservice.get("/bugs/14"))
-  HTTP/1.1 404 Not Found
-  ...
+    >>> print(public_webservice.get("/bugs/14"))
+    HTTP/1.1 404 Not Found
+    ...
 
-  >>> public_output = public_webservice.get(search).jsonBody()
-  >>> public_output['total_size']
-  3
-  >>> len(public_output['entries'])
-  2
+    >>> public_output = public_webservice.get(search).jsonBody()
+    >>> public_output['total_size']
+    3
+    >>> len(public_output['entries'])
+    2
 
 Although this behaviour is inconsistent, it doesn't leak any private
 information and implementing it consistently would be very difficult,
diff --git a/lib/lp/services/worlddata/doc/language.txt b/lib/lp/services/worlddata/doc/language.txt
index b5e5c0c..df5188b 100644
--- a/lib/lp/services/worlddata/doc/language.txt
+++ b/lib/lp/services/worlddata/doc/language.txt
@@ -2,151 +2,151 @@
 LanguageSet
 ===========
 
-  >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-  >>> language_set = getUtility(ILanguageSet)
+    >>> from lp.services.worlddata.interfaces.language import ILanguageSet
+    >>> language_set = getUtility(ILanguageSet)
 
 getLanguageByCode
 =================
 
 We can get hold of languages by their language code.
 
-  >>> language = language_set.getLanguageByCode('es')
-  >>> print(language.englishname)
-  Spanish
+    >>> language = language_set.getLanguageByCode('es')
+    >>> print(language.englishname)
+    Spanish
 
 Or if it doesn't exist, we return None.
 
-  >>> language_set.getLanguageByCode('not-existing') is None
-  True
+    >>> language_set.getLanguageByCode('not-existing') is None
+    True
 
 canonicalise_language_code
 ==========================
 
 We can convert language codes to standard form.
 
-  >>> print(language_set.canonicalise_language_code('pt'))
-  pt
-  >>> print(language_set.canonicalise_language_code('pt_BR'))
-  pt_BR
-  >>> print(language_set.canonicalise_language_code('pt-br'))
-  pt_BR
+    >>> print(language_set.canonicalise_language_code('pt'))
+    pt
+    >>> print(language_set.canonicalise_language_code('pt_BR'))
+    pt_BR
+    >>> print(language_set.canonicalise_language_code('pt-br'))
+    pt_BR
 
 createLanguage
 ==============
 
 This method creates a new language.
 
-  >>> foos = language_set.createLanguage('foos', 'Foo language')
-  >>> print(foos.code)
-  foos
-  >>> print(foos.englishname)
-  Foo language
+    >>> foos = language_set.createLanguage('foos', 'Foo language')
+    >>> print(foos.code)
+    foos
+    >>> print(foos.englishname)
+    Foo language
 
 search
 ======
 
 We are able to search languages with this method.
 
-  >>> languages = language_set.search('Spanish')
-  >>> for language in languages:
-  ...     print(language.code, language.englishname)
-  es Spanish
-  es_AR Spanish (Argentina)
-  es_BO Spanish (Bolivia)
-  es_CL Spanish (Chile)
-  es_CO Spanish (Colombia)
-  es_CR Spanish (Costa Rica)
-  es_DO Spanish (Dominican Republic)
-  es_EC Spanish (Ecuador)
-  es_SV Spanish (El Salvador)
-  es_GT Spanish (Guatemala)
-  es_HN Spanish (Honduras)
-  es_MX Spanish (Mexico)
-  es_NI Spanish (Nicaragua)
-  es_PA Spanish (Panama)
-  es_PY Spanish (Paraguay)
-  es_PE Spanish (Peru)
-  es_PR Spanish (Puerto Rico)
-  es_ES Spanish (Spain)
-  es_US Spanish (United States)
-  es_UY Spanish (Uruguay)
-  es_VE Spanish (Venezuela)
-  es@test Spanish test
+    >>> languages = language_set.search('Spanish')
+    >>> for language in languages:
+    ...     print(language.code, language.englishname)
+    es Spanish
+    es_AR Spanish (Argentina)
+    es_BO Spanish (Bolivia)
+    es_CL Spanish (Chile)
+    es_CO Spanish (Colombia)
+    es_CR Spanish (Costa Rica)
+    es_DO Spanish (Dominican Republic)
+    es_EC Spanish (Ecuador)
+    es_SV Spanish (El Salvador)
+    es_GT Spanish (Guatemala)
+    es_HN Spanish (Honduras)
+    es_MX Spanish (Mexico)
+    es_NI Spanish (Nicaragua)
+    es_PA Spanish (Panama)
+    es_PY Spanish (Paraguay)
+    es_PE Spanish (Peru)
+    es_PR Spanish (Puerto Rico)
+    es_ES Spanish (Spain)
+    es_US Spanish (United States)
+    es_UY Spanish (Uruguay)
+    es_VE Spanish (Venezuela)
+    es@test Spanish test
 
 It's case insensitive:
 
-  >>> languages = language_set.search('spanish')
-  >>> for language in languages:
-  ...     print(language.code, language.englishname)
-  es Spanish
-  es_AR Spanish (Argentina)
-  es_BO Spanish (Bolivia)
-  es_CL Spanish (Chile)
-  es_CO Spanish (Colombia)
-  es_CR Spanish (Costa Rica)
-  es_DO Spanish (Dominican Republic)
-  es_EC Spanish (Ecuador)
-  es_SV Spanish (El Salvador)
-  es_GT Spanish (Guatemala)
-  es_HN Spanish (Honduras)
-  es_MX Spanish (Mexico)
-  es_NI Spanish (Nicaragua)
-  es_PA Spanish (Panama)
-  es_PY Spanish (Paraguay)
-  es_PE Spanish (Peru)
-  es_PR Spanish (Puerto Rico)
-  es_ES Spanish (Spain)
-  es_US Spanish (United States)
-  es_UY Spanish (Uruguay)
-  es_VE Spanish (Venezuela)
-  es@test Spanish test
+    >>> languages = language_set.search('spanish')
+    >>> for language in languages:
+    ...     print(language.code, language.englishname)
+    es Spanish
+    es_AR Spanish (Argentina)
+    es_BO Spanish (Bolivia)
+    es_CL Spanish (Chile)
+    es_CO Spanish (Colombia)
+    es_CR Spanish (Costa Rica)
+    es_DO Spanish (Dominican Republic)
+    es_EC Spanish (Ecuador)
+    es_SV Spanish (El Salvador)
+    es_GT Spanish (Guatemala)
+    es_HN Spanish (Honduras)
+    es_MX Spanish (Mexico)
+    es_NI Spanish (Nicaragua)
+    es_PA Spanish (Panama)
+    es_PY Spanish (Paraguay)
+    es_PE Spanish (Peru)
+    es_PR Spanish (Puerto Rico)
+    es_ES Spanish (Spain)
+    es_US Spanish (United States)
+    es_UY Spanish (Uruguay)
+    es_VE Spanish (Venezuela)
+    es@test Spanish test
 
 And it even does substring searching!
 
-  >>> languages = language_set.search('panis')
-  >>> for language in languages:
-  ...     print(language.code, language.englishname)
-  es Spanish
-  es_AR Spanish (Argentina)
-  es_BO Spanish (Bolivia)
-  es_CL Spanish (Chile)
-  es_CO Spanish (Colombia)
-  es_CR Spanish (Costa Rica)
-  es_DO Spanish (Dominican Republic)
-  es_EC Spanish (Ecuador)
-  es_SV Spanish (El Salvador)
-  es_GT Spanish (Guatemala)
-  es_HN Spanish (Honduras)
-  es_MX Spanish (Mexico)
-  es_NI Spanish (Nicaragua)
-  es_PA Spanish (Panama)
-  es_PY Spanish (Paraguay)
-  es_PE Spanish (Peru)
-  es_PR Spanish (Puerto Rico)
-  es_ES Spanish (Spain)
-  es_US Spanish (United States)
-  es_UY Spanish (Uruguay)
-  es_VE Spanish (Venezuela)
-  es@test Spanish test
+    >>> languages = language_set.search('panis')
+    >>> for language in languages:
+    ...     print(language.code, language.englishname)
+    es Spanish
+    es_AR Spanish (Argentina)
+    es_BO Spanish (Bolivia)
+    es_CL Spanish (Chile)
+    es_CO Spanish (Colombia)
+    es_CR Spanish (Costa Rica)
+    es_DO Spanish (Dominican Republic)
+    es_EC Spanish (Ecuador)
+    es_SV Spanish (El Salvador)
+    es_GT Spanish (Guatemala)
+    es_HN Spanish (Honduras)
+    es_MX Spanish (Mexico)
+    es_NI Spanish (Nicaragua)
+    es_PA Spanish (Panama)
+    es_PY Spanish (Paraguay)
+    es_PE Spanish (Peru)
+    es_PR Spanish (Puerto Rico)
+    es_ES Spanish (Spain)
+    es_US Spanish (United States)
+    es_UY Spanish (Uruguay)
+    es_VE Spanish (Venezuela)
+    es@test Spanish test
 
 We escape special characters like '%', which is an SQL wildcard
 matching any string:
 
-  >>> languages = language_set.search('%')
-  >>> for language in languages:
-  ...     print(language.code, language.englishname)
+    >>> languages = language_set.search('%')
+    >>> for language in languages:
+    ...     print(language.code, language.englishname)
 
 Or '_', which means any character match, but we only get strings
 that contain the 'e_' substring:
 
-  >>> languages = language_set.search('e_')
-  >>> for language in languages:
-  ...     print(language.code, language.englishname)
-  de_AT German (Austria)
-  de_BE German (Belgium)
-  de_DE German (Germany)
-  de_LU German (Luxembourg)
-  de_CH German (Switzerland)
+    >>> languages = language_set.search('e_')
+    >>> for language in languages:
+    ...     print(language.code, language.englishname)
+    de_AT German (Austria)
+    de_BE German (Belgium)
+    de_DE German (Germany)
+    de_LU German (Luxembourg)
+    de_CH German (Switzerland)
 
 
 ========
@@ -210,9 +210,9 @@ Although we use underscores to separate language and country codes to
 represent, for instance pt_BR, when used on web pages, it should use
 instead a dash char. This method does it automatically:
 
-  >>> pt_BR = language_set.getLanguageByCode('pt_BR')
-  >>> print(pt_BR.dashedcode)
-  pt-BR
+    >>> pt_BR = language_set.getLanguageByCode('pt_BR')
+    >>> print(pt_BR.dashedcode)
+    pt-BR
 
 
 translators
@@ -221,48 +221,52 @@ translators
 Property `translators` contains the list of `Person`s who are considered
 translators for this language.
 
-  >>> sr = language_set.getLanguageByCode('sr')
-  >>> list(sr.translators)
-  []
+    >>> sr = language_set.getLanguageByCode('sr')
+    >>> list(sr.translators)
+    []
 
 To be considered a translator, they must have done some translations and
 have the language among their preferred languages.
 
-  >>> translator_10 = factory.makePerson(name=u'serbian-translator-karma-10')
-  >>> translator_10.addLanguage(sr)
-  >>> translator_20 = factory.makePerson(name=u'serbian-translator-karma-20')
-  >>> translator_20.addLanguage(sr)
-  >>> translator_30 = factory.makePerson(name=u'serbian-translator-karma-30')
-  >>> translator_30.addLanguage(sr)
-  >>> translator_40 = factory.makePerson(name=u'serbian-translator-karma-40')
-  >>> translator_40.addLanguage(sr)
-
-  # We need to fake some Karma.
-  >>> from lp.registry.model.karma import KarmaCategory, KarmaCache
-  >>> from lp.testing.dbuser import switch_dbuser
-
-  >>> switch_dbuser('karma')
-  >>> translations_category = KarmaCategory.selectOne(
-  ...     KarmaCategory.name=='translations')
-  >>> karma = KarmaCache(person=translator_30,
-  ...                    category=translations_category,
-  ...                    karmavalue=30)
-  >>> karma = KarmaCache(person=translator_10,
-  ...                    category=translations_category,
-  ...                    karmavalue=10)
-  >>> karma = KarmaCache(person=translator_20,
-  ...                    category=translations_category,
-  ...                    karmavalue=20)
-  >>> karma = KarmaCache(person=translator_40,
-  ...                    category=translations_category,
-  ...                    karmavalue=40)
-  >>> switch_dbuser('launchpad')
-  >>> for translator in sr.translators:
-  ...   print(translator.name)
-  serbian-translator-karma-40
-  serbian-translator-karma-30
-  serbian-translator-karma-20
-  serbian-translator-karma-10
+    >>> translator_10 = factory.makePerson(
+    ...     name=u'serbian-translator-karma-10')
+    >>> translator_10.addLanguage(sr)
+    >>> translator_20 = factory.makePerson(
+    ...     name=u'serbian-translator-karma-20')
+    >>> translator_20.addLanguage(sr)
+    >>> translator_30 = factory.makePerson(
+    ...     name=u'serbian-translator-karma-30')
+    >>> translator_30.addLanguage(sr)
+    >>> translator_40 = factory.makePerson(
+    ...     name=u'serbian-translator-karma-40')
+    >>> translator_40.addLanguage(sr)
+
+    # We need to fake some Karma.
+    >>> from lp.registry.model.karma import KarmaCategory, KarmaCache
+    >>> from lp.testing.dbuser import switch_dbuser
+
+    >>> switch_dbuser('karma')
+    >>> translations_category = KarmaCategory.selectOne(
+    ...     KarmaCategory.name=='translations')
+    >>> karma = KarmaCache(person=translator_30,
+    ...                    category=translations_category,
+    ...                    karmavalue=30)
+    >>> karma = KarmaCache(person=translator_10,
+    ...                    category=translations_category,
+    ...                    karmavalue=10)
+    >>> karma = KarmaCache(person=translator_20,
+    ...                    category=translations_category,
+    ...                    karmavalue=20)
+    >>> karma = KarmaCache(person=translator_40,
+    ...                    category=translations_category,
+    ...                    karmavalue=40)
+    >>> switch_dbuser('launchpad')
+    >>> for translator in sr.translators:
+    ...   print(translator.name)
+    serbian-translator-karma-40
+    serbian-translator-karma-30
+    serbian-translator-karma-20
+    serbian-translator-karma-10
 
 
 =========
@@ -272,93 +276,93 @@ Countries
 Property holding a list of countries a language is spoken in, and allowing
 reading and setting them.
 
-  >>> es = language_set.getLanguageByCode('es')
-  >>> for country in es.countries:
-  ...     print(country.name)
-  Argentina
-  Bolivia
-  Chile
-  Colombia
-  Costa Rica
-  Dominican Republic
-  Ecuador
-  El Salvador
-  Guatemala
-  Honduras
-  Mexico
-  Nicaragua
-  Panama
-  Paraguay
-  Peru
-  Puerto Rico
-  Spain
-  United States
-  Uruguay
-  Venezuela
+    >>> es = language_set.getLanguageByCode('es')
+    >>> for country in es.countries:
+    ...     print(country.name)
+    Argentina
+    Bolivia
+    Chile
+    Colombia
+    Costa Rica
+    Dominican Republic
+    Ecuador
+    El Salvador
+    Guatemala
+    Honduras
+    Mexico
+    Nicaragua
+    Panama
+    Paraguay
+    Peru
+    Puerto Rico
+    Spain
+    United States
+    Uruguay
+    Venezuela
 
 We can add countries using `ILanguage.addCountry` method.
 
-  >>> from lp.services.worlddata.interfaces.country import ICountrySet
-  >>> country_set = getUtility(ICountrySet)
-  >>> germany = country_set['DE']
-  >>> es.addCountry(germany)
-  >>> for country in es.countries:
-  ...     print(country.name)
-  Argentina
-  Bolivia
-  Chile
-  Colombia
-  Costa Rica
-  Dominican Republic
-  Ecuador
-  El Salvador
-  Germany
-  Guatemala
-  Honduras
-  Mexico
-  Nicaragua
-  Panama
-  Paraguay
-  Peru
-  Puerto Rico
-  Spain
-  United States
-  Uruguay
-  Venezuela
+    >>> from lp.services.worlddata.interfaces.country import ICountrySet
+    >>> country_set = getUtility(ICountrySet)
+    >>> germany = country_set['DE']
+    >>> es.addCountry(germany)
+    >>> for country in es.countries:
+    ...     print(country.name)
+    Argentina
+    Bolivia
+    Chile
+    Colombia
+    Costa Rica
+    Dominican Republic
+    Ecuador
+    El Salvador
+    Germany
+    Guatemala
+    Honduras
+    Mexico
+    Nicaragua
+    Panama
+    Paraguay
+    Peru
+    Puerto Rico
+    Spain
+    United States
+    Uruguay
+    Venezuela
 
 Or, we can remove countries using `ILanguage.removeCountry` method.
 
-  >>> argentina = country_set['AR']
-  >>> es.removeCountry(argentina)
-  >>> for country in es.countries:
-  ...     print(country.name)
-  Bolivia
-  Chile
-  Colombia
-  Costa Rica
-  Dominican Republic
-  Ecuador
-  El Salvador
-  Germany
-  Guatemala
-  Honduras
-  Mexico
-  Nicaragua
-  Panama
-  Paraguay
-  Peru
-  Puerto Rico
-  Spain
-  United States
-  Uruguay
-  Venezuela
+    >>> argentina = country_set['AR']
+    >>> es.removeCountry(argentina)
+    >>> for country in es.countries:
+    ...     print(country.name)
+    Bolivia
+    Chile
+    Colombia
+    Costa Rica
+    Dominican Republic
+    Ecuador
+    El Salvador
+    Germany
+    Guatemala
+    Honduras
+    Mexico
+    Nicaragua
+    Panama
+    Paraguay
+    Peru
+    Puerto Rico
+    Spain
+    United States
+    Uruguay
+    Venezuela
 
 We can also assign a complete set of languages directly to `countries`,
 but we need to log in as a translations administrator first.
 
-  >>> login('carlos@xxxxxxxxxxxxx')
-  >>> es.countries = set([argentina, germany])
-  >>> for country in es.countries:
-  ...     print(country.name)
-  Argentina
-  Germany
+    >>> login('carlos@xxxxxxxxxxxxx')
+    >>> es.countries = set([argentina, germany])
+    >>> for country in es.countries:
+    ...     print(country.name)
+    Argentina
+    Germany
diff --git a/lib/lp/soyuz/browser/tests/binarypackagerelease-views.txt b/lib/lp/soyuz/browser/tests/binarypackagerelease-views.txt
index 2b30901..9b8e4de 100644
--- a/lib/lp/soyuz/browser/tests/binarypackagerelease-views.txt
+++ b/lib/lp/soyuz/browser/tests/binarypackagerelease-views.txt
@@ -1,37 +1,39 @@
 BinaryPackageRelease Pages
 ==========================
 
-  >>> from zope.component import getMultiAdapter
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
+    >>> from zope.component import getMultiAdapter
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
 
-  >>> pmount_bin = BinaryPackageRelease.get(15)
-  >>> print(pmount_bin.name)
-  pmount
-  >>> print(pmount_bin.version)
-  0.1-1
+    >>> pmount_bin = BinaryPackageRelease.get(15)
+    >>> print(pmount_bin.name)
+    pmount
+    >>> print(pmount_bin.version)
+    0.1-1
 
 Get a "mock" request:
-  >>> mock_form = {}
-  >>> request = LaunchpadTestRequest(form=mock_form)
+    >>> mock_form = {}
+    >>> request = LaunchpadTestRequest(form=mock_form)
 
 Let's instantiate the view for +portlet-details:
 
-  >>> pmount_view = getMultiAdapter(
-  ...     (pmount_bin, request), name="+portlet-details")
+    >>> pmount_view = getMultiAdapter(
+    ...     (pmount_bin, request), name="+portlet-details")
 
 Main functionality of this class is to provide abstracted model of the
 stored package relationships. They are provided as a
 IPackageRelationshipSet. (see package-relationship.txt).
 
 
-  >>> pmount_deps = pmount_view.depends()
+    >>> pmount_deps = pmount_view.depends()
 
-  >>> from lp.soyuz.interfaces.packagerelationship import IPackageRelationshipSet
-  >>> from lp.testing import verifyObject
+    >>> from lp.soyuz.interfaces.packagerelationship import (
+    ...     IPackageRelationshipSet,
+    ...     )
+    >>> from lp.testing import verifyObject
 
-  >>> verifyObject(IPackageRelationshipSet, pmount_deps)
-  True
+    >>> verifyObject(IPackageRelationshipSet, pmount_deps)
+    True
 
 Let's check the rendering parameters for a specific dep:
 
@@ -46,10 +48,10 @@ package relationship isn't present in the DistroArchSeries in
 question. In this case 'url' will be None, which indicates no link
 should be rendered for this dependency.
 
-  >>> for dep in pmount_deps:
-  ...    print(pretty((dep.name, dep.operator, dep.version, dep.url)))
-  ('at', '>=', '3.14156', 'http://launchpad.test/ubuntu/hoary/i386/at')
-  ('linux-2.6.12', None, '', 'http://launchpad.test/ubuntu/hoary/i386/linux-2.6.12')
-  ('tramp-package', None, '', None)
+    >>> for dep in pmount_deps:
+    ...    print(pretty((dep.name, dep.operator, dep.version, dep.url)))
+    ('at', '>=', '3.14156', 'http://launchpad.test/ubuntu/hoary/i386/at')
+    ('linux-2.6.12', None, '', 'http://launchpad.test/ubuntu/hoary/i386/linux-2.6.12')
+    ('tramp-package', None, '', None)
 
 Other relationship groups use the same mechanism.
diff --git a/lib/lp/soyuz/browser/tests/distroseriesqueue-views.txt b/lib/lp/soyuz/browser/tests/distroseriesqueue-views.txt
index b1c3616..0816c39 100644
--- a/lib/lp/soyuz/browser/tests/distroseriesqueue-views.txt
+++ b/lib/lp/soyuz/browser/tests/distroseriesqueue-views.txt
@@ -6,121 +6,121 @@ for IDistroSeries context (IDistroSeriesView)
 
 Let's instantiate the view for +queue for anonymous access:
 
-  >>> from zope.component import queryMultiAdapter
-  >>> from lp.services.librarian.model import LibraryFileAlias
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
-  >>> fake_chroot = LibraryFileAlias.get(1)
-
-  >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
-  >>> breezy_autotest = ubuntu['breezy-autotest']
-  >>> breezy_autotest_i386 = breezy_autotest['i386']
-  >>> unused = breezy_autotest_i386.addOrUpdateChroot(fake_chroot)
-
-  >>> request = LaunchpadTestRequest()
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
+    >>> from zope.component import queryMultiAdapter
+    >>> from lp.services.librarian.model import LibraryFileAlias
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> fake_chroot = LibraryFileAlias.get(1)
+
+    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
+    >>> breezy_autotest = ubuntu['breezy-autotest']
+    >>> breezy_autotest_i386 = breezy_autotest['i386']
+    >>> unused = breezy_autotest_i386.addOrUpdateChroot(fake_chroot)
+
+    >>> request = LaunchpadTestRequest()
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
 
 View parameters need to be set properly before start:
 
-  >>> queue_view.setupQueueList()
+    >>> queue_view.setupQueueList()
 
 After setup we have a 'batched' list:
 
-  >>> from lp.services.webapp.interfaces import IBatchNavigator
-  >>> from lp.testing import verifyObject
-  >>> verifyObject(IBatchNavigator, queue_view.batchnav)
-  True
+    >>> from lp.services.webapp.interfaces import IBatchNavigator
+    >>> from lp.testing import verifyObject
+    >>> verifyObject(IBatchNavigator, queue_view.batchnav)
+    True
 
-  >>> len(queue_view.batchnav.currentBatch())
-  6
+    >>> len(queue_view.batchnav.currentBatch())
+    6
 
 The local state (PackageUploadStatus, dbschema)
 
-  >>> queue_view.state.name
-  'NEW'
+    >>> queue_view.state.name
+    'NEW'
 
 A list of available actions in this queue:
 
-  >>> queue_view.availableActions()
-  []
+    >>> queue_view.availableActions()
+    []
 
 Let's instantiate the view for a specific queue:
 
-  >>> from lp.soyuz.enums import PackageUploadStatus
-  >>> request = LaunchpadTestRequest(
-  ...     form={'queue_state': PackageUploadStatus.DONE.value})
-  >>> warty = ubuntu['warty']
-  >>> queue_view = queryMultiAdapter((warty, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  >>> queue_view.state.name
-  'DONE'
-  >>> len(queue_view.batchnav.currentBatch())
-  1
+    >>> from lp.soyuz.enums import PackageUploadStatus
+    >>> request = LaunchpadTestRequest(
+    ...     form={'queue_state': PackageUploadStatus.DONE.value})
+    >>> warty = ubuntu['warty']
+    >>> queue_view = queryMultiAdapter((warty, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    >>> queue_view.state.name
+    'DONE'
+    >>> len(queue_view.batchnav.currentBatch())
+    1
 
 Unexpected values for queue_state results in a proper error, anything
 that can't be can't fit as an integer is automatically assume as the
 default value (NEW queue).
 
-  >>> request = LaunchpadTestRequest(
-  ...     form={'queue_state': 'foo'})
-  >>> warty = ubuntu['warty']
-  >>> queue_view = queryMultiAdapter((warty, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  >>> queue_view.state.name
-  'NEW'
-  >>> len(queue_view.batchnav.currentBatch())
-  0
+    >>> request = LaunchpadTestRequest(
+    ...     form={'queue_state': 'foo'})
+    >>> warty = ubuntu['warty']
+    >>> queue_view = queryMultiAdapter((warty, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    >>> queue_view.state.name
+    'NEW'
+    >>> len(queue_view.batchnav.currentBatch())
+    0
 
 If a invalid integer is posted it raises.
 
-  >>> request = LaunchpadTestRequest(
-  ...     form={'queue_state': '10'})
-  >>> warty = ubuntu['warty']
-  >>> queue_view = queryMultiAdapter((warty, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  lp.app.errors.UnexpectedFormData: No suitable status found for value "10"
+    >>> request = LaunchpadTestRequest(
+    ...     form={'queue_state': '10'})
+    >>> warty = ubuntu['warty']
+    >>> queue_view = queryMultiAdapter((warty, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    lp.app.errors.UnexpectedFormData: No suitable status found for value "10"
 
 Anonymous users also have access to all queues, including UNAPPROVED
 but they are not allowed to perform any action.
 
-  >>> request = LaunchpadTestRequest(
-  ...     form={'queue_state': PackageUploadStatus.UNAPPROVED.value})
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  >>> queue_view.state.name
-  'UNAPPROVED'
-  >>> len(queue_view.batchnav.currentBatch())
-  5
+    >>> request = LaunchpadTestRequest(
+    ...     form={'queue_state': PackageUploadStatus.UNAPPROVED.value})
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    >>> queue_view.state.name
+    'UNAPPROVED'
+    >>> len(queue_view.batchnav.currentBatch())
+    5
 
-  >>> queue_view.availableActions()
-  []
+    >>> queue_view.availableActions()
+    []
 
 Now, let's instantiate the view for +queue as a privileged user:
 
-  >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login('foo.bar@xxxxxxxxxxxxx')
 
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  >>> queue_view.availableActions()
-  ['Accept', 'Reject']
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    >>> queue_view.availableActions()
+    ['Accept', 'Reject']
 
 Attempt to view and act on UNAPPROVED queue works for administrators.
 
-  >>> request = LaunchpadTestRequest(
-  ...     form={'queue_state': PackageUploadStatus.UNAPPROVED.value})
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  >>> queue_view.state.name
-  'UNAPPROVED'
-  >>> queue_view.availableActions()
-  ['Accept', 'Reject']
+    >>> request = LaunchpadTestRequest(
+    ...     form={'queue_state': PackageUploadStatus.UNAPPROVED.value})
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    >>> queue_view.state.name
+    'UNAPPROVED'
+    >>> queue_view.availableActions()
+    ['Accept', 'Reject']
 
 Action on presented queue are controlled and performed by the
 'performAction' method, which return a HTML-formatted report text
@@ -130,92 +130,92 @@ It accepts the 'Accept'/'Reject' and 'QUEUE_ID' arguments via POST.
 
 Accepting an item from NEW queue.
 
-  >>> from lp.soyuz.interfaces.queue import IPackageUploadSet
-  >>> getUtility(IPackageUploadSet).get(1).status.name
-  'NEW'
-  >>> getUtility(IPackageUploadSet).get(3).status.name
-  'NEW'
+    >>> from lp.soyuz.interfaces.queue import IPackageUploadSet
+    >>> getUtility(IPackageUploadSet).get(1).status.name
+    'NEW'
+    >>> getUtility(IPackageUploadSet).get(3).status.name
+    'NEW'
 
-  >>> request = LaunchpadTestRequest(
-  ...     form={'queue_state': PackageUploadStatus.NEW.value,
-  ...           'Accept': 'Accept',
-  ...           'QUEUE_ID': ['1', '3']})
-  >>> request.method = 'POST'
+    >>> request = LaunchpadTestRequest(
+    ...     form={'queue_state': PackageUploadStatus.NEW.value,
+    ...           'Accept': 'Accept',
+    ...           'QUEUE_ID': ['1', '3']})
+    >>> request.method = 'POST'
 
 Add fake librarian files so that email notifications work:
 
-  >>> from lp.archiveuploader.tests import (
-  ...     insertFakeChangesFileForAllPackageUploads)
-  >>> insertFakeChangesFileForAllPackageUploads()
+    >>> from lp.archiveuploader.tests import (
+    ...     insertFakeChangesFileForAllPackageUploads)
+    >>> insertFakeChangesFileForAllPackageUploads()
 
 Anonymous attempts to accept queue items are ignored and an error
 message is presented.
 
-  >>> login(ANONYMOUS)
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  >>> queue_view.performQueueAction()
-  >>> print(queue_view.error)
-  You do not have permission to act on queue items.
+    >>> login(ANONYMOUS)
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    >>> queue_view.performQueueAction()
+    >>> print(queue_view.error)
+    You do not have permission to act on queue items.
 
-  >>> getUtility(IPackageUploadSet).get(1).status.name
-  'NEW'
-  >>> getUtility(IPackageUploadSet).get(3).status.name
-  'NEW'
+    >>> getUtility(IPackageUploadSet).get(1).status.name
+    'NEW'
+    >>> getUtility(IPackageUploadSet).get(3).status.name
+    'NEW'
 
 Privileged user can accept queue items.
 
-  >>> login('foo.bar@xxxxxxxxxxxxx')
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
-  >>> queue_view.setupQueueList()
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
+    >>> queue_view.setupQueueList()
 
-  >>> queue_view.performQueueAction()
+    >>> queue_view.performQueueAction()
 
-  >>> getUtility(IPackageUploadSet).get(1).status.name
-  'ACCEPTED'
-  >>> getUtility(IPackageUploadSet).get(3).status.name
-  'DONE'
+    >>> getUtility(IPackageUploadSet).get(1).status.name
+    'ACCEPTED'
+    >>> getUtility(IPackageUploadSet).get(3).status.name
+    'DONE'
 
 Rejection an item from NEW queue:
 
-  >>> target = getUtility(IPackageUploadSet).get(2)
-  >>> target.status.name
-  'NEW'
+    >>> target = getUtility(IPackageUploadSet).get(2)
+    >>> target.status.name
+    'NEW'
 
-  >>> request = LaunchpadTestRequest(
-  ...     form={'queue_state': PackageUploadStatus.NEW.value,
-  ...           'rejection_comment': 'Foo',
-  ...           'Reject': 'Reject',
-  ...           'QUEUE_ID': '2'})
-  >>> request.method = 'POST'
+    >>> request = LaunchpadTestRequest(
+    ...     form={'queue_state': PackageUploadStatus.NEW.value,
+    ...           'rejection_comment': 'Foo',
+    ...           'Reject': 'Reject',
+    ...           'QUEUE_ID': '2'})
+    >>> request.method = 'POST'
 
 Anonymous attempts to reject queue items are ignored and an error
 message is presented.
 
-  >>> login(ANONYMOUS)
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
-  >>> queue_view.setupQueueList()
-  >>> queue_view.performQueueAction()
-  >>> print(queue_view.error)
-  You do not have permission to act on queue items.
+    >>> login(ANONYMOUS)
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
+    >>> queue_view.setupQueueList()
+    >>> queue_view.performQueueAction()
+    >>> print(queue_view.error)
+    You do not have permission to act on queue items.
 
-  >>> target.status.name
-  'NEW'
+    >>> target.status.name
+    'NEW'
 
 Privileged user can reject queue items.
 
-  >>> login('foo.bar@xxxxxxxxxxxxx')
-  >>> queue_view = queryMultiAdapter(
-  ...     (breezy_autotest, request), name="+queue")
-  >>> queue_view.setupQueueList()
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> queue_view = queryMultiAdapter(
+    ...     (breezy_autotest, request), name="+queue")
+    >>> queue_view.setupQueueList()
 
-  >>> queue_view.performQueueAction()
+    >>> queue_view.performQueueAction()
 
-  >>> target.status.name
-  'REJECTED'
+    >>> target.status.name
+    'REJECTED'
 
 
 Calculation of "new" binaries
@@ -300,5 +300,5 @@ is_new() method requires to work.
 
 We created librarian files that need cleaning up before leaving the test.
 
-  >>> from lp.testing.layers import LibrarianLayer
-  >>> LibrarianLayer.librarian_fixture.clear()
+    >>> from lp.testing.layers import LibrarianLayer
+    >>> LibrarianLayer.librarian_fixture.clear()
diff --git a/lib/lp/soyuz/browser/tests/sourcepackage-views.txt b/lib/lp/soyuz/browser/tests/sourcepackage-views.txt
index 63615d5..9697d91 100644
--- a/lib/lp/soyuz/browser/tests/sourcepackage-views.txt
+++ b/lib/lp/soyuz/browser/tests/sourcepackage-views.txt
@@ -1,24 +1,24 @@
 SourcePackage View Classes
 ==========================
 
-  >>> from zope.component import queryMultiAdapter
-  >>> from lp.services.webapp.servers import LaunchpadTestRequest
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> from zope.component import queryMultiAdapter
+    >>> from lp.services.webapp.servers import LaunchpadTestRequest
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
 
 Empty request.
 
-  >>> mock_form = {}
-  >>> request = LaunchpadTestRequest(form=mock_form)
+    >>> mock_form = {}
+    >>> request = LaunchpadTestRequest(form=mock_form)
 
 Retrieve an known Sourcepackage object:
 
-  >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
-  >>> hoary = ubuntu['hoary']
-  >>> pmount = hoary.getSourcePackage('pmount')
+    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
+    >>> hoary = ubuntu['hoary']
+    >>> pmount = hoary.getSourcePackage('pmount')
 
 Retrieve its respective View class:
 
-  >>> pmount_view = queryMultiAdapter((pmount, request), name="+index")
+    >>> pmount_view = queryMultiAdapter((pmount, request), name="+index")
 
 Check the consistency of a handy structure containing the organized
 published history of a sourcepackage. It should contain a list of
@@ -37,15 +37,16 @@ as 'packages', as:
 
 Each pocket should only contain packages marked as PUBLISHED.
 
-  >>> for pub in pmount_view.published_by_pocket():
-  ...     pkg_versions = [
-  ...         (p['spr'].version, p['component_name']) for p in pub['packages']]
-  ...     print(pub['pocketdetails'].title, pretty(sorted(pkg_versions)))
-  Release [('0.1-2', 'main')]
-  Security []
-  Updates []
-  Proposed []
-  Backports []
+    >>> for pub in pmount_view.published_by_pocket():
+    ...     pkg_versions = [
+    ...         (p['spr'].version, p['component_name'])
+    ...         for p in pub['packages']]
+    ...     print(pub['pocketdetails'].title, pretty(sorted(pkg_versions)))
+    Release [('0.1-2', 'main')]
+    Security []
+    Updates []
+    Proposed []
+    Backports []
 
 
 Check the consistence of the binaries dictionary, it should contains a
@@ -53,62 +54,64 @@ binarypackagename and the architecture where it was built.
 
 Let's retrieve a new view with useful dependency data:
 
-  >>> warty = ubuntu['warty']
-  >>> firefox = warty.getSourcePackage('mozilla-firefox')
-  >>> firefox_view = queryMultiAdapter((firefox, request), name="+index")
+    >>> warty = ubuntu['warty']
+    >>> firefox = warty.getSourcePackage('mozilla-firefox')
+    >>> firefox_view = queryMultiAdapter((firefox, request), name="+index")
 
 XXX cprov 20060210: this method is very confusing because the
 architecturespecific attribute is hidden, i.e, this binary is
 architecture independent and we don't know at this point, that's why we
 have only on binary.
 
-  >>> for bin_name, archs in sorted(firefox_view.binaries().items()):
-  ...    print(bin_name, pretty(archs))
-  mozilla-firefox ['hppa', 'i386']
-  mozilla-firefox-data ['hppa', 'i386']
+    >>> for bin_name, archs in sorted(firefox_view.binaries().items()):
+    ...    print(bin_name, pretty(archs))
+    mozilla-firefox ['hppa', 'i386']
+    mozilla-firefox-data ['hppa', 'i386']
 
 Check the formatted dependency lines provided by the view class, they
 return a IPackageRelationshipSet object (see package-relationship.txt).
 
-  >>> firefox_parsed_depends = firefox_view.builddepends
+    >>> firefox_parsed_depends = firefox_view.builddepends
 
-  >>> from lp.soyuz.interfaces.packagerelationship import IPackageRelationshipSet
-  >>> from lp.testing import verifyObject
-  >>> verifyObject(IPackageRelationshipSet, firefox_parsed_depends)
-  True
+    >>> from lp.soyuz.interfaces.packagerelationship import (
+    ...     IPackageRelationshipSet,
+    ...     )
+    >>> from lp.testing import verifyObject
+    >>> verifyObject(IPackageRelationshipSet, firefox_parsed_depends)
+    True
 
-  >>> for dep in firefox_parsed_depends:
-  ...    print(pretty((dep.name, dep.operator, dep.version, dep.url)))
-  ('gcc-3.4', '>=', '3.4.1-4sarge1', None)
-  ('gcc-3.4', '<<', '3.4.2', None)
-  ('gcc-3.4-base', None, '', None)
-  ('libc6', '>=', '2.3.2.ds1-4', None)
-  ('libstdc++6-dev', '>=', '3.4.1-4sarge1', None)
-  ('pmount', None, '', 'http://launchpad.test/ubuntu/warty/+package/pmount')
+    >>> for dep in firefox_parsed_depends:
+    ...    print(pretty((dep.name, dep.operator, dep.version, dep.url)))
+    ('gcc-3.4', '>=', '3.4.1-4sarge1', None)
+    ('gcc-3.4', '<<', '3.4.2', None)
+    ('gcc-3.4-base', None, '', None)
+    ('libc6', '>=', '2.3.2.ds1-4', None)
+    ('libstdc++6-dev', '>=', '3.4.1-4sarge1', None)
+    ('pmount', None, '', 'http://launchpad.test/ubuntu/warty/+package/pmount')
 
 
-  >>> firefox_parsed_dependsindep = firefox_view.builddependsindep
+    >>> firefox_parsed_dependsindep = firefox_view.builddependsindep
 
-  >>> verifyObject(IPackageRelationshipSet, firefox_parsed_dependsindep)
-  True
+    >>> verifyObject(IPackageRelationshipSet, firefox_parsed_dependsindep)
+    True
 
-  >>> for dep in firefox_parsed_dependsindep:
-  ...    print(pretty((dep.name, dep.operator, dep.version, dep.url)))
-  ('bacula-common', '=', '1.34.6-2', None)
-  ('bacula-director-common', '=', '1.34.6-2', None)
-  ('pmount', None, '', 'http://launchpad.test/ubuntu/warty/+package/pmount')
-  ('postgresql-client', '>=', '7.4', None)
+    >>> for dep in firefox_parsed_dependsindep:
+    ...    print(pretty((dep.name, dep.operator, dep.version, dep.url)))
+    ('bacula-common', '=', '1.34.6-2', None)
+    ('bacula-director-common', '=', '1.34.6-2', None)
+    ('pmount', None, '', 'http://launchpad.test/ubuntu/warty/+package/pmount')
+    ('postgresql-client', '>=', '7.4', None)
 
 Ensure we have fixed bug 31039, by properly escape the
 sourcepackagename before passing to regexp.
 
-  >>> libc = ubuntu.getSourcePackage('libstdc++').getVersion('b8p')
-  >>> libc_view = queryMultiAdapter((libc, request), name="+changelog")
-  >>> print(libc_view.changelog_entry)
-  libstdc++ (9.9-1) hoary; urgency=high
-  <BLANKLINE>
-   * Placeholder
-  <BLANKLINE>
-   -- Sample Person &lt;email address hidden&gt; Tue, 10 Feb 2006 10:10:08 +0300
-  <BLANKLINE>
-  <BLANKLINE>
+    >>> libc = ubuntu.getSourcePackage('libstdc++').getVersion('b8p')
+    >>> libc_view = queryMultiAdapter((libc, request), name="+changelog")
+    >>> print(libc_view.changelog_entry)
+    libstdc++ (9.9-1) hoary; urgency=high
+    <BLANKLINE>
+     * Placeholder
+    <BLANKLINE>
+     -- Sample Person &lt;email address hidden&gt; Tue, 10 Feb 2006 10:10:08 +0300
+    <BLANKLINE>
+    <BLANKLINE>
diff --git a/lib/lp/soyuz/doc/binarypackagerelease.txt b/lib/lp/soyuz/doc/binarypackagerelease.txt
index cbe81eb..f97a86f 100644
--- a/lib/lp/soyuz/doc/binarypackagerelease.txt
+++ b/lib/lp/soyuz/doc/binarypackagerelease.txt
@@ -4,30 +4,30 @@ BinaryPackageRelease
 BinaryPackageRelease stores unique versions of binarypackagenames
 across build records.
 
-   >>> from lp.testing import verifyObject
-   >>> from lp.soyuz.interfaces.binarypackagerelease import (
-   ...     IBinaryPackageRelease,
-   ...     )
-   >>> from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
+    >>> from lp.testing import verifyObject
+    >>> from lp.soyuz.interfaces.binarypackagerelease import (
+    ...     IBinaryPackageRelease,
+    ...     )
+    >>> from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
 
-   >>> firefox_bin_release = BinaryPackageRelease.get(12)
-   >>> verifyObject(IBinaryPackageRelease, firefox_bin_release)
-   True
+    >>> firefox_bin_release = BinaryPackageRelease.get(12)
+    >>> verifyObject(IBinaryPackageRelease, firefox_bin_release)
+    True
 
 Useful properties:
 
-   >>> print(firefox_bin_release.name)
-   mozilla-firefox
-   >>> print(firefox_bin_release.version)
-   0.9
+    >>> print(firefox_bin_release.name)
+    mozilla-firefox
+    >>> print(firefox_bin_release.version)
+    0.9
 
-   >>> from lp.registry.interfaces.distroseries import IDistroSeriesSet
-   >>> warty = getUtility(IDistroSeriesSet).get(1)
-   >>> print(warty.name)
-   warty
-   >>> hoary = getUtility(IDistroSeriesSet).get(3)
-   >>> print(hoary.name)
-   hoary
+    >>> from lp.registry.interfaces.distroseries import IDistroSeriesSet
+    >>> warty = getUtility(IDistroSeriesSet).get(1)
+    >>> print(warty.name)
+    warty
+    >>> hoary = getUtility(IDistroSeriesSet).get(3)
+    >>> print(hoary.name)
+    hoary
 
 The IBinaryPackageNameSet.getNotNewByNames() returns all the
 BinaryPackageName records for BinaryPackageReleases that are published
@@ -77,50 +77,50 @@ in ftp-master/queue tool and other scripts.
 
 Display the current firefox component and section:
 
-   >>> print(firefox_bin_release.component.name)
-   main
-   >>> print(firefox_bin_release.section.name)
-   base
+    >>> print(firefox_bin_release.component.name)
+    main
+    >>> print(firefox_bin_release.section.name)
+    base
 
 Fetch brand new component, section and priority:
 
-   >>> from lp.soyuz.enums import PackagePublishingPriority
-   >>> from lp.soyuz.interfaces.component import IComponentSet
-   >>> from lp.soyuz.interfaces.section import ISectionSet
-   >>> new_comp = getUtility(IComponentSet)['universe']
-   >>> new_sec = getUtility(ISectionSet)['mail']
-   >>> new_priority = PackagePublishingPriority.IMPORTANT
+    >>> from lp.soyuz.enums import PackagePublishingPriority
+    >>> from lp.soyuz.interfaces.component import IComponentSet
+    >>> from lp.soyuz.interfaces.section import ISectionSet
+    >>> new_comp = getUtility(IComponentSet)['universe']
+    >>> new_sec = getUtility(ISectionSet)['mail']
+    >>> new_priority = PackagePublishingPriority.IMPORTANT
 
 Override the current firefox with new component/section/priority:
 
-   >>> firefox_bin_release.override(component=new_comp, section=new_sec,
-   ...              priority=new_priority)
+    >>> firefox_bin_release.override(component=new_comp, section=new_sec,
+    ...              priority=new_priority)
 
 Check if it got overridden correctly:
 
-   >>> print(firefox_bin_release.component.name)
-   universe
-   >>> print(firefox_bin_release.section.name)
-   mail
-   >>> print(firefox_bin_release.priority.name)
-   IMPORTANT
+    >>> print(firefox_bin_release.component.name)
+    universe
+    >>> print(firefox_bin_release.section.name)
+    mail
+    >>> print(firefox_bin_release.priority.name)
+    IMPORTANT
 
 Override again; ensure that only the changed item actually changes:
 
-   >>> new_sec = getUtility(ISectionSet)['net']
-   >>> firefox_bin_release.override(section=new_sec)
-   >>> print(firefox_bin_release.component.name)
-   universe
-   >>> print(firefox_bin_release.section.name)
-   net
-   >>> print(firefox_bin_release.priority.name)
-   IMPORTANT
+    >>> new_sec = getUtility(ISectionSet)['net']
+    >>> firefox_bin_release.override(section=new_sec)
+    >>> print(firefox_bin_release.component.name)
+    universe
+    >>> print(firefox_bin_release.section.name)
+    net
+    >>> print(firefox_bin_release.priority.name)
+    IMPORTANT
 
 
 Abort transaction to avoid error propagation of the new attributes:
 
-   >>> import transaction
-   >>> transaction.abort()
+    >>> import transaction
+    >>> transaction.abort()
 
 
 Binary file association
diff --git a/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt b/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt
index 0b72202..92d6aee 100644
--- a/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt
+++ b/lib/lp/soyuz/doc/build-failedtoupload-workflow.txt
@@ -18,107 +18,107 @@ Once a build result is recognised as FAILEDTOUPLOAD by the
 buildmaster/slave-scanner code, an build notification will be issued.
 See more information in build-notification.txt file.
 
-  >>> from lp.soyuz.interfaces.binarypackagebuild import (
-  ...     IBinaryPackageBuildSet)
-  >>> from lp.testing.mail_helpers import pop_notifications
-  >>> buildset = getUtility(IBinaryPackageBuildSet)
+    >>> from lp.soyuz.interfaces.binarypackagebuild import (
+    ...     IBinaryPackageBuildSet)
+    >>> from lp.testing.mail_helpers import pop_notifications
+    >>> buildset = getUtility(IBinaryPackageBuildSet)
 
 Let's use a sampledata build record in FAILEDTOUPLOAD:
 
-  >>> failedtoupload_candidate = buildset.getByID(22)
+    >>> failedtoupload_candidate = buildset.getByID(22)
 
-  >>> print(failedtoupload_candidate.title)
-  i386 build of cdrkit 1.0 in ubuntu breezy-autotest RELEASE
+    >>> print(failedtoupload_candidate.title)
+    i386 build of cdrkit 1.0 in ubuntu breezy-autotest RELEASE
 
-  >>> print(failedtoupload_candidate.status.name)
-  FAILEDTOUPLOAD
+    >>> print(failedtoupload_candidate.status.name)
+    FAILEDTOUPLOAD
 
-  >>> print(failedtoupload_candidate.upload_log.filename)
-  upload_22_log.txt
+    >>> print(failedtoupload_candidate.upload_log.filename)
+    upload_22_log.txt
 
 FAILEDTOUPLOAD notification requires 'extra_info' argument to be filled:
 
-  >>> failedtoupload_candidate.notify()
-  Traceback (most recent call last):
-  ...
-  AssertionError: Extra information is required for FAILEDTOUPLOAD notifications.
+    >>> failedtoupload_candidate.notify()
+    Traceback (most recent call last):
+    ...
+    AssertionError: Extra information is required for FAILEDTOUPLOAD notifications.
 
 Normally 'extra_info' will contain the output generated by the binary
 upload procedure with instructions to reprocess it:
 
-  >>> failedtoupload_candidate.notify(extra_info='ANY OUTPUT')
+    >>> failedtoupload_candidate.notify(extra_info='ANY OUTPUT')
 
-  >>> notifications = pop_notifications()
-  >>> len(notifications)
-  3
+    >>> notifications = pop_notifications()
+    >>> len(notifications)
+    3
 
 As for the other failure notifications we will send emails for the
 'lp-buildd-admins' team members (celso.providelo & foo.bar) and for
 source creator (mark) as specified in the test configuration:
 
-  >>> from lp.services.config import config
-  >>> config.builddmaster.notify_owner
-  True
+    >>> from lp.services.config import config
+    >>> config.builddmaster.notify_owner
+    True
 
 
-  >>> for build_notification in notifications:
-  ...      build_notification['To']
-  'Celso Providelo <celso.providelo@xxxxxxxxxxxxx>'
-  'Foo Bar <foo.bar@xxxxxxxxxxxxx>'
-  'Mark Shuttleworth <mark@xxxxxxxxxxx>'
+    >>> for build_notification in notifications:
+    ...      build_notification['To']
+    'Celso Providelo <celso.providelo@xxxxxxxxxxxxx>'
+    'Foo Bar <foo.bar@xxxxxxxxxxxxx>'
+    'Mark Shuttleworth <mark@xxxxxxxxxxx>'
 
 Note that the generated notification contain the 'extra_info' content:
 
-  >>> build_notification = notifications[0]
-
-  >>> build_notification['Subject']
-  '[Build #22] i386 build of cdrkit 1.0 in ubuntu breezy-autotest RELEASE'
-
-  >>> build_notification['X-Launchpad-Build-State']
-  'FAILEDTOUPLOAD'
-
-  >>> build_notification['X-Creator-Recipient']
-  'mark@xxxxxxxxxxx'
-
-  >>> notification_body = six.ensure_text(
-  ...     build_notification.get_payload(decode=True))
-  >>> print(notification_body) #doctest: -NORMALIZE_WHITESPACE
-  <BLANKLINE>
-   * Source Package: cdrkit
-   * Version: 1.0
-   * Architecture: i386
-   * Archive: ubuntu
-   * Component: main
-   * State: Failed to upload
-   * Duration: 1 minute
-   * Build Log: http://launchpad.test/ubuntu/+source/cdrkit/1.0/+build/22/+files/netapplet-1.0.0.tar.gz
-   * Builder: http://launchpad.test/builders/bob
-   * Source: http://launchpad.test/ubuntu/+source/cdrkit/1.0
-  <BLANKLINE>
-  Upload log:
-  ANY OUTPUT
-  <BLANKLINE>
-  If you want further information about this situation, feel free to
-  contact us by asking a question on Launchpad
-  (https://answers.launchpad.net/launchpad/+addquestion).
-  <BLANKLINE>
-  -- 
-  i386 build of cdrkit 1.0 in ubuntu breezy-autotest RELEASE
-  http://launchpad.test/ubuntu/+source/cdrkit/1.0/+build/22
-  <BLANKLINE>
-  You are receiving this email because you are a buildd administrator.
-  <BLANKLINE>
+    >>> build_notification = notifications[0]
+
+    >>> build_notification['Subject']
+    '[Build #22] i386 build of cdrkit 1.0 in ubuntu breezy-autotest RELEASE'
+
+    >>> build_notification['X-Launchpad-Build-State']
+    'FAILEDTOUPLOAD'
+
+    >>> build_notification['X-Creator-Recipient']
+    'mark@xxxxxxxxxxx'
+
+    >>> notification_body = six.ensure_text(
+    ...     build_notification.get_payload(decode=True))
+    >>> print(notification_body) #doctest: -NORMALIZE_WHITESPACE
+    <BLANKLINE>
+     * Source Package: cdrkit
+     * Version: 1.0
+     * Architecture: i386
+     * Archive: ubuntu
+     * Component: main
+     * State: Failed to upload
+     * Duration: 1 minute
+     * Build Log: http://launchpad.test/ubuntu/+source/cdrkit/1.0/+build/22/+files/netapplet-1.0.0.tar.gz
+     * Builder: http://launchpad.test/builders/bob
+     * Source: http://launchpad.test/ubuntu/+source/cdrkit/1.0
+    <BLANKLINE>
+    Upload log:
+    ANY OUTPUT
+    <BLANKLINE>
+    If you want further information about this situation, feel free to
+    contact us by asking a question on Launchpad
+    (https://answers.launchpad.net/launchpad/+addquestion).
+    <BLANKLINE>
+    -- 
+    i386 build of cdrkit 1.0 in ubuntu breezy-autotest RELEASE
+    http://launchpad.test/ubuntu/+source/cdrkit/1.0/+build/22
+    <BLANKLINE>
+    You are receiving this email because you are a buildd administrator.
+    <BLANKLINE>
 
 The other notifications are similar except for the footer.
 
-  >>> print(notifications[1].get_payload())
-  <BLANKLINE>
-  ...
-  You are receiving this email because you are a buildd administrator.
-  <BLANKLINE>
-  >>> print(notifications[2].get_payload())
-  <BLANKLINE>
-  ...
-  You are receiving this email because you created this version of this
-  package.
-  <BLANKLINE>
+    >>> print(notifications[1].get_payload())
+    <BLANKLINE>
+    ...
+    You are receiving this email because you are a buildd administrator.
+    <BLANKLINE>
+    >>> print(notifications[2].get_payload())
+    <BLANKLINE>
+    ...
+    You are receiving this email because you created this version of this
+    package.
+    <BLANKLINE>
diff --git a/lib/lp/soyuz/doc/components-and-sections.txt b/lib/lp/soyuz/doc/components-and-sections.txt
index dcdd476..a3fbd62 100644
--- a/lib/lp/soyuz/doc/components-and-sections.txt
+++ b/lib/lp/soyuz/doc/components-and-sections.txt
@@ -6,166 +6,166 @@ are related by their need, shipment condition and/or licence.
 
 Zope auxiliary test toolchain:
 
- >>> from zope.component import getUtility
- >>> from lp.testing import verifyObject
+    >>> from zope.component import getUtility
+    >>> from lp.testing import verifyObject
 
 Importing Component content class and its interface:
 
- >>> from lp.soyuz.interfaces.component import IComponent
- >>> from lp.soyuz.model.component import Component
+    >>> from lp.soyuz.interfaces.component import IComponent
+    >>> from lp.soyuz.model.component import Component
 
 Get an Component instance from the current sampledata:
 
- >>> main = Component.get(1)
+    >>> main = Component.get(1)
 
 Test some attributes:
 
- >>> print(main.id, main.name)
- 1 main
+    >>> print(main.id, main.name)
+    1 main
 
 Check if the instance corresponds to the declared interface:
 
- >>> verifyObject(IComponent, main)
- True
+    >>> verifyObject(IComponent, main)
+    True
 
 Now perform the tests for the Component ContentSet class, ComponentSet.
 
 Check if it can be imported:
 
- >>> from lp.soyuz.interfaces.component import IComponentSet
+    >>> from lp.soyuz.interfaces.component import IComponentSet
 
 Check we can use the set as a utility:
 
- >>> component_set = getUtility(IComponentSet)
+    >>> component_set = getUtility(IComponentSet)
 
 Test iteration over the sampledata default components:
 
- >>> for c in component_set:
- ...    print(c.name)
- main
- restricted
- universe
- multiverse
- partner
+    >>> for c in component_set:
+    ...    print(c.name)
+    main
+    restricted
+    universe
+    multiverse
+    partner
 
 by default, they are ordered by 'id'.
 
 Test __getitem__ method, retrieving a component by name:
 
- >>> print(component_set['universe'].name)
- universe
+    >>> print(component_set['universe'].name)
+    universe
 
 Test get method, retrieving a component by its id:
 
- >>> print(component_set.get(2).name)
- restricted
+    >>> print(component_set.get(2).name)
+    restricted
 
 New component creation for a given name:
 
- >>> new_comp = component_set.new('test')
- >>> print(new_comp.name)
- test
+    >>> new_comp = component_set.new('test')
+    >>> print(new_comp.name)
+    test
 
 Ensuring a component (if not found, create it):
 
- >>> component_set.ensure('test').id == new_comp.id
- True
+    >>> component_set.ensure('test').id == new_comp.id
+    True
 
- >>> component_set.ensure('test2').id == new_comp.id
- False
+    >>> component_set.ensure('test2').id == new_comp.id
+    False
 
 
 Importing Section content class and its interface:
 
- >>> from lp.soyuz.interfaces.section import ISection
- >>> from lp.soyuz.model.section import Section
+    >>> from lp.soyuz.interfaces.section import ISection
+    >>> from lp.soyuz.model.section import Section
 
 Get a Section instance from the current sampledata:
 
- >>> base = Section.get(1)
+    >>> base = Section.get(1)
 
 Test some attributes:
 
- >>> print(base.id, base.name)
- 1 base
+    >>> print(base.id, base.name)
+    1 base
 
 Check if the instance corresponds to the declared interface:
 
- >>> verifyObject(ISection, base)
- True
+    >>> verifyObject(ISection, base)
+    True
 
 Now perform the tests for the Section ContentSet class, SectionSet.
 
 Check if it can be imported:
 
- >>> from lp.soyuz.interfaces.section import ISectionSet
+    >>> from lp.soyuz.interfaces.section import ISectionSet
 
 Check we can use the set as a utility:
 
- >>> section_set = getUtility(ISectionSet)
+    >>> section_set = getUtility(ISectionSet)
 
 Test iteration over the sampledata default sections:
 
- >>> for s in section_set:
- ...    print(s.name)
- base
- web
- editors
- admin
- comm
- debian-installer
- devel
- doc
- games
- gnome
- graphics
- interpreters
- kde
- libdevel
- libs
- mail
- math
- misc
- net
- news
- oldlibs
- otherosfs
- perl
- python
- shells
- sound
- tex
- text
- translations
- utils
- x11
- electronics
- embedded
- hamradio
- science
+    >>> for s in section_set:
+    ...    print(s.name)
+    base
+    web
+    editors
+    admin
+    comm
+    debian-installer
+    devel
+    doc
+    games
+    gnome
+    graphics
+    interpreters
+    kde
+    libdevel
+    libs
+    mail
+    math
+    misc
+    net
+    news
+    oldlibs
+    otherosfs
+    perl
+    python
+    shells
+    sound
+    tex
+    text
+    translations
+    utils
+    x11
+    electronics
+    embedded
+    hamradio
+    science
 
 by default they are ordered by 'id'.
 
 Test __getitem__ method, retrieving a section by name:
 
- >>> print(section_set['science'].name)
- science
+    >>> print(section_set['science'].name)
+    science
 
 Test get method, retrieving a section by its id:
 
- >>> print(section_set.get(2).name)
- web
+    >>> print(section_set.get(2).name)
+    web
 
 New section creation for a given name:
 
- >>> new_sec = section_set.new('test')
- >>> print(new_sec.name)
- test
+    >>> new_sec = section_set.new('test')
+    >>> print(new_sec.name)
+    test
 
 Ensuring a section (if not found, create it):
 
- >>> section_set.ensure('test').id == new_sec.id
- True
+    >>> section_set.ensure('test').id == new_sec.id
+    True
 
- >>> section_set.ensure('test2').id == new_sec.id
- False
+    >>> section_set.ensure('test2').id == new_sec.id
+    False
diff --git a/lib/lp/soyuz/doc/distroarchseriesbinarypackagerelease.txt b/lib/lp/soyuz/doc/distroarchseriesbinarypackagerelease.txt
index 9cfd45b..5bfa59d 100644
--- a/lib/lp/soyuz/doc/distroarchseriesbinarypackagerelease.txt
+++ b/lib/lp/soyuz/doc/distroarchseriesbinarypackagerelease.txt
@@ -1,49 +1,50 @@
 Distro Arch Release Binary Package Release
 ==========================================
 
-  >>> from lp.soyuz.model.distroarchseriesbinarypackagerelease \
-  ...   import DistroArchSeriesBinaryPackageRelease as DARBPR
-  >>> from lp.soyuz.model.binarypackagerelease import (
-  ...     BinaryPackageRelease)
-  >>> from lp.soyuz.model.distroarchseries import (
-  ...     DistroArchSeries)
+    >>> from lp.soyuz.model.distroarchseriesbinarypackagerelease \
+    ...   import DistroArchSeriesBinaryPackageRelease as DARBPR
+    >>> from lp.soyuz.model.binarypackagerelease import (
+    ...     BinaryPackageRelease)
+    >>> from lp.soyuz.model.distroarchseries import (
+    ...     DistroArchSeries)
 
 Grab the relevant DARs and BPRs:
 
-  >>> warty = DistroArchSeries.get(1)
-  >>> print(warty.distroseries.name)
-  warty
-  >>> hoary = DistroArchSeries.get(6)
-  >>> print(hoary.distroseries.name)
-  hoary
+    >>> warty = DistroArchSeries.get(1)
+    >>> print(warty.distroseries.name)
+    warty
+    >>> hoary = DistroArchSeries.get(6)
+    >>> print(hoary.distroseries.name)
+    hoary
 
-  >>> mf = BinaryPackageRelease.get(12)
-  >>> print(mf.binarypackagename.name)
-  mozilla-firefox
+    >>> mf = BinaryPackageRelease.get(12)
+    >>> print(mf.binarypackagename.name)
+    mozilla-firefox
 
-  >>> pm = BinaryPackageRelease.get(15)
-  >>> print(pm.binarypackagename.name)
-  pmount
+    >>> pm = BinaryPackageRelease.get(15)
+    >>> print(pm.binarypackagename.name)
+    pmount
 
 Assemble our DARBPRs for fun and profit:
 
-  >>> mf_warty = DARBPR(warty, mf)
-  >>> mf_hoary = DARBPR(hoary, mf)
-  >>> pm_warty = DARBPR(warty, pm)
-  >>> pm_hoary = DARBPR(hoary, pm)
-
-  >>> for darbpr in [mf_warty, mf_hoary, pm_warty, pm_hoary]:
-  ...   print(
-  ...       darbpr.name, darbpr.version, darbpr._latest_publishing_record())
-  mozilla-firefox 0.9 <BinaryPackagePublishingHistory at 0x...>
-  mozilla-firefox 0.9 None
-  pmount 0.1-1 <BinaryPackagePublishingHistory at 0x...>
-  pmount 0.1-1 <BinaryPackagePublishingHistory at 0x...>
-
-  >>> print(
-  ...     mf_warty.status.title, pm_warty.status.title,
-  ...     pm_hoary.status.title)
-  Published Superseded Published
+    >>> mf_warty = DARBPR(warty, mf)
+    >>> mf_hoary = DARBPR(hoary, mf)
+    >>> pm_warty = DARBPR(warty, pm)
+    >>> pm_hoary = DARBPR(hoary, pm)
+
+    >>> for darbpr in [mf_warty, mf_hoary, pm_warty, pm_hoary]:
+    ...     print(
+    ...         darbpr.name, darbpr.version,
+    ...         darbpr._latest_publishing_record())
+    mozilla-firefox 0.9 <BinaryPackagePublishingHistory at 0x...>
+    mozilla-firefox 0.9 None
+    pmount 0.1-1 <BinaryPackagePublishingHistory at 0x...>
+    pmount 0.1-1 <BinaryPackagePublishingHistory at 0x...>
+
+    >>> print(
+    ...     mf_warty.status.title, pm_warty.status.title,
+    ...     pm_hoary.status.title)
+    Published Superseded Published
 
 
 Retrieving the parent object, a DistroArchSeriesBinaryPackage.
diff --git a/lib/lp/soyuz/doc/package-cache-script.txt b/lib/lp/soyuz/doc/package-cache-script.txt
index 031b88b..8b45e2f 100644
--- a/lib/lp/soyuz/doc/package-cache-script.txt
+++ b/lib/lp/soyuz/doc/package-cache-script.txt
@@ -6,76 +6,77 @@ Package cache system is better described in package-cache.txt.
 'update-pkgcache.py' is supposed to run periodically in our
 infrastructure and it is localised in the 'cronscripts' directory
 
-  >>> import os
-  >>> from lp.services.config import config
-  >>> script = os.path.join(config.root, "cronscripts", "update-pkgcache.py")
+    >>> import os
+    >>> from lp.services.config import config
+    >>> script = os.path.join(
+    ...     config.root, "cronscripts", "update-pkgcache.py")
 
 Database sampledata has two pending modifications of package cache
 contents:
 
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
-  >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
-  >>> warty = ubuntu['warty']
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
+    >>> warty = ubuntu['warty']
 
 'cdrkit' source and binary are published but it's not present in
 cache:
 
-  >>> ubuntu.searchSourcePackages(u'cdrkit').count()
-  0
-  >>> warty.searchPackages(u'cdrkit').count()
-  0
+    >>> ubuntu.searchSourcePackages(u'cdrkit').count()
+    0
+    >>> warty.searchPackages(u'cdrkit').count()
+    0
 
 'foobar' source and binary are removed but still present in cache:
 
-  >>> ubuntu.searchSourcePackages(u'foobar').count()
-  1
-  >>> warty.searchPackages(u'foobar').count()
-  1
+    >>> ubuntu.searchSourcePackages(u'foobar').count()
+    1
+    >>> warty.searchPackages(u'foobar').count()
+    1
 
 Normal operation produces INFO level information about the
 distribution and respective distroseriess considered in stderr.
 
-  >>> import subprocess
-  >>> import sys
-  >>> process = subprocess.Popen([sys.executable, script],
-  ...                            stdout=subprocess.PIPE,
-  ...                            stderr=subprocess.PIPE,
-  ...                            universal_newlines=True)
-  >>> stdout, stderr = process.communicate()
-  >>> process.returncode
-  0
-
-  >>> print(stdout)
-
-  >>> print(stderr)
-  INFO    Creating lockfile: /var/lock/launchpad-update-cache.lock
-  INFO    Updating ubuntu package counters
-  INFO    Updating ubuntu main archives
-  ...
-  INFO    Updating ubuntu official branch links
-  INFO    Updating ubuntu PPAs
-  ...
-  INFO    redhat done
+    >>> import subprocess
+    >>> import sys
+    >>> process = subprocess.Popen([sys.executable, script],
+    ...                            stdout=subprocess.PIPE,
+    ...                            stderr=subprocess.PIPE,
+    ...                            universal_newlines=True)
+    >>> stdout, stderr = process.communicate()
+    >>> process.returncode
+    0
+
+    >>> print(stdout)
+
+    >>> print(stderr)
+    INFO    Creating lockfile: /var/lock/launchpad-update-cache.lock
+    INFO    Updating ubuntu package counters
+    INFO    Updating ubuntu main archives
+    ...
+    INFO    Updating ubuntu official branch links
+    INFO    Updating ubuntu PPAs
+    ...
+    INFO    redhat done
 
 Rollback the old transaction in order to get the modifications done by
 the external script:
 
- >>> import transaction
- >>> transaction.abort()
+    >>> import transaction
+    >>> transaction.abort()
 
 Now, search results are up to date:
 
-  >>> ubuntu.searchSourcePackages(u'cdrkit').count()
-  1
-  >>> warty.searchPackages(u'cdrkit').count()
-  1
+    >>> ubuntu.searchSourcePackages(u'cdrkit').count()
+    1
+    >>> warty.searchPackages(u'cdrkit').count()
+    1
 
-  >>> ubuntu.searchSourcePackages(u'foobar').count()
-  0
-  >>> warty.searchPackages(u'foobar').count()
-  0
+    >>> ubuntu.searchSourcePackages(u'foobar').count()
+    0
+    >>> warty.searchPackages(u'foobar').count()
+    0
 
 Explicitly mark the database as dirty so that it is cleaned (see bug 994158).
 
-  >>> from lp.testing.layers import DatabaseLayer
-  >>> DatabaseLayer.force_dirty_database()
+    >>> from lp.testing.layers import DatabaseLayer
+    >>> DatabaseLayer.force_dirty_database()
diff --git a/lib/lp/soyuz/doc/package-meta-classes.txt b/lib/lp/soyuz/doc/package-meta-classes.txt
index a5d5f4b..9703977 100644
--- a/lib/lp/soyuz/doc/package-meta-classes.txt
+++ b/lib/lp/soyuz/doc/package-meta-classes.txt
@@ -4,13 +4,15 @@ Package Meta Classes
 There are a bunch of meta classes used for combine information from
 our Database Model for packages in a intuitive manner, they are:
 
- >>> from lp.registry.model.distribution import Distribution
- >>> from lp.registry.model.sourcepackagename import SourcePackageName
- >>> from lp.soyuz.model.distributionsourcepackagerelease import (
- ...     DistributionSourcePackageRelease)
- >>> from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
+    >>> from lp.registry.model.distribution import Distribution
+    >>> from lp.registry.model.sourcepackagename import SourcePackageName
+    >>> from lp.soyuz.model.distributionsourcepackagerelease import (
+    ...     DistributionSourcePackageRelease)
+    >>> from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 
- >>> from lp.soyuz.interfaces.distributionsourcepackagerelease import IDistributionSourcePackageRelease
+    >>> from lp.soyuz.interfaces.distributionsourcepackagerelease import (
+    ...     IDistributionSourcePackageRelease,
+    ...     )
 
 
 DistributionSourcePackage class is tested in:
@@ -18,27 +20,27 @@ DistributionSourcePackage class is tested in:
 
 Combining Distribution and SourcePackageRelease:
 
- >>> distribution = Distribution.get(1)
- >>> print(distribution.name)
- ubuntu
+    >>> distribution = Distribution.get(1)
+    >>> print(distribution.name)
+    ubuntu
 
- >>> src_name = SourcePackageName.selectOneBy(name='pmount')
- >>> print(src_name.name)
- pmount
+    >>> src_name = SourcePackageName.selectOneBy(name='pmount')
+    >>> print(src_name.name)
+    pmount
 
- >>> sourcepackagerelease = SourcePackageRelease.selectOneBy(
- ...     sourcepackagenameID=src_name.id, version='0.1-1')
- >>> print(sourcepackagerelease.name)
- pmount
+    >>> sourcepackagerelease = SourcePackageRelease.selectOneBy(
+    ...     sourcepackagenameID=src_name.id, version='0.1-1')
+    >>> print(sourcepackagerelease.name)
+    pmount
 
- >>> from lp.testing import verifyObject
- >>> dspr = DistributionSourcePackageRelease(distribution,
- ...                                         sourcepackagerelease)
- >>> verifyObject(IDistributionSourcePackageRelease, dspr)
- True
+    >>> from lp.testing import verifyObject
+    >>> dspr = DistributionSourcePackageRelease(distribution,
+    ...                                         sourcepackagerelease)
+    >>> verifyObject(IDistributionSourcePackageRelease, dspr)
+    True
 
- >>> print(dspr.displayname)
- pmount 0.1-1
+    >>> print(dspr.displayname)
+    pmount 0.1-1
 
 
 Querying builds for DistributionSourcePackageRelease
diff --git a/lib/lp/soyuz/doc/package-relationship-pages.txt b/lib/lp/soyuz/doc/package-relationship-pages.txt
index c49d90d..fec6824 100644
--- a/lib/lp/soyuz/doc/package-relationship-pages.txt
+++ b/lib/lp/soyuz/doc/package-relationship-pages.txt
@@ -11,39 +11,41 @@ group. Some example of pages using it are:
 
 Let's fill a IPackageRelationshipSet:
 
-  >>> from lp.soyuz.browser.packagerelationship import PackageRelationshipSet
-
-  >>> relationship_set = PackageRelationshipSet()
-  >>> relationship_set.add(
-  ...    name="foobar",
-  ...    operator=">=",
-  ...    version="1.0.2",
-  ...    url="http://whatever/";)
-
-  >>> relationship_set.add(
-  ...    name="test",
-  ...    operator="=",
-  ...    version="1.0",
-  ...    url=None)
+    >>> from lp.soyuz.browser.packagerelationship import (
+    ...     PackageRelationshipSet,
+    ...     )
+
+    >>> relationship_set = PackageRelationshipSet()
+    >>> relationship_set.add(
+    ...    name="foobar",
+    ...    operator=">=",
+    ...    version="1.0.2",
+    ...    url="http://whatever/";)
+
+    >>> relationship_set.add(
+    ...    name="test",
+    ...    operator="=",
+    ...    version="1.0",
+    ...    url=None)
 
 Note that iterations over PackageRelationshipSet are sorted
 alphabetically according to the relationship 'name':
 
-  >>> for relationship in relationship_set:
-  ...     print(relationship.name)
-  foobar
-  test
+    >>> for relationship in relationship_set:
+    ...     print(relationship.name)
+    foobar
+    test
 
 It will cause all the relationship contents to be rendered in this order.
 
 Let's get the view class:
 
-  >>> from zope.component import queryMultiAdapter
-  >>> from zope.publisher.browser import TestRequest
+    >>> from zope.component import queryMultiAdapter
+    >>> from zope.publisher.browser import TestRequest
 
-  >>> request = TestRequest(form={})
-  >>> pkg_rel_view = queryMultiAdapter(
-  ...     (relationship_set, request), name="+render-list")
+    >>> request = TestRequest(form={})
+    >>> pkg_rel_view = queryMultiAdapter(
+    ...     (relationship_set, request), name="+render-list")
 
 This view has no methods, so just demonstrate that it renders
 correctly like:
@@ -58,11 +60,11 @@ correctly like:
      </li>
   </ul>
 
-  >>> from lp.testing.pages import parse_relationship_section
+    >>> from lp.testing.pages import parse_relationship_section
 
-  >>> parse_relationship_section(pkg_rel_view())
-  LINK: "foobar (>= 1.0.2)" -> http://whatever/
-  TEXT: "test (= 1.0)"
+    >>> parse_relationship_section(pkg_rel_view())
+    LINK: "foobar (>= 1.0.2)" -> http://whatever/
+    TEXT: "test (= 1.0)"
 
 
 Note that no link is rendered for IPackageReleationship where 'url' is
diff --git a/lib/lp/soyuz/doc/package-relationship.txt b/lib/lp/soyuz/doc/package-relationship.txt
index 18f13e3..56ec8ae 100644
--- a/lib/lp/soyuz/doc/package-relationship.txt
+++ b/lib/lp/soyuz/doc/package-relationship.txt
@@ -30,14 +30,14 @@ element follows this format:
 
 For example:
 
-  >>> relationship_line = (
-  ...    'gcc-3.4-base, libc6 (>= 2.3.2.ds1-4), gcc-3.4 ( = 3.4.1-4sarge1)')
+    >>> relationship_line = (
+    ...    'gcc-3.4-base, libc6 (>= 2.3.2.ds1-4), gcc-3.4 ( = 3.4.1-4sarge1)')
 
 Launchpad models package relationship elements via the
 IPackageRelationship instance. We use deb822 to parse the relationship
 lines:
 
-  >>> from debian.deb822 import PkgRelation
+    >>> from debian.deb822 import PkgRelation
 
 PkgRelation.parse_relations returns a 'list of lists of dicts' as:
 
@@ -47,37 +47,39 @@ PkgRelation.parse_relations returns a 'list of lists of dicts' as:
 
 So we need to massage its result into the form we prefer:
 
-  >>> def parse_relations(line):
-  ...     for (rel,) in PkgRelation.parse_relations(relationship_line):
-  ...         if rel['version'] is None:
-  ...             operator, version = '', ''
-  ...         else:
-  ...             operator, version = rel['version']
-  ...         yield rel['name'], version, operator
-  >>> parsed_relationships = list(parse_relations(relationship_line))
-  >>> parsed_relationships
-  [('gcc-3.4-base', '', ''), ('libc6', '2.3.2.ds1-4', '>='), ('gcc-3.4', '3.4.1-4sarge1', '=')]
+    >>> def parse_relations(line):
+    ...     for (rel,) in PkgRelation.parse_relations(relationship_line):
+    ...         if rel['version'] is None:
+    ...             operator, version = '', ''
+    ...         else:
+    ...             operator, version = rel['version']
+    ...         yield rel['name'], version, operator
+    >>> parsed_relationships = list(parse_relations(relationship_line))
+    >>> parsed_relationships
+    [('gcc-3.4-base', '', ''), ('libc6', '2.3.2.ds1-4', '>='), ('gcc-3.4', '3.4.1-4sarge1', '=')]
 
 Now for each parsed element we can build an IPackageRelationship:
 
-  >>> from lp.soyuz.browser.packagerelationship import PackageRelationship
-  >>> from lp.soyuz.interfaces.packagerelationship import IPackageRelationship
-  >>> from lp.testing import verifyObject
-
-  >>> name, version, operator = parsed_relationships[1]
-  >>> fake_url = 'http://host/path'
-
-  >>> pkg_relationship = PackageRelationship(
-  ...     name, operator, version, url=fake_url)
-
-  >>> verifyObject(IPackageRelationship, pkg_relationship)
-  True
-
-  >>> pkg_relationship.name
-  'libc6'
-  >>> pkg_relationship.operator
-  '>='
-  >>> pkg_relationship.version
-  '2.3.2.ds1-4'
-  >>> pkg_relationship.url == fake_url
-  True
+    >>> from lp.soyuz.browser.packagerelationship import PackageRelationship
+    >>> from lp.soyuz.interfaces.packagerelationship import (
+    ...     IPackageRelationship,
+    ...     )
+    >>> from lp.testing import verifyObject
+
+    >>> name, version, operator = parsed_relationships[1]
+    >>> fake_url = 'http://host/path'
+
+    >>> pkg_relationship = PackageRelationship(
+    ...     name, operator, version, url=fake_url)
+
+    >>> verifyObject(IPackageRelationship, pkg_relationship)
+    True
+
+    >>> pkg_relationship.name
+    'libc6'
+    >>> pkg_relationship.operator
+    '>='
+    >>> pkg_relationship.version
+    '2.3.2.ds1-4'
+    >>> pkg_relationship.url == fake_url
+    True
diff --git a/lib/lp/soyuz/doc/pocketchroot.txt b/lib/lp/soyuz/doc/pocketchroot.txt
index 86e3b43..3686ebe 100644
--- a/lib/lp/soyuz/doc/pocketchroot.txt
+++ b/lib/lp/soyuz/doc/pocketchroot.txt
@@ -6,103 +6,103 @@ PocketChroot records combine DistroArchSeries and a Chroot.
 Chroot are identified per LibraryFileAlias and we offer three method
 based on IDistroArchSeries to handle them: get, add and update.
 
-  >>> from lp.services.librarian.interfaces import ILibraryFileAliasSet
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
-  >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
-  >>> from lp.testing import login_admin
+    >>> from lp.services.librarian.interfaces import ILibraryFileAliasSet
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
+    >>> from lp.testing import login_admin
 
-  >>> _ = login_admin()
+    >>> _ = login_admin()
 
 
 Grab a distroarchseries:
 
-  >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
-  >>> hoary = ubuntu['hoary']
-  >>> hoary_i386 = hoary['i386']
+    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
+    >>> hoary = ubuntu['hoary']
+    >>> hoary_i386 = hoary['i386']
 
 Grab some files to be used as Chroots (it doesn't really matter what
 they are, they simply need to be provide ILFA interface):
 
-  >>> chroot1 = getUtility(ILibraryFileAliasSet)[1]
-  >>> chroot2 = getUtility(ILibraryFileAliasSet)[2]
+    >>> chroot1 = getUtility(ILibraryFileAliasSet)[1]
+    >>> chroot2 = getUtility(ILibraryFileAliasSet)[2]
 
 Check if getPocketChroot returns None for unknown chroots:
 
-  >>> p_chroot = hoary_i386.getPocketChroot(PackagePublishingPocket.RELEASE)
-  >>> print(p_chroot)
-  None
+    >>> p_chroot = hoary_i386.getPocketChroot(PackagePublishingPocket.RELEASE)
+    >>> print(p_chroot)
+    None
 
 Check if getChroot returns the 'default' argument on not found chroots:
 
-  >>> print(hoary_i386.getChroot(default='duuuuh'))
-  duuuuh
+    >>> print(hoary_i386.getChroot(default='duuuuh'))
+    duuuuh
 
 Invoke addOrUpdateChroot for missing chroot, so it will insert a new
 record in PocketChroot:
 
-  >>> p_chroot1 = hoary_i386.addOrUpdateChroot(chroot=chroot1)
-  >>> print(p_chroot1.distroarchseries.architecturetag)
-  i386
-  >>> print(p_chroot1.pocket.name)
-  RELEASE
-  >>> print(p_chroot1.chroot.id)
-  1
+    >>> p_chroot1 = hoary_i386.addOrUpdateChroot(chroot=chroot1)
+    >>> print(p_chroot1.distroarchseries.architecturetag)
+    i386
+    >>> print(p_chroot1.pocket.name)
+    RELEASE
+    >>> print(p_chroot1.chroot.id)
+    1
 
 Invoke addOrUpdateChroot on an existing PocketChroot, it will update
 the chroot:
 
-  >>> p_chroot2 = hoary_i386.addOrUpdateChroot(chroot=chroot2)
-  >>> print(p_chroot2.distroarchseries.architecturetag)
-  i386
-  >>> print(p_chroot2.pocket.name)
-  RELEASE
-  >>> print(p_chroot2.chroot.id)
-  2
-  >>> p_chroot2 == p_chroot1
-  True
+    >>> p_chroot2 = hoary_i386.addOrUpdateChroot(chroot=chroot2)
+    >>> print(p_chroot2.distroarchseries.architecturetag)
+    i386
+    >>> print(p_chroot2.pocket.name)
+    RELEASE
+    >>> print(p_chroot2.chroot.id)
+    2
+    >>> p_chroot2 == p_chroot1
+    True
 
 Ensure chroot was updated by retriving it from DB again:
 
-  >>> hoary_i386.getPocketChroot(PackagePublishingPocket.RELEASE).chroot.id
-  2
+    >>> hoary_i386.getPocketChroot(PackagePublishingPocket.RELEASE).chroot.id
+    2
 
 Check if getChroot returns the correspondent Chroot LFA instance for
 valid chroots.
 
-  >>> chroot = hoary_i386.getChroot()
-  >>> chroot.id
-  2
+    >>> chroot = hoary_i386.getChroot()
+    >>> chroot.id
+    2
 
 PocketChroots can also (per the name) be set for specific pockets:
 
-  >>> chroot3 = getUtility(ILibraryFileAliasSet)[3]
-  >>> p_chroot3 = hoary_i386.addOrUpdateChroot(
-  ...     chroot=chroot3, pocket=PackagePublishingPocket.UPDATES)
-  >>> print(p_chroot3.distroarchseries.architecturetag)
-  i386
-  >>> print(p_chroot3.pocket.name)
-  UPDATES
-  >>> print(p_chroot3.chroot.id)
-  3
-  >>> hoary_i386.getPocketChroot(PackagePublishingPocket.UPDATES).chroot.id
-  3
-  >>> hoary_i386.getChroot(pocket=PackagePublishingPocket.UPDATES).id
-  3
+    >>> chroot3 = getUtility(ILibraryFileAliasSet)[3]
+    >>> p_chroot3 = hoary_i386.addOrUpdateChroot(
+    ...     chroot=chroot3, pocket=PackagePublishingPocket.UPDATES)
+    >>> print(p_chroot3.distroarchseries.architecturetag)
+    i386
+    >>> print(p_chroot3.pocket.name)
+    UPDATES
+    >>> print(p_chroot3.chroot.id)
+    3
+    >>> hoary_i386.getPocketChroot(PackagePublishingPocket.UPDATES).chroot.id
+    3
+    >>> hoary_i386.getChroot(pocket=PackagePublishingPocket.UPDATES).id
+    3
 
 getPocketChroot falls back to depended-on pockets if necessary:
 
-  >>> hoary_i386.getPocketChroot(PackagePublishingPocket.SECURITY).chroot.id
-  2
-  >>> print(hoary_i386.getPocketChroot(
-  ...     PackagePublishingPocket.SECURITY, exact_pocket=True))
-  None
-  >>> hoary_i386.getChroot(pocket=PackagePublishingPocket.SECURITY).id
-  2
-  >>> hoary_i386.removeChroot(pocket=PackagePublishingPocket.UPDATES)
-  >>> hoary_i386.getChroot(pocket=PackagePublishingPocket.UPDATES).id
-  2
+    >>> hoary_i386.getPocketChroot(PackagePublishingPocket.SECURITY).chroot.id
+    2
+    >>> print(hoary_i386.getPocketChroot(
+    ...     PackagePublishingPocket.SECURITY, exact_pocket=True))
+    None
+    >>> hoary_i386.getChroot(pocket=PackagePublishingPocket.SECURITY).id
+    2
+    >>> hoary_i386.removeChroot(pocket=PackagePublishingPocket.UPDATES)
+    >>> hoary_i386.getChroot(pocket=PackagePublishingPocket.UPDATES).id
+    2
 
 Force transaction commit in order to test DB constraints:
 
-  >>> import transaction
-  >>> transaction.commit()
+    >>> import transaction
+    >>> transaction.commit()
diff --git a/lib/lp/soyuz/doc/sourcepackagerelease.txt b/lib/lp/soyuz/doc/sourcepackagerelease.txt
index 45226aa..b4dd9e3 100644
--- a/lib/lp/soyuz/doc/sourcepackagerelease.txt
+++ b/lib/lp/soyuz/doc/sourcepackagerelease.txt
@@ -16,14 +16,14 @@ Basic attributes
 
 Let's get one from the database:
 
-   >>> from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
-   >>> spr = SourcePackageRelease.get(20)
-   >>> print(spr.name)
-   pmount
-   >>> print(spr.version)
-   0.1-1
-   >>> spr.dateuploaded
-   datetime.datetime(2005, 3, 24, 20, 59, 31, 439579, tzinfo=<UTC>)
+    >>> from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
+    >>> spr = SourcePackageRelease.get(20)
+    >>> print(spr.name)
+    pmount
+    >>> print(spr.version)
+    0.1-1
+    >>> spr.dateuploaded
+    datetime.datetime(2005, 3, 24, 20, 59, 31, 439579, tzinfo=<UTC>)
 
 published_archives returns a set of all the archives that this
 SourcePackageRelease is published in.
@@ -39,156 +39,159 @@ NOW - dateuploaded
 }}}
 It returns a timedelta object:
 
-   >>> spr.age
-   datetime.timedelta(...)
+    >>> spr.age
+    datetime.timedelta(...)
 
 Check if the result match the locally calculated one:
 
-   >>> import datetime
-   >>> import pytz
-   >>> local_now = datetime.datetime.now(pytz.timezone('UTC'))
+    >>> import datetime
+    >>> import pytz
+    >>> local_now = datetime.datetime.now(pytz.timezone('UTC'))
 
-   >>> expected_age = local_now - spr.dateuploaded
-   >>> spr.age.days == expected_age.days
-   True
+    >>> expected_age = local_now - spr.dateuploaded
+    >>> spr.age.days == expected_age.days
+    True
 
 Modify dateuploaded to a certain number of days in the past and check
 if the 'age' result looks sane:
 
-   >>> spr.dateuploaded = (local_now - datetime.timedelta(days=10))
-   >>> spr.age.days == 10
-   True
+    >>> spr.dateuploaded = (local_now - datetime.timedelta(days=10))
+    >>> spr.age.days == 10
+    True
 
 pmount 0.1-1 has got some builds. including a PPA build.  The 'builds'
 property only returns the non-PPA builds.
 
-   >>> from lp.registry.interfaces.person import IPersonSet
-   >>> from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
-   >>> from storm.store import Store
-   >>> cprov_ppa = getUtility(IPersonSet).getByName('cprov').archive
-   >>> ff_ppa_build = Store.of(cprov_ppa).find(
-   ...     BinaryPackageBuild,
-   ...     BinaryPackageBuild.source_package_release == spr,
-   ...     BinaryPackageBuild.archive == cprov_ppa)
-   >>> ff_ppa_build.count()
-   1
-   >>> ff_ppa_build[0].archive.purpose.name
-   'PPA'
-   >>> spr.builds.count()
-   4
+    >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
+    >>> from storm.store import Store
+    >>> cprov_ppa = getUtility(IPersonSet).getByName('cprov').archive
+    >>> ff_ppa_build = Store.of(cprov_ppa).find(
+    ...     BinaryPackageBuild,
+    ...     BinaryPackageBuild.source_package_release == spr,
+    ...     BinaryPackageBuild.archive == cprov_ppa)
+    >>> ff_ppa_build.count()
+    1
+    >>> ff_ppa_build[0].archive.purpose.name
+    'PPA'
+    >>> spr.builds.count()
+    4
 
 All the builds returned are for non-PPA archives:
 
-   >>> for item in set(build.archive.purpose.name for build in spr.builds):
-   ...     print(item)
-   PRIMARY
+    >>> for item in set(build.archive.purpose.name for build in spr.builds):
+    ...     print(item)
+    PRIMARY
 
 Check that the uploaded changesfile works:
 
-   >>> commercial = SourcePackageRelease.get(36)
-   >>> commercial.upload_changesfile.http_url
-   'http://.../commercialpackage_1.0-1_source.changes'
+    >>> commercial = SourcePackageRelease.get(36)
+    >>> commercial.upload_changesfile.http_url
+    'http://.../commercialpackage_1.0-1_source.changes'
 
 Check ISourcePackageRelease.override() behaviour:
 
-   >>> print(spr.component.name)
-   main
-   >>> print(spr.section.name)
-   web
+    >>> print(spr.component.name)
+    main
+    >>> print(spr.section.name)
+    web
 
-   >>> from lp.soyuz.interfaces.component import IComponentSet
-   >>> from lp.soyuz.interfaces.section import ISectionSet
-   >>> new_comp = getUtility(IComponentSet)['universe']
-   >>> new_sec = getUtility(ISectionSet)['mail']
+    >>> from lp.soyuz.interfaces.component import IComponentSet
+    >>> from lp.soyuz.interfaces.section import ISectionSet
+    >>> new_comp = getUtility(IComponentSet)['universe']
+    >>> new_sec = getUtility(ISectionSet)['mail']
 
 Override the current sourcepackagerelease with new component/section
 pair:
 
-   >>> spr.override(component=new_comp, section=new_sec)
+    >>> spr.override(component=new_comp, section=new_sec)
 
-   >>> print(spr.component.name)
-   universe
-   >>> print(spr.section.name)
-   mail
+    >>> print(spr.component.name)
+    universe
+    >>> print(spr.section.name)
+    mail
 
 Abort transaction to avoid error propagation of the new attributes:
 
-   >>> import transaction
-   >>> transaction.abort()
+    >>> import transaction
+    >>> transaction.abort()
 
 
 Verify the creation of a new ISourcePackageRelease based on the
 IDistroSeries API:
 
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
-  >>> from lp.registry.interfaces.gpg import IGPGKeySet
-  >>> from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
-  >>> from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> from lp.registry.interfaces.gpg import IGPGKeySet
+    >>> from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
+    >>> from lp.registry.interfaces.sourcepackagename import (
+    ...     ISourcePackageNameSet,
+    ...     )
 
-  >>> hoary = getUtility(IDistributionSet)['ubuntu']['hoary']
+    >>> hoary = getUtility(IDistributionSet)['ubuntu']['hoary']
 
 All the arguments to create an ISourcePackageRelease are obtained when
 processing a source upload, see more details in nascentupload.txt.
 Some of the 20 required arguments are foreign keys or DB contants:
 
-  >>> arg_name = getUtility(ISourcePackageNameSet)['pmount']
-  >>> arg_comp = getUtility(IComponentSet)['universe']
-  >>> arg_sect = getUtility(ISectionSet)['web']
-  >>> arg_key = getUtility(IGPGKeySet).getByFingerprint('ABCDEF0123456789ABCDDCBA0000111112345678')
-  >>> arg_maintainer = hoary.owner
-  >>> arg_creator = hoary.owner
-  >>> arg_urgency = SourcePackageUrgency.LOW
-  >>> arg_recipebuild = factory.makeSourcePackageRecipeBuild()
-  >>> changelog = None
+    >>> arg_name = getUtility(ISourcePackageNameSet)['pmount']
+    >>> arg_comp = getUtility(IComponentSet)['universe']
+    >>> arg_sect = getUtility(ISectionSet)['web']
+    >>> arg_key = getUtility(IGPGKeySet).getByFingerprint(
+    ...     'ABCDEF0123456789ABCDDCBA0000111112345678')
+    >>> arg_maintainer = hoary.owner
+    >>> arg_creator = hoary.owner
+    >>> arg_urgency = SourcePackageUrgency.LOW
+    >>> arg_recipebuild = factory.makeSourcePackageRecipeBuild()
+    >>> changelog = None
 
 The other argurments are strings:
 
-  >>> version = '0.0.99'
-  >>> dsc = 'smashed dsc...'
-  >>> copyright = 'smashed debian/copyright ...'
-  >>> changelog_entry = 'contigous text....'
-  >>> archhintlist = 'any'
-  >>> builddepends = 'cdbs, debhelper (>= 4.1.0), libsysfs-dev, libhal-dev'
-  >>> builddependsindep = ''
-  >>> dsc_maintainer_rfc822 = 'Foo Bar <foo@xxxxxxx>'
-  >>> dsc_standards_version = '2.6.1'
-  >>> dsc_format = '1.0'
-  >>> dsc_binaries = 'pmount'
-  >>> archive = hoary.main_archive
+    >>> version = '0.0.99'
+    >>> dsc = 'smashed dsc...'
+    >>> copyright = 'smashed debian/copyright ...'
+    >>> changelog_entry = 'contigous text....'
+    >>> archhintlist = 'any'
+    >>> builddepends = 'cdbs, debhelper (>= 4.1.0), libsysfs-dev, libhal-dev'
+    >>> builddependsindep = ''
+    >>> dsc_maintainer_rfc822 = 'Foo Bar <foo@xxxxxxx>'
+    >>> dsc_standards_version = '2.6.1'
+    >>> dsc_format = '1.0'
+    >>> dsc_binaries = 'pmount'
+    >>> archive = hoary.main_archive
 
 Having proper arguments in hand we can create a new
 ISourcePackageRelease, it will automatically set the
 'upload_distroseries' to the API entry point, in this case Hoary.
 
-  >>> new_spr = hoary.createUploadedSourcePackageRelease(
-  ...     arg_name, version, arg_maintainer,
-  ...     builddepends, builddependsindep, archhintlist, arg_comp, arg_creator,
-  ...     arg_urgency, changelog, changelog_entry, dsc, arg_key, arg_sect,
-  ...     dsc_maintainer_rfc822, dsc_standards_version, dsc_format,
-  ...     dsc_binaries, archive, copyright=copyright,
-  ...     build_conflicts=None, build_conflicts_indep=None,
-  ...     source_package_recipe_build=arg_recipebuild)
-
-  >>> print(new_spr.upload_distroseries.name)
-  hoary
-  >>> print(new_spr.version)
-  0.0.99
-  >>> new_spr.upload_archive.id == hoary.main_archive.id
-  True
-  >>> print(new_spr.copyright)
-  smashed debian/copyright ...
-  >>> new_spr.source_package_recipe_build == arg_recipebuild
-  True
+    >>> new_spr = hoary.createUploadedSourcePackageRelease(
+    ...     arg_name, version, arg_maintainer,
+    ...     builddepends, builddependsindep, archhintlist, arg_comp,
+    ...     arg_creator, arg_urgency, changelog, changelog_entry, dsc,
+    ...     arg_key, arg_sect, dsc_maintainer_rfc822, dsc_standards_version,
+    ...     dsc_format, dsc_binaries, archive, copyright=copyright,
+    ...     build_conflicts=None, build_conflicts_indep=None,
+    ...     source_package_recipe_build=arg_recipebuild)
+
+    >>> print(new_spr.upload_distroseries.name)
+    hoary
+    >>> print(new_spr.version)
+    0.0.99
+    >>> new_spr.upload_archive.id == hoary.main_archive.id
+    True
+    >>> print(new_spr.copyright)
+    smashed debian/copyright ...
+    >>> new_spr.source_package_recipe_build == arg_recipebuild
+    True
 
 Throw away the DB changes:
 
-  >>> transaction.abort()
+    >>> transaction.abort()
 
 Let's get a sample SourcePackageRelease:
 
-   >>> spr_test = SourcePackageRelease.get(20)
-   >>> print(spr_test.name)
-   pmount
+    >>> spr_test = SourcePackageRelease.get(20)
+    >>> print(spr_test.name)
+    pmount
 
 
 Package sizes
diff --git a/lib/lp/soyuz/stories/ppa/xx-ppa-private-teams.txt b/lib/lp/soyuz/stories/ppa/xx-ppa-private-teams.txt
index a010611..47b7b2c 100644
--- a/lib/lp/soyuz/stories/ppa/xx-ppa-private-teams.txt
+++ b/lib/lp/soyuz/stories/ppa/xx-ppa-private-teams.txt
@@ -28,21 +28,21 @@ user/team page.
     ...     auth='Basic celso.providelo@xxxxxxxxxxxxx:test')
     >>> browser.open("http://launchpad.test/~private-team";)
 
-   >>> print_tag_with_id(browser.contents, 'ppas')
-   Personal package archives
-   Create a new PPA
+    >>> print_tag_with_id(browser.contents, 'ppas')
+    Personal package archives
+    Create a new PPA
 
 The form looks almost identical to that for a public team.
 
-   >>> browser.getLink('Create a new PPA').click()
-   >>> print(browser.title)
-   Activate PPA : ...Private Team...
+    >>> browser.getLink('Create a new PPA').click()
+    >>> print(browser.title)
+    Activate PPA : ...Private Team...
 
 There is, however, an extra bit of information indicating the new PPA
 will be private.
 
-   >>> print_tag_with_id(browser.contents, 'ppa-privacy-statement')
-   Since 'Private Team' is a private team this PPA will be private.
+    >>> print_tag_with_id(browser.contents, 'ppa-privacy-statement')
+    Since 'Private Team' is a private team this PPA will be private.
 
 The URL template also shows the private URL.
 
@@ -55,11 +55,12 @@ The URL template also shows the private URL.
     ...
 
 
-   >>> browser.getControl(name='field.displayname').value = "Private Team PPA"
-   >>> browser.getControl(name='field.accepted').value = True
-   >>> browser.getControl("Activate").click()
-   >>> print(browser.title)
-   Private Team PPA : “Private Team” team
+    >>> browser.getControl(name='field.displayname').value = (
+    ...     "Private Team PPA")
+    >>> browser.getControl(name='field.accepted').value = True
+    >>> browser.getControl("Activate").click()
+    >>> print(browser.title)
+    Private Team PPA : “Private Team” team
 
 
 Administrator changes to the PPA
@@ -68,15 +69,15 @@ Administrator changes to the PPA
 An administrator viewing the PPA administration page sees that it is
 marked private.
 
-   >>> admin_browser.open(
-   ...     'http://launchpad.test/~private-team/+archive/ppa/+admin')
-   >>> admin_browser.getControl(name='field.private').value
-   True
+    >>> admin_browser.open(
+    ...     'http://launchpad.test/~private-team/+archive/ppa/+admin')
+    >>> admin_browser.getControl(name='field.private').value
+    True
 
 Attempting to change the PPA to public is thwarted.
 
-   >>> admin_browser.getControl(name='field.private').value = False
-   >>> admin_browser.getControl('Save').click()
-   >>> print_feedback_messages(admin_browser.contents)
-   There is 1 error.
-   Private teams may not have public archives.
+    >>> admin_browser.getControl(name='field.private').value = False
+    >>> admin_browser.getControl('Save').click()
+    >>> print_feedback_messages(admin_browser.contents)
+    There is 1 error.
+    Private teams may not have public archives.
diff --git a/lib/lp/soyuz/stories/soyuz/xx-binarypackagerelease-index.txt b/lib/lp/soyuz/stories/soyuz/xx-binarypackagerelease-index.txt
index a292ca1..4ca2f64 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-binarypackagerelease-index.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-binarypackagerelease-index.txt
@@ -7,62 +7,62 @@ content class/page.
 
 Let's find a build that produced some binaries:
 
-  >>> browser.open("http://launchpad.test/ubuntu/+builds";)
-  >>> browser.getControl(name="build_state").value = ['built']
-  >>> browser.getControl("Filter").click()
-  >>> browser.getLink("Next").click()
-  >>> build_link = browser.getLink(
-  ...     'i386 build of mozilla-firefox 0.9 in ubuntu '
-  ...     'warty RELEASE')
-  >>> build_link.url
-  'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9/+build/2'
+    >>> browser.open("http://launchpad.test/ubuntu/+builds";)
+    >>> browser.getControl(name="build_state").value = ['built']
+    >>> browser.getControl("Filter").click()
+    >>> browser.getLink("Next").click()
+    >>> build_link = browser.getLink(
+    ...     'i386 build of mozilla-firefox 0.9 in ubuntu '
+    ...     'warty RELEASE')
+    >>> build_link.url
+    'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9/+build/2'
 
 Next, we'll manually create a suitable package upload record for our
 build:
 XXX: noodles 2009-01-16 bug 317863: move this into the STP.
 
-  >>> from lp.soyuz.model.queue import PackageUploadBuild
-  >>> from lp.soyuz.interfaces.binarypackagebuild import (
-  ...     IBinaryPackageBuildSet)
-  >>> from lp.registry.interfaces.pocket import (
-  ...     PackagePublishingPocket)
-  >>> from zope.component import getUtility
+    >>> from lp.soyuz.model.queue import PackageUploadBuild
+    >>> from lp.soyuz.interfaces.binarypackagebuild import (
+    ...     IBinaryPackageBuildSet)
+    >>> from lp.registry.interfaces.pocket import (
+    ...     PackagePublishingPocket)
+    >>> from zope.component import getUtility
 
-  >>> login('foo.bar@xxxxxxxxxxxxx')
-  >>> build = getUtility(IBinaryPackageBuildSet).getByID(2)
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> build = getUtility(IBinaryPackageBuildSet).getByID(2)
 
 The sample data doesn't have any Built-Using references.  For now, just
 manually insert one so that we can check how it's rendered.
 
-  >>> from lp.soyuz.enums import BinarySourceReferenceType
-  >>> from lp.soyuz.interfaces.binarysourcereference import (
-  ...     IBinarySourceReferenceSet,
-  ...     )
-  >>> bpr = build.getBinaryPackageRelease('mozilla-firefox')
-  >>> _ = getUtility(IBinarySourceReferenceSet).createFromRelationship(
-  ...     bpr, 'iceweasel (= 1.0)', BinarySourceReferenceType.BUILT_USING)
-
-  >>> package_upload = build.distro_series.createQueueEntry(
-  ...     PackagePublishingPocket.UPDATES, build.archive,
-  ...    'changes.txt', b'my changes')
-  >>> package_upload_build = PackageUploadBuild(
-  ...     packageupload =package_upload,
-  ...     build=build)
-  >>> package_upload.setDone()
-  >>> logout()
-
-  >>> build_link.click()
-  >>> browser.url
-  'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9/+build/2'
+    >>> from lp.soyuz.enums import BinarySourceReferenceType
+    >>> from lp.soyuz.interfaces.binarysourcereference import (
+    ...     IBinarySourceReferenceSet,
+    ...     )
+    >>> bpr = build.getBinaryPackageRelease('mozilla-firefox')
+    >>> _ = getUtility(IBinarySourceReferenceSet).createFromRelationship(
+    ...     bpr, 'iceweasel (= 1.0)', BinarySourceReferenceType.BUILT_USING)
+
+    >>> package_upload = build.distro_series.createQueueEntry(
+    ...     PackagePublishingPocket.UPDATES, build.archive,
+    ...    'changes.txt', b'my changes')
+    >>> package_upload_build = PackageUploadBuild(
+    ...     packageupload =package_upload,
+    ...     build=build)
+    >>> package_upload.setDone()
+    >>> logout()
+
+    >>> build_link.click()
+    >>> browser.url
+    'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9/+build/2'
 
 This build produced one BinaryPackage, called 'mozilla-firefox 0.9',
 which is presented in the right portlet, called 'Resulting Binaries'.
 Let's just check if the page is presented without errors (see bug
 #76163):
 
-  >>> browser.getLink('mozilla-firefox 0.9').click()
-  >>> browser.url
-  'http://launchpad.test/ubuntu/warty/i386/mozilla-firefox/0.9'
+    >>> browser.getLink('mozilla-firefox 0.9').click()
+    >>> browser.url
+    'http://launchpad.test/ubuntu/warty/i386/mozilla-firefox/0.9'
 
 When rendering package relationships only existent packages contain
 links to within LP application, not found packages are rendered as
@@ -71,68 +71,67 @@ simple text.
 'Provides', 'Pre-Depends', 'Enhances' and 'Breaks' sections contain
 links to a binary in the context in question.
 
-  >>> def print_relation(id):
-  ...     section = find_tag_by_id(browser.contents, id)
-  ...     parse_relationship_section(str(section))
+    >>> def print_relation(id):
+    ...     section = find_tag_by_id(browser.contents, id)
+    ...     parse_relationship_section(str(section))
 
-  >>> print_relation('provides')
-  LINK: "mozilla-firefox" -> http://launchpad.test/ubuntu/warty/i386/mozilla-firefox
+    >>> print_relation('provides')
+    LINK: "mozilla-firefox" -> http://launchpad.test/ubuntu/warty/i386/mozilla-firefox
 
-  >>> print_relation('predepends')
-  TEXT: "foo"
-  LINK: "pmount" -> http://launchpad.test/ubuntu/warty/i386/pmount
+    >>> print_relation('predepends')
+    TEXT: "foo"
+    LINK: "pmount" -> http://launchpad.test/ubuntu/warty/i386/pmount
 
-  >>> print_relation('enhances')
-  TEXT: "bar"
-  LINK: "pmount" -> http://launchpad.test/ubuntu/warty/i386/pmount
+    >>> print_relation('enhances')
+    TEXT: "bar"
+    LINK: "pmount" -> http://launchpad.test/ubuntu/warty/i386/pmount
 
-  >>> print_relation('breaks')
-  TEXT: "baz"
-  LINK: "pmount" -> http://launchpad.test/ubuntu/warty/i386/pmount
+    >>> print_relation('breaks')
+    TEXT: "baz"
+    LINK: "pmount" -> http://launchpad.test/ubuntu/warty/i386/pmount
 
 The 'Built-Using' section contains a link to a source in the context in
 question.
 
-  >>> print_relation('builtusing')
-  LINK: "iceweasel (= 1.0)" ->
-  http://launchpad.test/ubuntu/warty/+source/iceweasel
+    >>> print_relation('builtusing')
+    LINK: "iceweasel (= 1.0)" ->
+    http://launchpad.test/ubuntu/warty/+source/iceweasel
 
 
 'Depends', 'Conflicts', 'Replaces', 'Suggests' and 'Recommends'
 sections contain only unsatisfied dependencies, which are rendered as
 text:
 
-  >>> print_relation('depends')
-  TEXT: "gcc-3.4 (>= 3.4.1-4sarge1)"
-  TEXT: "gcc-3.4 (<< 3.4.2)"
-  TEXT: "gcc-3.4-base"
-  TEXT: "libc6 (>= 2.3.2.ds1-4)"
-  TEXT: "libstdc++6-dev (>= 3.4.1-4sarge1)"
+    >>> print_relation('depends')
+    TEXT: "gcc-3.4 (>= 3.4.1-4sarge1)"
+    TEXT: "gcc-3.4 (<< 3.4.2)"
+    TEXT: "gcc-3.4-base"
+    TEXT: "libc6 (>= 2.3.2.ds1-4)"
+    TEXT: "libstdc++6-dev (>= 3.4.1-4sarge1)"
 
-  >>> print_relation('conflicts')
-  TEXT: "firefox"
-  TEXT: "mozilla-web-browser"
+    >>> print_relation('conflicts')
+    TEXT: "firefox"
+    TEXT: "mozilla-web-browser"
 
-  >>> print_relation('suggests')
-  TEXT: "firefox-gnome-support (= 1.0.7-0ubuntu20)"
-  TEXT: "latex-xft-fonts"
-  TEXT: "xprint"
+    >>> print_relation('suggests')
+    TEXT: "firefox-gnome-support (= 1.0.7-0ubuntu20)"
+    TEXT: "latex-xft-fonts"
+    TEXT: "xprint"
 
-  >>> print_relation('replaces')
-  TEXT: "gnome-mozilla-browser"
+    >>> print_relation('replaces')
+    TEXT: "gnome-mozilla-browser"
 
-  >>> print_relation('recommends')
-  TEXT: "gcc-3.4 (>= 3.4.1-4sarge1)"
-  TEXT: "gcc-3.4 (<< 3.4.2)"
-  TEXT: "gcc-3.4-base"
-  TEXT: "libc6 (>= 2.3.2.ds1-4)"
-  TEXT: "libstdc++6-dev (>= 3.4.1-4sarge1)"
+    >>> print_relation('recommends')
+    TEXT: "gcc-3.4 (>= 3.4.1-4sarge1)"
+    TEXT: "gcc-3.4 (<< 3.4.2)"
+    TEXT: "gcc-3.4-base"
+    TEXT: "libc6 (>= 2.3.2.ds1-4)"
+    TEXT: "libstdc++6-dev (>= 3.4.1-4sarge1)"
 
 Even when there is no information to present and the package control
 files don't contain the field, we still present the  corresponding
 relationship section.
 
-  >>> browser.open('http://launchpad.test/ubuntu/warty/i386/pmount/0.1-1')
-  >>> print_relation('predepends')
-  EMPTY SECTION
-
+    >>> browser.open('http://launchpad.test/ubuntu/warty/i386/pmount/0.1-1')
+    >>> print_relation('predepends')
+    EMPTY SECTION
diff --git a/lib/lp/soyuz/stories/soyuz/xx-distribution-add.txt b/lib/lp/soyuz/stories/soyuz/xx-distribution-add.txt
index 92b0331..7805e1f 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-distribution-add.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-distribution-add.txt
@@ -4,44 +4,44 @@ Creating new distributions
 A non launchpad admin doesn't see the link to create a new distribution on 
 the distributions page:
 
-  >>> user_browser.open("http://launchpad.test/distros";)
-  >>> user_browser.getLink("Register a distribution")
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.testbrowser.browser.LinkNotFoundError
+    >>> user_browser.open("http://launchpad.test/distros";)
+    >>> user_browser.getLink("Register a distribution")
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.testbrowser.browser.LinkNotFoundError
 
 A launchpad admin sees the link to create a new distribution:
 
-  >>> admin_browser.open("http://launchpad.test/distros";)
-  >>> admin_browser.getLink("Register a distribution").url
-  'http://launchpad.test/distros/+add'
+    >>> admin_browser.open("http://launchpad.test/distros";)
+    >>> admin_browser.getLink("Register a distribution").url
+    'http://launchpad.test/distros/+add'
 
 A launchpad admin can create a new distribution:
 
-  >>> user_browser.open("http://launchpad.test/distros/+add";)
-  ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
-  Traceback (most recent call last):
-  ...
-  zope.security.interfaces.Unauthorized: ...
+    >>> user_browser.open("http://launchpad.test/distros/+add";)
+    ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+    Traceback (most recent call last):
+    ...
+    zope.security.interfaces.Unauthorized: ...
 
 Create a Test distribution:
 
-  >>> admin_browser.open("http://launchpad.test/distros/+add";)
-  >>> admin_browser.url
-  'http://launchpad.test/distros/+add'
+    >>> admin_browser.open("http://launchpad.test/distros/+add";)
+    >>> admin_browser.url
+    'http://launchpad.test/distros/+add'
 
-  >>> admin_browser.getControl(name="field.name").value = 'test'
-  >>> admin_browser.getControl("Display Name").value = 'Test Distro'
-  >>> admin_browser.getControl("Summary").value = 'Test Distro Summary'
-  >>> admin_browser.getControl("Description").value = 'Test Distro Description'
-  >>> admin_browser.getControl("Web site URL").value = 'foo.com'
-  >>> admin_browser.getControl("Members").value = 'mark'
+    >>> admin_browser.getControl(name="field.name").value = 'test'
+    >>> admin_browser.getControl("Display Name").value = 'Test Distro'
+    >>> admin_browser.getControl("Summary").value = 'Test Distro Summary'
+    >>> admin_browser.getControl("Description").value = (
+    ...     'Test Distro Description')
+    >>> admin_browser.getControl("Web site URL").value = 'foo.com'
+    >>> admin_browser.getControl("Members").value = 'mark'
 
-  >>> admin_browser.getControl("Save").click()
-  >>> admin_browser.url
-  'http://launchpad.test/test'
-
-  >>> admin_browser.contents
-  '...Test Distro...'
+    >>> admin_browser.getControl("Save").click()
+    >>> admin_browser.url
+    'http://launchpad.test/test'
 
+    >>> admin_browser.contents
+    '...Test Distro...'
diff --git a/lib/lp/soyuz/stories/soyuz/xx-distribution-edit.txt b/lib/lp/soyuz/stories/soyuz/xx-distribution-edit.txt
index f9e808e..f85a5c9 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-distribution-edit.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-distribution-edit.txt
@@ -3,26 +3,26 @@ Editing distributions
 
 Change some details of the Ubuntu distribution that were incorrect.
 
-  >>> admin_browser.open("http://launchpad.test/ubuntu";)
-  >>> admin_browser.getLink("Change details").click()
-  >>> admin_browser.url
-  'http://launchpad.test/ubuntu/+edit'
+    >>> admin_browser.open("http://launchpad.test/ubuntu";)
+    >>> admin_browser.getLink("Change details").click()
+    >>> admin_browser.url
+    'http://launchpad.test/ubuntu/+edit'
 
-  >>> admin_browser.getControl("Display Name").value
-  'Ubuntu'
-  >>> admin_browser.getControl("Display Name").value = 'Test Distro'
+    >>> admin_browser.getControl("Display Name").value
+    'Ubuntu'
+    >>> admin_browser.getControl("Display Name").value = 'Test Distro'
 
-  >>> admin_browser.getControl("Summary").value = 'Test Distro Summary'
-  >>> admin_browser.getControl("Description").value = 'Test Distro Description'
+    >>> admin_browser.getControl("Summary").value = 'Test Distro Summary'
+    >>> admin_browser.getControl("Description").value = (
+    ...     'Test Distro Description')
 
-  >>> admin_browser.getControl("Change", index=3).click()
-  >>> admin_browser.url
-  'http://launchpad.test/ubuntu'
+    >>> admin_browser.getControl("Change", index=3).click()
+    >>> admin_browser.url
+    'http://launchpad.test/ubuntu'
 
 The changed values can be seen on the distribution's +edit page.
 
-  >>> admin_browser.getLink("Change details").click()
-
-  >>> admin_browser.getControl("Display Name").value
-  'Test Distro'
+    >>> admin_browser.getLink("Change details").click()
 
+    >>> admin_browser.getControl("Display Name").value
+    'Test Distro'
diff --git a/lib/lp/soyuz/stories/soyuz/xx-distro-distros-index.txt b/lib/lp/soyuz/stories/soyuz/xx-distro-distros-index.txt
index c355d93..8a00a19 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-distro-distros-index.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-distro-distros-index.txt
@@ -1,13 +1,12 @@
 Check if the distros root page is not broken.
 In this page we can see all the Distributions in Launchpad.
 
-  >>> browser.open("http://localhost/distros";)
-  >>> browser.contents
-  '...Distributions...'
+    >>> browser.open("http://localhost/distros";)
+    >>> browser.contents
+    '...Distributions...'
 
-  >>> browser.getLink("Kubuntu").click()
-  >>> browser.url
-  'http://localhost/kubuntu'
-  >>> browser.contents
-  '...Kubuntu...'
- 
+    >>> browser.getLink("Kubuntu").click()
+    >>> browser.url
+    'http://localhost/kubuntu'
+    >>> browser.contents
+    '...Kubuntu...'
diff --git a/lib/lp/soyuz/stories/soyuz/xx-distroseries-index.txt b/lib/lp/soyuz/stories/soyuz/xx-distroseries-index.txt
index ca0c94a..f3b2cdb 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-distroseries-index.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-distroseries-index.txt
@@ -34,28 +34,28 @@ Each entry contains:
  * age, as approximateduration representation of the time passed since
    the upload was done.
 
-  >>> anon_browser.open(
-  ...     "http://launchpad.test/ubuntu/warty/+portlet-latestuploads";)
-  >>> latest_uploads = str(find_tag_by_id(anon_browser.contents,
-  ...                      "latest-uploads"))
-  >>> 'mozilla-firefox 0.9' in latest_uploads
-  True
-  >>> 'Mark Shuttleworth' in latest_uploads
-  True
+    >>> anon_browser.open(
+    ...     "http://launchpad.test/ubuntu/warty/+portlet-latestuploads";)
+    >>> latest_uploads = str(find_tag_by_id(anon_browser.contents,
+    ...                      "latest-uploads"))
+    >>> 'mozilla-firefox 0.9' in latest_uploads
+    True
+    >>> 'Mark Shuttleworth' in latest_uploads
+    True
 
 The link presented points to the SourcePackageRelease inside the
 Distribution in question (a IDSPR), we can check for consistency
 clicking on it:
 
-  >>> anon_browser.getLink("mozilla-firefox 0.9").click()
-  >>> anon_browser.url
-  'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9'
+    >>> anon_browser.getLink("mozilla-firefox 0.9").click()
+    >>> anon_browser.url
+    'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9'
 
 Empty results are also presented properly (even if they are quite rare
 in production environment):
 
-  >>> anon_browser.open(
-  ...     "http://launchpad.test/ubuntutest/breezy-autotest/";
-  ...     "+portlet-latestuploads")
-  >>> find_tag_by_id(anon_browser.contents, 'no-latest-uploads') is not None
-  True
+    >>> anon_browser.open(
+    ...     "http://launchpad.test/ubuntutest/breezy-autotest/";
+    ...     "+portlet-latestuploads")
+    >>> find_tag_by_id(anon_browser.contents, 'no-latest-uploads') is not None
+    True
diff --git a/lib/lp/soyuz/stories/soyuz/xx-distroseries-sources.txt b/lib/lp/soyuz/stories/soyuz/xx-distroseries-sources.txt
index 05f49cf..2881c1a 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-distroseries-sources.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-distroseries-sources.txt
@@ -26,42 +26,42 @@ published packages. We will do the last:
 
 Starting from distribution page:
 
-  >>> browser.open('http://launchpad.test/ubuntu')
+    >>> browser.open('http://launchpad.test/ubuntu')
 
 Search for mozilla in the packages:
 
-  >>> browser.getControl(name='text').value = 'mozilla'
-  >>> browser.getControl("Find a Package").click()
+    >>> browser.getControl(name='text').value = 'mozilla'
+    >>> browser.getControl("Find a Package").click()
 
 Let's have a look at the firefox DistributionSourcePackage.
 
-  >>> browser.getLink("mozilla-firefox").click()
-  >>> print(browser.url)
-  http://launchpad.test/ubuntu/+source/mozilla-firefox
+    >>> browser.getLink("mozilla-firefox").click()
+    >>> print(browser.url)
+    http://launchpad.test/ubuntu/+source/mozilla-firefox
 
 Click the "See full publishing history" link to see specific information
 about Firefox's publishing history.
 
-  >>> browser.getLink("View full publishing history").click()
-  >>> table = find_tag_by_id(browser.contents, 'publishing-summary')
-  >>> print(extract_text(table))
-  Date                      Status     Target   Pocket   Component   Section   Version
-  2006-02-13 12:19:00 UTC   Published  Warty    release  main        web       0.9
-  Published on 2006-02-13
-  2004-09-27 11:57:13 UTC   Pending    Warty   release   main        editors   0.9
-  >>> print(table.find_all("tr")[2].td["colspan"])
-  8
+    >>> browser.getLink("View full publishing history").click()
+    >>> table = find_tag_by_id(browser.contents, 'publishing-summary')
+    >>> print(extract_text(table))
+    Date                      Status     Target   Pocket   Component   Section   Version
+    2006-02-13 12:19:00 UTC   Published  Warty    release  main        web       0.9
+    Published on 2006-02-13
+    2004-09-27 11:57:13 UTC   Pending    Warty   release   main        editors   0.9
+    >>> print(table.find_all("tr")[2].td["colspan"])
+    8
 
 Jump back to the DistributionSourcePackage page to continue the tests:
 
-  >>> browser.open('http://launchpad.test/ubuntu/+source/mozilla-firefox')
+    >>> browser.open('http://launchpad.test/ubuntu/+source/mozilla-firefox')
 
 By clicking in the 'target' distroseries we will get to the
 SourcePackage page:
 
-  >>> browser.getLink("The Warty Warthog Release").click()
-  >>> browser.url
-  'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox'
+    >>> browser.getLink("The Warty Warthog Release").click()
+    >>> browser.url
+    'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox'
 
 Any user can see the package summary.
 
@@ -83,40 +83,40 @@ We can see 'mozilla-firefox' is published once in pocket RELEASE:
 The user can also download the files for the "currentrelease" (last
 published version) if they are available:
 
-  >>> print(extract_text(find_portlet(
-  ...     content, 'Download files from current release (0.9)')))
-  Download files from current release (0.9)
-  File Size SHA-256 Checksum
-  firefox_0.9.2.orig.tar.gz 9.5 MiB ...
+    >>> print(extract_text(find_portlet(
+    ...     content, 'Download files from current release (0.9)')))
+    Download files from current release (0.9)
+    File Size SHA-256 Checksum
+    firefox_0.9.2.orig.tar.gz 9.5 MiB ...
 
-  >>> print(browser.getLink("firefox_0.9.2.orig.tar.gz").url)
-  http://launchpad.test/ubuntu/+archive/primary/+sourcefiles/mozilla-firefox/0.9/firefox_0.9.2.orig.tar.gz
+    >>> print(browser.getLink("firefox_0.9.2.orig.tar.gz").url)
+    http://launchpad.test/ubuntu/+archive/primary/+sourcefiles/mozilla-firefox/0.9/firefox_0.9.2.orig.tar.gz
 
 This page also provides links to the binary packages generated by this
 source in a specfic architecture:
 
-  >>> print(extract_text(find_tag_by_id(content, 'binaries')))
-  mozilla-firefox
-  (hppa)
-  (i386)
-  mozilla-firefox-data
-  (hppa)
-  (i386)
+    >>> print(extract_text(find_tag_by_id(content, 'binaries')))
+    mozilla-firefox
+    (hppa)
+    (i386)
+    mozilla-firefox-data
+    (hppa)
+    (i386)
 
 Let's check the link to the binary package built on i386 architecture,
 a DistroArchSeriesBinaryPackage:
 
-  >>> print(browser.getLink("i386").url)
-  http://launchpad.test/ubuntu/warty/i386/mozilla-firefox
+    >>> print(browser.getLink("i386").url)
+    http://launchpad.test/ubuntu/warty/i386/mozilla-firefox
 
 More information about this page can be found at
 17-distroarchseries-binpackages.txt.
 
 Move back to the SourcePackage page to continue the tests:
 
-  >>> browser.open(
-  ...     'http://launchpad.test/ubuntu/breezy-autotest/+source/'
-  ...     'commercialpackage')
+    >>> browser.open(
+    ...     'http://launchpad.test/ubuntu/breezy-autotest/+source/'
+    ...     'commercialpackage')
 
 PackageRelationships, 'builddepends', 'builddependsindep', 'builddependsarch',
 'build_conflicts', 'build_conflicts_indep', and 'build_conflicts_arch' for the
@@ -125,72 +125,74 @@ source in question are provided in this page.
 Even when the relationship section is empty they are presented,
 keeping the page format constant.
 
-  >>> depends_section = find_tag_by_id(browser.contents, 'depends')
-  >>> parse_relationship_section(str(depends_section))
-  EMPTY SECTION
+    >>> depends_section = find_tag_by_id(browser.contents, 'depends')
+    >>> parse_relationship_section(str(depends_section))
+    EMPTY SECTION
 
-  >>> dependsindep_section = find_tag_by_id(browser.contents, 'dependsindep')
-  >>> parse_relationship_section(str(dependsindep_section))
-  EMPTY SECTION
+    >>> dependsindep_section = find_tag_by_id(
+    ...     browser.contents, 'dependsindep')
+    >>> parse_relationship_section(str(dependsindep_section))
+    EMPTY SECTION
 
-  >>> dependsarch_section = find_tag_by_id(browser.contents, 'dependsarch')
-  >>> parse_relationship_section(str(dependsarch_section))
-  EMPTY SECTION
+    >>> dependsarch_section = find_tag_by_id(browser.contents, 'dependsarch')
+    >>> parse_relationship_section(str(dependsarch_section))
+    EMPTY SECTION
 
-  >>> conflicts_section = find_tag_by_id(browser.contents, 'conflicts')
-  >>> parse_relationship_section(str(conflicts_section))
-  EMPTY SECTION
+    >>> conflicts_section = find_tag_by_id(browser.contents, 'conflicts')
+    >>> parse_relationship_section(str(conflicts_section))
+    EMPTY SECTION
 
-  >>> conflictsindep_section = find_tag_by_id(
-  ...     browser.contents, 'conflictsindep')
-  >>> parse_relationship_section(str(conflictsindep_section))
-  EMPTY SECTION
+    >>> conflictsindep_section = find_tag_by_id(
+    ...     browser.contents, 'conflictsindep')
+    >>> parse_relationship_section(str(conflictsindep_section))
+    EMPTY SECTION
 
-  >>> conflictsarch_section = find_tag_by_id(
-  ...     browser.contents, 'conflictsarch')
-  >>> parse_relationship_section(str(conflictsarch_section))
-  EMPTY SECTION
+    >>> conflictsarch_section = find_tag_by_id(
+    ...     browser.contents, 'conflictsarch')
+    >>> parse_relationship_section(str(conflictsarch_section))
+    EMPTY SECTION
 
 Let's inspect a page with non-empty relationships.
 
-  >>> browser.open(
-  ...     'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox')
-
-  >>> depends_section = find_tag_by_id(browser.contents, 'depends')
-  >>> parse_relationship_section(str(depends_section))
-  TEXT: "gcc-3.4 (>= 3.4.1-4sarge1)"
-  TEXT: "gcc-3.4 (<< 3.4.2)"
-  TEXT: "gcc-3.4-base"
-  TEXT: "libc6 (>= 2.3.2.ds1-4)"
-  TEXT: "libstdc++6-dev (>= 3.4.1-4sarge1)"
-  LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
-
-  >>> dependsindep_section = find_tag_by_id(browser.contents, 'dependsindep')
-  >>> parse_relationship_section(str(dependsindep_section))
-  TEXT: "bacula-common (= 1.34.6-2)"
-  TEXT: "bacula-director-common (= 1.34.6-2)"
-  LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
-  TEXT: "postgresql-client (>= 7.4)"
-
-  >>> dependsarch_section = find_tag_by_id(browser.contents, 'dependsarch')
-  >>> parse_relationship_section(str(dependsarch_section))
-  EMPTY SECTION
-
-  >>> conflicts_section = find_tag_by_id(browser.contents, 'conflicts')
-  >>> parse_relationship_section(str(conflicts_section))
-  TEXT: "gcc-4.0"
-  LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
-
-  >>> conflictsindep_section = find_tag_by_id(
-  ...     browser.contents, 'conflictsindep')
-  >>> parse_relationship_section(str(conflictsindep_section))
-  TEXT: "gcc-4.0-base"
-  LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
-
-  >>> conflictsarch_section = find_tag_by_id(
-  ...     browser.contents, 'conflictsarch')
-  >>> parse_relationship_section(str(conflictsarch_section))
-  EMPTY SECTION
+    >>> browser.open(
+    ...     'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox')
+
+    >>> depends_section = find_tag_by_id(browser.contents, 'depends')
+    >>> parse_relationship_section(str(depends_section))
+    TEXT: "gcc-3.4 (>= 3.4.1-4sarge1)"
+    TEXT: "gcc-3.4 (<< 3.4.2)"
+    TEXT: "gcc-3.4-base"
+    TEXT: "libc6 (>= 2.3.2.ds1-4)"
+    TEXT: "libstdc++6-dev (>= 3.4.1-4sarge1)"
+    LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
+
+    >>> dependsindep_section = find_tag_by_id(
+    ...     browser.contents, 'dependsindep')
+    >>> parse_relationship_section(str(dependsindep_section))
+    TEXT: "bacula-common (= 1.34.6-2)"
+    TEXT: "bacula-director-common (= 1.34.6-2)"
+    LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
+    TEXT: "postgresql-client (>= 7.4)"
+
+    >>> dependsarch_section = find_tag_by_id(browser.contents, 'dependsarch')
+    >>> parse_relationship_section(str(dependsarch_section))
+    EMPTY SECTION
+
+    >>> conflicts_section = find_tag_by_id(browser.contents, 'conflicts')
+    >>> parse_relationship_section(str(conflicts_section))
+    TEXT: "gcc-4.0"
+    LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
+
+    >>> conflictsindep_section = find_tag_by_id(
+    ...     browser.contents, 'conflictsindep')
+    >>> parse_relationship_section(str(conflictsindep_section))
+    TEXT: "gcc-4.0-base"
+    LINK: "pmount" -> http://launchpad.test/ubuntu/warty/+package/pmount
+
+    >>> conflictsarch_section = find_tag_by_id(
+    ...     browser.contents, 'conflictsarch')
+    >>> parse_relationship_section(str(conflictsarch_section))
+    EMPTY SECTION
 
 
 The '+changelog' page provides an aggregation of the changelogs for
@@ -208,19 +210,19 @@ The text is generated automatically by appending:
 
 for each published version.
 
-  >>> browser.getLink("View changelog").click()
-  >>> browser.url
-  'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox/+changelog'
+    >>> browser.getLink("View changelog").click()
+    >>> browser.url
+    'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox/+changelog'
 
-  >>> tag = find_tag_by_id(browser.contents, 'mozilla-firefox_0.9')
-  >>> print(extract_text(tag))
-  Mozilla dummy Changelog......
+    >>> tag = find_tag_by_id(browser.contents, 'mozilla-firefox_0.9')
+    >>> print(extract_text(tag))
+    Mozilla dummy Changelog......
 
 
 Back to the SourcePackage page:
 
-  >>> browser.open(
-  ...     'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox')
+    >>> browser.open(
+    ...     'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox')
 
 Any user can see the copyright for the most recent source package release.
 
@@ -253,72 +255,72 @@ Any user can see the copyright for the most recent source package release.
 We can visit a specific published release of "mozilla-firefox", this
 page is provided by an DistributionSourcePackageRelease instance:
 
-  >>> browser.getLink("mozilla-firefox 0.9").click()
-  >>> browser.url
-  'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9'
+    >>> browser.getLink("mozilla-firefox 0.9").click()
+    >>> browser.url
+    'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9'
 
 The deprecated DistroSeriesSourcePackageRelease page redirects to the
 same place.
 
-  >>> browser.open(
-  ...     'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox/0.9')
-  >>> browser.url
-  'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9'
+    >>> browser.open(
+    ...     'http://launchpad.test/ubuntu/warty/+source/mozilla-firefox/0.9')
+    >>> browser.url
+    'http://launchpad.test/ubuntu/+source/mozilla-firefox/0.9'
 
 There we can see the respective 'changelog' content for this version:
 
-  >>> tag = find_tag_by_id(browser.contents, 'mozilla-firefox_0.9')
-  >>> print(extract_text(tag))
-  Mozilla dummy Changelog......
+    >>> tag = find_tag_by_id(browser.contents, 'mozilla-firefox_0.9')
+    >>> print(extract_text(tag))
+    Mozilla dummy Changelog......
 
 With the possibility to download the entire changesfile (if available):
 
-  >>> print(browser.getLink('View changes file').url)
-  http://.../52/mozilla-firefox_0.9_i386.changes
+    >>> print(browser.getLink('View changes file').url)
+    http://.../52/mozilla-firefox_0.9_i386.changes
 
 And also download the files contained in this source, like '.orig',
 '.diff' and the DSC:
 
-  >>> print(extract_text(find_portlet(browser.contents, 'Downloads')))
-  Downloads
-  File Size SHA-256 Checksum
-  firefox_0.9.2.orig.tar.gz 9.5 MiB ...
+    >>> print(extract_text(find_portlet(browser.contents, 'Downloads')))
+    Downloads
+    File Size SHA-256 Checksum
+    firefox_0.9.2.orig.tar.gz 9.5 MiB ...
 
-  >>> print(browser.getLink("firefox_0.9.2.orig.tar.gz").url)
-  http://launchpad.test/ubuntu/+archive/primary/+sourcefiles/mozilla-firefox/0.9/firefox_0.9.2.orig.tar.gz
+    >>> print(browser.getLink("firefox_0.9.2.orig.tar.gz").url)
+    http://launchpad.test/ubuntu/+archive/primary/+sourcefiles/mozilla-firefox/0.9/firefox_0.9.2.orig.tar.gz
 
 If we go to the same page for alsa-utils, the changelog has text that is
 linkified.
 
-  >>> browser.open(
-  ...  'http://launchpad.test/ubuntu/+source/alsa-utils/1.0.9a-4ubuntu1')
+    >>> browser.open(
+    ...  'http://launchpad.test/ubuntu/+source/alsa-utils/1.0.9a-4ubuntu1')
 
 This changelog has got text of the form 'LP: #nnn' where nnn is a bug number,
 and this is linkified so that when clicked it takes us to the bug page:
 
-  >>> browser.getLink('#10').url
-  'http://launchpad.test/bugs/10'
+    >>> browser.getLink('#10').url
+    'http://launchpad.test/bugs/10'
 
 The same page for commercialpackage has an email address in the
 changelog that is recognised in Launchpad.  It is linkified to point at
 the profile page for that person:
 
-  >>> user_browser.open(
-  ...     "http://launchpad.test/ubuntu/+source/commercialpackage/1.0-1";)
-  >>> print(user_browser.getLink('foo.bar@xxxxxxxxxxxxx').url)
-  http://launchpad.test/~name16
+    >>> user_browser.open(
+    ...     "http://launchpad.test/ubuntu/+source/commercialpackage/1.0-1";)
+    >>> print(user_browser.getLink('foo.bar@xxxxxxxxxxxxx').url)
+    http://launchpad.test/~name16
 
 Let's check how the page behaves if we no files are present:
 
-  >>> browser.open(
-  ...     'http://launchpad.test/ubuntu/+source/cnews/cr.g7-37')
+    >>> browser.open(
+    ...     'http://launchpad.test/ubuntu/+source/cnews/cr.g7-37')
 
 The Downloads portlet indicates that no files are available.
 
-  >>> print(extract_text(find_portlet(browser.contents, 'Downloads')))
-  Downloads
-  No files available for download.
-  No changes file available.
+    >>> print(extract_text(find_portlet(browser.contents, 'Downloads')))
+    Downloads
+    No files available for download.
+    No changes file available.
 
 
 DistroSeries Partner Source Package Pages
diff --git a/lib/lp/soyuz/stories/webservice/xx-person-createppa.txt b/lib/lp/soyuz/stories/webservice/xx-person-createppa.txt
index 0f17592..e7aa8cb 100644
--- a/lib/lp/soyuz/stories/webservice/xx-person-createppa.txt
+++ b/lib/lp/soyuz/stories/webservice/xx-person-createppa.txt
@@ -1,118 +1,118 @@
 Creating a PPA
 ==============
 
-  >>> from zope.component import getUtility
-  >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-  >>> from lp.registry.interfaces.distribution import IDistributionSet
-  >>> from lp.testing import celebrity_logged_in
-  >>> from lp.testing.sampledata import ADMIN_EMAIL
-  >>> login(ADMIN_EMAIL)
-  >>> getUtility(IDistributionSet)['ubuntutest'].supports_ppas = True
-  >>> owner = factory.makePerson()
-  >>> url = "/~%s" % owner.name
-  >>> logout()
-  >>> ppa_owner = webservice.get(url).jsonBody()
-
-  >>> from lp.testing.pages import webservice_for_person
-  >>> from lp.services.webapp.interfaces import OAuthPermission
-  >>> ppa_owner_webservice = webservice_for_person(
-  ...     owner, permission=OAuthPermission.WRITE_PRIVATE)
-
-  >>> print(ppa_owner_webservice.named_post(
-  ...     ppa_owner['self_link'], 'createPPA', {}, distribution='/ubuntu',
-  ...     name='yay', displayname='My shiny new PPA',
-  ...     description='Shinyness!'))
-  HTTP/1.1 201 Created
-  ...
-  Location: http://api.launchpad.test/.../+archive/ubuntu/yay
-  ...
-
-  >>> print(ppa_owner_webservice.named_post(
-  ...     ppa_owner['self_link'], 'createPPA', {}, name='ubuntu',
-  ...     displayname='My shiny new PPA', description='Shinyness!',
-  ...     ))
-  HTTP/1.1 400 Bad Request
-  ...
-  A PPA cannot have the same name as its distribution.
+    >>> from zope.component import getUtility
+    >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> from lp.testing import celebrity_logged_in
+    >>> from lp.testing.sampledata import ADMIN_EMAIL
+    >>> login(ADMIN_EMAIL)
+    >>> getUtility(IDistributionSet)['ubuntutest'].supports_ppas = True
+    >>> owner = factory.makePerson()
+    >>> url = "/~%s" % owner.name
+    >>> logout()
+    >>> ppa_owner = webservice.get(url).jsonBody()
+
+    >>> from lp.testing.pages import webservice_for_person
+    >>> from lp.services.webapp.interfaces import OAuthPermission
+    >>> ppa_owner_webservice = webservice_for_person(
+    ...     owner, permission=OAuthPermission.WRITE_PRIVATE)
+
+    >>> print(ppa_owner_webservice.named_post(
+    ...     ppa_owner['self_link'], 'createPPA', {}, distribution='/ubuntu',
+    ...     name='yay', displayname='My shiny new PPA',
+    ...     description='Shinyness!'))
+    HTTP/1.1 201 Created
+    ...
+    Location: http://api.launchpad.test/.../+archive/ubuntu/yay
+    ...
+
+    >>> print(ppa_owner_webservice.named_post(
+    ...     ppa_owner['self_link'], 'createPPA', {}, name='ubuntu',
+    ...     displayname='My shiny new PPA', description='Shinyness!',
+    ...     ))
+    HTTP/1.1 400 Bad Request
+    ...
+    A PPA cannot have the same name as its distribution.
 
 Creating private PPAs
 ---------------------
 
 Our PPA owner now has a single PPA.
 
-  >>> print_self_link_of_entries(webservice.get(
-  ...     ppa_owner['ppas_collection_link']).jsonBody())
-  http://api.launchpad.test/beta/~.../+archive/ubuntu/yay
+    >>> print_self_link_of_entries(webservice.get(
+    ...     ppa_owner['ppas_collection_link']).jsonBody())
+    http://api.launchpad.test/beta/~.../+archive/ubuntu/yay
 
 They aren't a commercial admin, so they cannot create private PPAs.
 
-  >>> print(ppa_owner_webservice.named_post(
-  ...     ppa_owner['self_link'], 'createPPA', {}, name='whatever',
-  ...     displayname='My secret new PPA', description='Secretness!',
-  ...     private=True,
-  ...     ))
-  HTTP/1.1 400 Bad Request
-  ...
-  ... is not allowed to make private PPAs
+    >>> print(ppa_owner_webservice.named_post(
+    ...     ppa_owner['self_link'], 'createPPA', {}, name='whatever',
+    ...     displayname='My secret new PPA', description='Secretness!',
+    ...     private=True,
+    ...     ))
+    HTTP/1.1 400 Bad Request
+    ...
+    ... is not allowed to make private PPAs
 
 After attempting and failing to create a private PPA, they still have the same
 single PPA they had at the beginning:
 
-  >>> print_self_link_of_entries(webservice.get(
-  ...     ppa_owner['ppas_collection_link']).jsonBody())
-  http://api.launchpad.test/beta/~.../+archive/ubuntu/yay
+    >>> print_self_link_of_entries(webservice.get(
+    ...     ppa_owner['ppas_collection_link']).jsonBody())
+    http://api.launchpad.test/beta/~.../+archive/ubuntu/yay
 
 However, we can grant them commercial admin access:
 
-  >>> with celebrity_logged_in('admin'):
-  ...     comm = getUtility(ILaunchpadCelebrities).commercial_admin
-  ...     comm.addMember(owner, comm.teamowner)
-  (True, <DBItem TeamMembershipStatus.APPROVED, (2) Approved>)
+    >>> with celebrity_logged_in('admin'):
+    ...     comm = getUtility(ILaunchpadCelebrities).commercial_admin
+    ...     comm.addMember(owner, comm.teamowner)
+    (True, <DBItem TeamMembershipStatus.APPROVED, (2) Approved>)
 
 Once they have commercial access, they can create private PPAs:
 
-  >>> print(ppa_owner_webservice.named_post(
-  ...     ppa_owner['self_link'], 'createPPA', {}, name='secret',
-  ...     displayname='My secret new PPA', description='Secretness!',
-  ...     private=True,
-  ...     ))
-  HTTP/1.1 201 Created
-  ...
-  Location: http://api.launchpad.test/.../+archive/ubuntu/secret
-  ...
+    >>> print(ppa_owner_webservice.named_post(
+    ...     ppa_owner['self_link'], 'createPPA', {}, name='secret',
+    ...     displayname='My secret new PPA', description='Secretness!',
+    ...     private=True,
+    ...     ))
+    HTTP/1.1 201 Created
+    ...
+    Location: http://api.launchpad.test/.../+archive/ubuntu/secret
+    ...
 
 And the PPA appears in their list of PPAs:
 
-  >>> print_self_link_of_entries(webservice.get(
-  ...     ppa_owner['ppas_collection_link']).jsonBody())
-  http://api.launchpad.test/.../+archive/ubuntu/secret
-  http://api.launchpad.test/.../+archive/ubuntu/yay
+    >>> print_self_link_of_entries(webservice.get(
+    ...     ppa_owner['ppas_collection_link']).jsonBody())
+    http://api.launchpad.test/.../+archive/ubuntu/secret
+    http://api.launchpad.test/.../+archive/ubuntu/yay
 
 And the PPA is, of course, private:
 
-  >>> ppa = ppa_owner_webservice.named_get(
-  ...     ppa_owner['self_link'], 'getPPAByName', name='secret').jsonBody()
-  >>> ppa['private']
-  True
+    >>> ppa = ppa_owner_webservice.named_get(
+    ...     ppa_owner['self_link'], 'getPPAByName', name='secret').jsonBody()
+    >>> ppa['private']
+    True
 
 It's possible to create PPAs for all sorts of distributions.
 
-  >>> print(ppa_owner_webservice.named_post(
-  ...     ppa_owner['self_link'], 'createPPA', {}, distribution='/ubuntutest',
-  ...     name='ppa'))
-  HTTP/1.1 201 Created
-  ...
-  Location: http://api.launchpad.test/.../+archive/ubuntutest/ppa
-  ...
+    >>> print(ppa_owner_webservice.named_post(
+    ...     ppa_owner['self_link'], 'createPPA', {}, distribution='/ubuntutest',
+    ...     name='ppa'))
+    HTTP/1.1 201 Created
+    ...
+    Location: http://api.launchpad.test/.../+archive/ubuntutest/ppa
+    ...
 
 But not for distributions that don't have PPAs enabled.
 
-  >>> print(ppa_owner_webservice.named_post(
-  ...     ppa_owner['self_link'], 'createPPA', {}, distribution='/redhat',
-  ...     name='ppa'))
-  HTTP/1.1 400 Bad Request
-  ...
-  Red Hat does not support PPAs.
+    >>> print(ppa_owner_webservice.named_post(
+    ...     ppa_owner['self_link'], 'createPPA', {}, distribution='/redhat',
+    ...     name='ppa'))
+    HTTP/1.1 400 Bad Request
+    ...
+    Red Hat does not support PPAs.
 
 
 Defaults
@@ -122,9 +122,9 @@ createPPA's distribution and name arguments were added years after the
 method, so they remain optional and default to Ubuntu and "ppa"
 respectively.
 
-  >>> print(ppa_owner_webservice.named_post(
-  ...     ppa_owner['self_link'], 'createPPA', {}))
-  HTTP/1.1 201 Created
-  ...
-  Location: http://api.launchpad.test/.../+archive/ubuntu/ppa
-  ...
+    >>> print(ppa_owner_webservice.named_post(
+    ...     ppa_owner['self_link'], 'createPPA', {}))
+    HTTP/1.1 201 Created
+    ...
+    Location: http://api.launchpad.test/.../+archive/ubuntu/ppa
+    ...
diff --git a/lib/lp/testing/doc/sample-data-assertions.txt b/lib/lp/testing/doc/sample-data-assertions.txt
index e2ac29a..90164e7 100644
--- a/lib/lp/testing/doc/sample-data-assertions.txt
+++ b/lib/lp/testing/doc/sample-data-assertions.txt
@@ -12,9 +12,9 @@ AT LEAST run this test to see that you haven't broken any assumptions.
 User Accounts and Teams
 -----------------------
 
-  >>> from zope.component import getUtility
-  >>> from lp.registry.interfaces.person import IPersonSet
-  >>> personset = getUtility(IPersonSet)
+    >>> from zope.component import getUtility
+    >>> from lp.registry.interfaces.person import IPersonSet
+    >>> personset = getUtility(IPersonSet)
 
 Here we make assertions about each of the key user accounts which should be
 used in Launchpad page tests. These should be the ONLY user accounts
@@ -23,13 +23,13 @@ specifically referenced in Launchpad tests.
 * No Team Memberships
   This user is not supposed to be a member of any teams.
 
-  >>> no_team_memberships = personset.getByName('no-team-memberships')
-  >>> no_team_memberships.team_memberships.count()
-  0
+    >>> no_team_memberships = personset.getByName('no-team-memberships')
+    >>> no_team_memberships.team_memberships.count()
+    0
 
 * One Team Membership
   This user is supposed to be a member of only one team, the "Simple Team".
 
-  >>> one_membership = personset.getByName('one-membership')
-  >>> for t in one_membership.team_memberships: print(t.team.displayname)
-  Simple Team
+    >>> one_membership = personset.getByName('one-membership')
+    >>> for t in one_membership.team_memberships: print(t.team.displayname)
+    Simple Team
diff --git a/lib/lp/translations/doc/pomsgid.txt b/lib/lp/translations/doc/pomsgid.txt
index c53e535..71beb53 100644
--- a/lib/lp/translations/doc/pomsgid.txt
+++ b/lib/lp/translations/doc/pomsgid.txt
@@ -3,13 +3,13 @@ POMsgID.getByMsgid()
 
 Test that getByMsgid is working:
 
->>> from lp.translations.model.pomsgid import POMsgID
->>> created = POMsgID.new("This is a launchpad test")
->>> got = POMsgID.getByMsgid("This is a launchpad test")
->>> got == created
-True
+    >>> from lp.translations.model.pomsgid import POMsgID
+    >>> created = POMsgID.new("This is a launchpad test")
+    >>> got = POMsgID.getByMsgid("This is a launchpad test")
+    >>> got == created
+    True
 
->>> created = POMsgID.new("This is a very \t\n\b'?'\\ odd test")
->>> got = POMsgID.getByMsgid("This is a very \t\n\b'?'\\ odd test")
->>> got == created
-True
+    >>> created = POMsgID.new("This is a very \t\n\b'?'\\ odd test")
+    >>> got = POMsgID.getByMsgid("This is a very \t\n\b'?'\\ odd test")
+    >>> got == created
+    True
diff --git a/lib/lp/translations/stories/importqueue/xx-translation-import-queue-edit-autofilling.txt b/lib/lp/translations/stories/importqueue/xx-translation-import-queue-edit-autofilling.txt
index f8387f1..061165f 100644
--- a/lib/lp/translations/stories/importqueue/xx-translation-import-queue-edit-autofilling.txt
+++ b/lib/lp/translations/stories/importqueue/xx-translation-import-queue-edit-autofilling.txt
@@ -3,96 +3,104 @@ of the Translation import queue reviewers.
 
 First, we need to feed the import queue.
 
-  >>> import lp.translations
-  >>> import os.path
-  >>> test_file_name = os.path.join(
-  ...     os.path.dirname(lp.translations.__file__),
-  ...     'stories/importqueue/xx-translation-import-queue-edit-autofilling.tar.gz')
-  >>> tarball = open(test_file_name, 'rb')
-
-  >>> browser = setupBrowser(auth='Basic carlos@xxxxxxxxxxxxx:test')
-  >>> browser.open(
-  ...     'http://translations.launchpad.test/alsa-utils/trunk/'
-  ...     '+translations-upload')
-  >>> file_ctrl = browser.getControl('File:')
-  >>> file_ctrl.add_file(
-  ...     tarball, 'application/x-gzip', 'test-autofilling.tar.gz')
-  >>> browser.getControl('Upload').click()
-  >>> browser.url
-  'http://translations.launchpad.test/alsa-utils/trunk/+translations-upload'
-  >>> for tag in find_tags_by_class(browser.contents, 'message'):
-  ...     print(tag)
-  <div...Thank you for your upload. 2 files from the tarball...
+    >>> import lp.translations
+    >>> import os.path
+    >>> test_file_name = os.path.join(
+    ...     os.path.dirname(lp.translations.__file__),
+    ...     'stories/importqueue/'
+    ...     'xx-translation-import-queue-edit-autofilling.tar.gz')
+    >>> tarball = open(test_file_name, 'rb')
+
+    >>> browser = setupBrowser(auth='Basic carlos@xxxxxxxxxxxxx:test')
+    >>> browser.open(
+    ...     'http://translations.launchpad.test/alsa-utils/trunk/'
+    ...     '+translations-upload')
+    >>> file_ctrl = browser.getControl('File:')
+    >>> file_ctrl.add_file(
+    ...     tarball, 'application/x-gzip', 'test-autofilling.tar.gz')
+    >>> browser.getControl('Upload').click()
+    >>> browser.url
+    'http://translations.launchpad.test/alsa-utils/trunk/+translations-upload'
+    >>> for tag in find_tags_by_class(browser.contents, 'message'):
+    ...     print(tag)
+    <div...Thank you for your upload. 2 files from the tarball...
 
 Let's check the values we get by default from the .pot file. The name field
 and the translation domain field are pre-filled from the name of the file.
 
-  >>> login(ANONYMOUS)
-  >>> from zope.component import getUtility
-  >>> from lp.registry.interfaces.product import IProductSet
-  >>> series = getUtility(IProductSet).getByName('alsa-utils').getSeries('trunk')
-  >>> pot_qid = series.getTranslationImportQueueEntries(file_extension='pot')[0].id
-  >>> po_qid = series.getTranslationImportQueueEntries(file_extension='po')[0].id
-  >>> logout()
-
-  >>> browser.open('http://translations.launchpad.test/+imports/%d' % pot_qid)
-  >>> browser.getControl(name='field.name').value
-  'test'
-  >>> browser.getControl(name='field.translation_domain').value
-  'test'
+    >>> login(ANONYMOUS)
+    >>> from zope.component import getUtility
+    >>> from lp.registry.interfaces.product import IProductSet
+    >>> series = getUtility(IProductSet).getByName('alsa-utils').getSeries(
+    ...     'trunk')
+    >>> pot_qid = series.getTranslationImportQueueEntries(
+    ...     file_extension='pot')[0].id
+    >>> po_qid = series.getTranslationImportQueueEntries(
+    ...     file_extension='po')[0].id
+    >>> logout()
+
+    >>> browser.open(
+    ...     'http://translations.launchpad.test/+imports/%d' % pot_qid)
+    >>> browser.getControl(name='field.name').value
+    'test'
+    >>> browser.getControl(name='field.translation_domain').value
+    'test'
 
 But the path field has been preloaded with the value from the tar ball and
 the file type has been determined correctly from it.
 
-  >>> browser.getControl(name='field.path').value
-  'test/test.pot'
-  >>> browser.getControl(name='field.file_type').value
-  ['POT']
+    >>> browser.getControl(name='field.path').value
+    'test/test.pot'
+    >>> browser.getControl(name='field.file_type').value
+    ['POT']
 
 Let's fill in the information.
 
-  >>> browser.getControl('Name').value = 'alsa-utils'
-  >>> browser.getControl('Translation domain').value = 'alsa-utils'
-  >>> browser.getControl('Approve').click()
-  >>> browser.url
-  'http://translations.launchpad.test/+imports'
+    >>> browser.getControl('Name').value = 'alsa-utils'
+    >>> browser.getControl('Translation domain').value = 'alsa-utils'
+    >>> browser.getControl('Approve').click()
+    >>> browser.url
+    'http://translations.launchpad.test/+imports'
 
 Now, as we already know the name, a new form load should
 give us that field with information.
 
-  >>> browser.open('http://translations.launchpad.test/+imports/%d' % pot_qid)
-  >>> browser.getControl(name='field.name').value
-  'alsa-utils'
+    >>> browser.open(
+    ...     'http://translations.launchpad.test/+imports/%d' % pot_qid)
+    >>> browser.getControl(name='field.name').value
+    'alsa-utils'
 
 Let's move to the .po file. The language is guessed from the file name
 and the user sees a warning so they check that it's ok.
 
-  >>> browser.open('http://translations.launchpad.test/+imports/%d' % po_qid)
-  >>> browser.getControl(name='field.file_type').value
-  ['PO']
-  >>> browser.getControl(name='field.path').value
-  'test/es.po'
-  >>> browser.getControl(name='field.potemplate').value
-  ['']
-  >>> browser.getControl(name='field.language').value
-  ['es']
+    >>> browser.open(
+    ...     'http://translations.launchpad.test/+imports/%d' % po_qid)
+    >>> browser.getControl(name='field.file_type').value
+    ['PO']
+    >>> browser.getControl(name='field.path').value
+    'test/es.po'
+    >>> browser.getControl(name='field.potemplate').value
+    ['']
+    >>> browser.getControl(name='field.language').value
+    ['es']
 
 Rosetta experts should be able to override the path in the source tree from
 where this entry comes. To be sure that the path changed but that it's still
 the same entry, previous_url holds current value.
 
-  >>> browser.getControl(name='field.path').value = 'po/es.po'
-  >>> browser.getControl(name='field.potemplate').value = ['10']
-  >>> browser.getControl('Approve').click()
-  >>> browser.url
-  'http://translations.launchpad.test/+imports'
+    >>> browser.getControl(name='field.path').value = 'po/es.po'
+    >>> browser.getControl(name='field.potemplate').value = ['10']
+    >>> browser.getControl('Approve').click()
+    >>> browser.url
+    'http://translations.launchpad.test/+imports'
 
 Reloading the form shows all the submitted information applied.
 
-  >>> browser.open('http://translations.launchpad.test/+imports/%d' % po_qid)
-  >>> browser.getControl(name='field.path').value
-  'po/es.po'
-  >>> browser.getControl(name='field.potemplate').value
-  ['10']
-  >>> browser.getControl(name='field.language').value
-  ['es']
+    >>> browser.open(
+    ...     'http://translations.launchpad.test/+imports/%d' % po_qid)
+    >>> browser.getControl(name='field.path').value
+    'po/es.po'
+    >>> browser.getControl(name='field.potemplate').value
+    ['10']
+    >>> browser.getControl(name='field.language').value
+    ['es']
diff --git a/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt b/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt
index f119ac0..e1633ef 100644
--- a/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt
+++ b/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt
@@ -91,76 +91,76 @@ ignored, as well as empty files, and things whose names end in .po or
 If we are logged in as an administrator, the same page provides a link
 to where we can edit imports.
 
-  >>> browser = setupBrowser(auth='Basic jordi@xxxxxxxxxx:test')
-  >>> browser.open('http://translations.launchpad.test/+imports')
-  >>> 'po/es.po' in browser.contents
-  True
-  >>> 'Mozilla Firefox 1.0 series' in browser.contents
-  True
-  >>> link = browser.getLink('Change this entry', index=2)
-  >>> link
-  <Link text='Change this entry' url='http://translations.launchpad.test/+imports/...'>
-  >>> qid = int(link.url.rsplit('/', 1)[-1])
-  >>> browser.getControl(name='field.status_%d' % qid).displayValue
-  ['Needs Review']
+    >>> browser = setupBrowser(auth='Basic jordi@xxxxxxxxxx:test')
+    >>> browser.open('http://translations.launchpad.test/+imports')
+    >>> 'po/es.po' in browser.contents
+    True
+    >>> 'Mozilla Firefox 1.0 series' in browser.contents
+    True
+    >>> link = browser.getLink('Change this entry', index=2)
+    >>> link
+    <Link text='Change this entry' url='http://translations.launchpad.test/+imports/...'>
+    >>> qid = int(link.url.rsplit('/', 1)[-1])
+    >>> browser.getControl(name='field.status_%d' % qid).displayValue
+    ['Needs Review']
 
 Now, we attach a new file to an already existing translation resource.
 
-  >>> browser.open(
-  ...     'http://translations.launchpad.test/ubuntu/hoary/+source/evolution/'
-  ...     '+pots/evolution-2.2/+upload')
-  >>> upload = browser.getControl('File')
-  >>> upload
-  <Control name='file' type='file'>
-  >>> from io import BytesIO
-  >>> upload.add_file(BytesIO(b'# foo\n'),
-  ...   'text/x-gettext-translation-template', 'evolution.pot')
-  >>> browser.getControl('Upload').click()
-  >>> print(browser.url)
-  http://translations.launchpad.test/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+upload
-  >>> for tag in find_tags_by_class(browser.contents, 'message'):
-  ...     print(tag.decode_contents())
-  Thank you for your upload.  It will be automatically reviewed...
+    >>> browser.open(
+    ...     'http://translations.launchpad.test/ubuntu/hoary/+source/evolution/'
+    ...     '+pots/evolution-2.2/+upload')
+    >>> upload = browser.getControl('File')
+    >>> upload
+    <Control name='file' type='file'>
+    >>> from io import BytesIO
+    >>> upload.add_file(BytesIO(b'# foo\n'),
+    ...   'text/x-gettext-translation-template', 'evolution.pot')
+    >>> browser.getControl('Upload').click()
+    >>> print(browser.url)
+    http://translations.launchpad.test/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+upload
+    >>> for tag in find_tags_by_class(browser.contents, 'message'):
+    ...     print(tag.decode_contents())
+    Thank you for your upload.  It will be automatically reviewed...
 
 The import queue should have three additional entries with the last upload as
 the last entry.
 
-  >>> anon_browser.open('http://translations.launchpad.test/+imports')
-  >>> nav_index = first_tag_by_class(anon_browser.contents,
-  ...     'batch-navigation-index')
-  >>> print(extract_text(nav_index, formatter='html'))
-  1 &rarr; 5 of 5 results
-  >>> rows = find_tags_by_class(anon_browser.contents, 'import_entry_row')
-  >>> print(extract_text(rows[4]))
-  evolution.pot in
-  evolution in Ubuntu Hoary
-  Needs Review
+    >>> anon_browser.open('http://translations.launchpad.test/+imports')
+    >>> nav_index = first_tag_by_class(anon_browser.contents,
+    ...     'batch-navigation-index')
+    >>> print(extract_text(nav_index, formatter='html'))
+    1 &rarr; 5 of 5 results
+    >>> rows = find_tags_by_class(anon_browser.contents, 'import_entry_row')
+    >>> print(extract_text(rows[4]))
+    evolution.pot in
+    evolution in Ubuntu Hoary
+    Needs Review
 
 Open the edit form for the third entry.
 
-  >>> browser.open('http://translations.launchpad.test/+imports')
-  >>> browser.getLink(url='imports/%d' % qid).click()
+    >>> browser.open('http://translations.launchpad.test/+imports')
+    >>> browser.getLink(url='imports/%d' % qid).click()
 
 And provide information for this IPOTemplate to be newly created. Invalid
 names for the template are rejected.
 
-  >>> browser.getControl('File Type').value = ['POT']
-  >>> browser.getControl('Path').value = 'pkgconf-mozilla.pot'
-  >>> browser.getControl('Name').value = '.InvalidName'
-  >>> browser.getControl('Translation domain').value = 'pkgconf-mozilla'
-  >>> browser.getControl('Approve').click()
-  >>> print(browser.url)
-  http://translations.launchpad.test/+imports/.../+index
-  >>> message = find_tags_by_class(browser.contents, 'message')[1]
-  >>> print(message.string)
-  Please specify a valid name...
+    >>> browser.getControl('File Type').value = ['POT']
+    >>> browser.getControl('Path').value = 'pkgconf-mozilla.pot'
+    >>> browser.getControl('Name').value = '.InvalidName'
+    >>> browser.getControl('Translation domain').value = 'pkgconf-mozilla'
+    >>> browser.getControl('Approve').click()
+    >>> print(browser.url)
+    http://translations.launchpad.test/+imports/.../+index
+    >>> message = find_tags_by_class(browser.contents, 'message')[1]
+    >>> print(message.string)
+    Please specify a valid name...
 
 So we'd better specify a valid name.
 
-  >>> browser.getControl('Name').value = 'pkgconf-mozilla'
-  >>> browser.getControl('Approve').click()
-  >>> print(browser.url)
-  http://translations.launchpad.test/+imports
+    >>> browser.getControl('Name').value = 'pkgconf-mozilla'
+    >>> browser.getControl('Approve').click()
+    >>> print(browser.url)
+    http://translations.launchpad.test/+imports
 
 Open the edit form for the fourth entry.
 
@@ -169,37 +169,37 @@ bug, so we need to reopen the page we are currently at to set 'referer'
 header properly.  This seems similar to #98437 but the fix proposed
 there doesn't help.
 
-  >>> browser.open('http://translations.launchpad.test/+imports')
-  >>> browser.getLink(url='imports/%d' % (qid + 1)).click()
+    >>> browser.open('http://translations.launchpad.test/+imports')
+    >>> browser.getLink(url='imports/%d' % (qid + 1)).click()
 
 And provide information for this IPOFile to be newly created.
 
-  >>> browser.getControl('File Type').value = ['PO']
-  >>> browser.getControl(name='field.potemplate').displayValue = [
-  ...     'pkgconf-mozilla']
-  >>> browser.getControl('Language').value = ['es']
-  >>> browser.getControl('Approve').click()
-  >>> print(browser.url)
-  http://translations.launchpad.test/+imports
+    >>> browser.getControl('File Type').value = ['PO']
+    >>> browser.getControl(name='field.potemplate').displayValue = [
+    ...     'pkgconf-mozilla']
+    >>> browser.getControl('Language').value = ['es']
+    >>> browser.getControl('Approve').click()
+    >>> print(browser.url)
+    http://translations.launchpad.test/+imports
 
 The entries are approved, and now have the place where they will be
 imported assigned.
 
-  >>> anon_browser.open('http://translations.launchpad.test/+imports')
-  >>> imports_table = find_tag_by_id(
-  ...     anon_browser.contents, 'import-entries-list')
-  >>> print(extract_text(imports_table))
-  pkgconf-mozilla.pot in
-  Mozilla Firefox 1.0 series
-  Approved
-  ...
-  Template "pkgconf-mozilla" in Mozilla Firefox 1.0
-  po/es.po in
-  Mozilla Firefox 1.0 series
-  Approved
-  ...
-  Spanish (es) translation of pkgconf-mozilla in Mozilla Firefox 1.0
-  ...
+    >>> anon_browser.open('http://translations.launchpad.test/+imports')
+    >>> imports_table = find_tag_by_id(
+    ...     anon_browser.contents, 'import-entries-list')
+    >>> print(extract_text(imports_table))
+    pkgconf-mozilla.pot in
+    Mozilla Firefox 1.0 series
+    Approved
+    ...
+    Template "pkgconf-mozilla" in Mozilla Firefox 1.0
+    po/es.po in
+    Mozilla Firefox 1.0 series
+    Approved
+    ...
+    Spanish (es) translation of pkgconf-mozilla in Mozilla Firefox 1.0
+    ...
 
 Removing from the import queue
 ------------------------------
@@ -208,84 +208,86 @@ There is an option to remove entries from the queue.
 
 No Privileges Person tries to remove entries but to no effect.
 
-  >>> from six.moves.urllib.parse import urlencode
-  >>> post_data = urlencode(
-  ...     {
-  ...         'field.filter_target': 'all',
-  ...         'field.filter_status': 'all',
-  ...         'field.filter_extension': 'all',
-  ...         'field.status_1': 'DELETED',
-  ...         'field.status_2': 'DELETED',
-  ...         'field.status_3': 'DELETED',
-  ...         'field.status_4': 'DELETED',
-  ...         'field.status_5': 'DELETED',
-  ...         'field.actions.change_status': 'Change status',
-  ...     })
-  >>> user_browser.addHeader("Referer", "http://launchpad.test";)
-  >>> user_browser.open(
-  ...     'http://translations.launchpad.test/+imports',
-  ...     data=post_data)
-  >>> for status in find_tags_by_class(user_browser.contents, 'import_status'):
-  ...     print(extract_text(status))
-  Approved
-  Approved
-  Imported
-  Imported
-  Needs Review
+    >>> from six.moves.urllib.parse import urlencode
+    >>> post_data = urlencode(
+    ...     {
+    ...         'field.filter_target': 'all',
+    ...         'field.filter_status': 'all',
+    ...         'field.filter_extension': 'all',
+    ...         'field.status_1': 'DELETED',
+    ...         'field.status_2': 'DELETED',
+    ...         'field.status_3': 'DELETED',
+    ...         'field.status_4': 'DELETED',
+    ...         'field.status_5': 'DELETED',
+    ...         'field.actions.change_status': 'Change status',
+    ...     })
+    >>> user_browser.addHeader("Referer", "http://launchpad.test";)
+    >>> user_browser.open(
+    ...     'http://translations.launchpad.test/+imports',
+    ...     data=post_data)
+    >>> for status in find_tags_by_class(
+    ...         user_browser.contents, 'import_status'):
+    ...     print(extract_text(status))
+    Approved
+    Approved
+    Imported
+    Imported
+    Needs Review
 
 But Jordi, a Rosetta expert, will be allowed to remove it.
 
-  >>> jordi_browser = setupBrowser(auth='Basic jordi@xxxxxxxxxx:test')
-  >>> jordi_browser.open('http://translations.launchpad.test/+imports')
-  >>> jordi_browser.getControl(name='field.status_1').value = ['DELETED']
-  >>> jordi_browser.getControl('Change status').click()
-  >>> jordi_browser.url
-  'http://translations.launchpad.test/+imports/+index'
-
-  >>> print(find_main_content(jordi_browser.contents))
-  <...po/evolution-2.2-test.pot...
-  ...Evolution trunk series...
-  ...field.status_1...
-  ...selected="selected" value="DELETED"...
-  ...Foo Bar...
-  ...Template "evolution-2.2-test" in Evolution trunk...
+    >>> jordi_browser = setupBrowser(auth='Basic jordi@xxxxxxxxxx:test')
+    >>> jordi_browser.open('http://translations.launchpad.test/+imports')
+    >>> jordi_browser.getControl(name='field.status_1').value = ['DELETED']
+    >>> jordi_browser.getControl('Change status').click()
+    >>> jordi_browser.url
+    'http://translations.launchpad.test/+imports/+index'
+
+    >>> print(find_main_content(jordi_browser.contents))
+    <...po/evolution-2.2-test.pot...
+    ...Evolution trunk series...
+    ...field.status_1...
+    ...selected="selected" value="DELETED"...
+    ...Foo Bar...
+    ...Template "evolution-2.2-test" in Evolution trunk...
 
 Foo Bar Person is a launchpad admin and they're allowed to remove an entry.
 
-  >>> admin_browser.open('http://translations.launchpad.test/+imports')
-  >>> admin_browser.getControl(name='field.status_2').value = ['DELETED']
-  >>> admin_browser.getControl('Change status').click()
-  >>> admin_browser.url
-  'http://translations.launchpad.test/+imports/+index'
-
-  >>> print(find_main_content(admin_browser.contents))
-  <...po/pt_BR.po...
-  ...Evolution trunk series...
-  ...field.status_2...
-  ...selected="selected" value="DELETED"...
-  ...Foo Bar...
-  ...Portuguese (Brazil) (pt_BR) translation of evolution-2.2-test
-    in Evolution trunk...
+    >>> admin_browser.open('http://translations.launchpad.test/+imports')
+    >>> admin_browser.getControl(name='field.status_2').value = ['DELETED']
+    >>> admin_browser.getControl('Change status').click()
+    >>> admin_browser.url
+    'http://translations.launchpad.test/+imports/+index'
+
+    >>> print(find_main_content(admin_browser.contents))
+    <...po/pt_BR.po...
+    ...Evolution trunk series...
+    ...field.status_2...
+    ...selected="selected" value="DELETED"...
+    ...Foo Bar...
+    ...Portuguese (Brazil) (pt_BR) translation of evolution-2.2-test
+      in Evolution trunk...
 
 And finally, we make sure that the importer is also allowed to remove their
 own imports.
 
-  >>> ff_owner_browser.open('http://translations.launchpad.test/+imports')
-  >>> status = ff_owner_browser.getControl(name='field.status_%d' % (qid + 1))
-  >>> status.value
-  ['APPROVED']
-  >>> status.value = ['DELETED']
-  >>> ff_owner_browser.getControl('Change status').click()
+    >>> ff_owner_browser.open('http://translations.launchpad.test/+imports')
+    >>> status = ff_owner_browser.getControl(
+    ...     name='field.status_%d' % (qid + 1))
+    >>> status.value
+    ['APPROVED']
+    >>> status.value = ['DELETED']
+    >>> ff_owner_browser.getControl('Change status').click()
 
 The entry now appears deleted.
 
-  >>> print(find_main_content(ff_owner_browser.contents))
-  <...po/es.po...
-  ...Mozilla Firefox 1.0 series...
-  ...field.status_...
-  ...selected="selected" value="DELETED"...
-  ...Sample Person...
-  ...Spanish (es) translation of pkgconf-mozilla in Mozilla Firefox 1.0...
+    >>> print(find_main_content(ff_owner_browser.contents))
+    <...po/es.po...
+    ...Mozilla Firefox 1.0 series...
+    ...field.status_...
+    ...selected="selected" value="DELETED"...
+    ...Sample Person...
+    ...Spanish (es) translation of pkgconf-mozilla in Mozilla Firefox 1.0...
 
 
 Ubuntu uploads
@@ -338,112 +340,112 @@ Corner cases
 
 Let's check tar.bz2 uploads. They work ;-)
 
-  >>> evo_owner_browser = ff_owner_browser
-  >>> evo_owner_browser.open(
-  ...     'http://translations.launchpad.test/evolution/trunk/'
-  ...     '+translations-upload')
-
-  >>> test_file_name = os.path.join(
-  ...     os.path.dirname(lp.translations.__file__),
-  ...     'stories/importqueue/xx-translation-import-queue.tar.bz2')
-  >>> tarball = open(test_file_name, 'rb')
-
-  >>> evo_owner_browser.getControl('File').add_file(
-  ...     tarball, 'application/x-bzip', test_file_name)
-  >>> evo_owner_browser.getControl('Upload').click()
-  >>> evo_owner_browser.url
-  'http://translations.launchpad.test/evolution/trunk/+translations-upload'
-  >>> for tag in find_tags_by_class(evo_owner_browser.contents, 'message'):
-  ...     print(extract_text(tag))
-  Thank you for your upload. 2 files from the tarball will be automatically
-  reviewed...
+    >>> evo_owner_browser = ff_owner_browser
+    >>> evo_owner_browser.open(
+    ...     'http://translations.launchpad.test/evolution/trunk/'
+    ...     '+translations-upload')
+
+    >>> test_file_name = os.path.join(
+    ...     os.path.dirname(lp.translations.__file__),
+    ...     'stories/importqueue/xx-translation-import-queue.tar.bz2')
+    >>> tarball = open(test_file_name, 'rb')
+
+    >>> evo_owner_browser.getControl('File').add_file(
+    ...     tarball, 'application/x-bzip', test_file_name)
+    >>> evo_owner_browser.getControl('Upload').click()
+    >>> evo_owner_browser.url
+    'http://translations.launchpad.test/evolution/trunk/+translations-upload'
+    >>> for tag in find_tags_by_class(evo_owner_browser.contents, 'message'):
+    ...     print(extract_text(tag))
+    Thank you for your upload. 2 files from the tarball will be automatically
+    reviewed...
 
 Let's try breaking the form by not supplying a file object. It give us a
 decent error message:
 
-  >>> browser.open(
-  ...     'http://translations.launchpad.test/ubuntu/hoary/'
-  ...     '+source/evolution/+pots/evolution-2.2/+upload')
-  >>> browser.getControl('Upload').click()
-  >>> for tag in find_tags_by_class(browser.contents, 'message'):
-  ...     print(tag)
-  <div...Your upload was ignored because you didn't select a file....
-  ...Please select a file and try again.</div>...
+    >>> browser.open(
+    ...     'http://translations.launchpad.test/ubuntu/hoary/'
+    ...     '+source/evolution/+pots/evolution-2.2/+upload')
+    >>> browser.getControl('Upload').click()
+    >>> for tag in find_tags_by_class(browser.contents, 'message'):
+    ...     print(tag)
+    <div...Your upload was ignored because you didn't select a file....
+    ...Please select a file and try again.</div>...
 
 Let's try now a tarball upload. Should work:
 
-  >>> evo_owner_browser.open(
-  ...     'http://translations.launchpad.test/evolution/trunk/'
-  ...     '+translations-upload')
-
-  >>> test_file_name = os.path.join(
-  ...     os.path.dirname(lp.translations.__file__),
-  ...     'stories/importqueue/xx-translation-import-queue.tar')
-  >>> tarball = open(test_file_name, 'rb')
-
-  >>> evo_owner_browser.getControl('File').add_file(
-  ...     tarball, 'application/x-gzip', test_file_name)
-  >>> evo_owner_browser.getControl('Upload').click()
-  >>> evo_owner_browser.url
-  'http://translations.launchpad.test/evolution/trunk/+translations-upload'
-  >>> for tag in find_tags_by_class(evo_owner_browser.contents, 'message'):
-  ...     print(extract_text(tag))
-  Thank you for your upload. 1 file from the tarball will be automatically
-  reviewed...
+    >>> evo_owner_browser.open(
+    ...     'http://translations.launchpad.test/evolution/trunk/'
+    ...     '+translations-upload')
+
+    >>> test_file_name = os.path.join(
+    ...     os.path.dirname(lp.translations.__file__),
+    ...     'stories/importqueue/xx-translation-import-queue.tar')
+    >>> tarball = open(test_file_name, 'rb')
+
+    >>> evo_owner_browser.getControl('File').add_file(
+    ...     tarball, 'application/x-gzip', test_file_name)
+    >>> evo_owner_browser.getControl('Upload').click()
+    >>> evo_owner_browser.url
+    'http://translations.launchpad.test/evolution/trunk/+translations-upload'
+    >>> for tag in find_tags_by_class(evo_owner_browser.contents, 'message'):
+    ...     print(extract_text(tag))
+    Thank you for your upload. 1 file from the tarball will be automatically
+    reviewed...
 
 We can handle an empty file disguised as a bzipped tarfile:
 
-  >>> evo_owner_browser.open(
-  ...     'http://translations.launchpad.test/evolution/trunk/'
-  ...     '+translations-upload')
-
-  >>> test_file_name = os.path.join(
-  ...     os.path.dirname(lp.translations.__file__),
-  ...     'stories/importqueue/empty.tar.bz2')
-  >>> tarball = open(test_file_name, 'rb')
+    >>> evo_owner_browser.open(
+    ...     'http://translations.launchpad.test/evolution/trunk/'
+    ...     '+translations-upload')
 
-  >>> evo_owner_browser.getControl('File').add_file(
-  ...     tarball, 'application/x-gzip', test_file_name)
-  >>> evo_owner_browser.getControl('Upload').click()
-  >>> evo_owner_browser.url
-  'http://translations.launchpad.test/evolution/trunk/+translations-upload'
-  >>> for tag in find_tags_by_class(evo_owner_browser.contents, 'message'):
-  ...     print(extract_text(tag))
-  Upload ignored.  The tarball you uploaded did not contain...
+    >>> test_file_name = os.path.join(
+    ...     os.path.dirname(lp.translations.__file__),
+    ...     'stories/importqueue/empty.tar.bz2')
+    >>> tarball = open(test_file_name, 'rb')
+
+    >>> evo_owner_browser.getControl('File').add_file(
+    ...     tarball, 'application/x-gzip', test_file_name)
+    >>> evo_owner_browser.getControl('Upload').click()
+    >>> evo_owner_browser.url
+    'http://translations.launchpad.test/evolution/trunk/+translations-upload'
+    >>> for tag