launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27308
[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 "password"
- >>> for field, error in view.widget_errors.items():
- ... print("%s: %s" % (field, error))
- password: your password must not be "password"
+ >>> 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 "password"
+ >>> for field, error in view.widget_errors.items():
+ ... print("%s: %s" % (field, error))
+ password: your password must not be "password"
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 "test-blueprint"' 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 "test-blueprint"' 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 "canvas"' 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 "canvas"' 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 <canvas> 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 <canvas> 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 <canvas> 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 <canvas> 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 <canvas> Objects" .../>
- <area shape="poly" ...title="A title with non-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 <canvas> Objects" .../>
+ <area shape="poly" ...title="A title with non-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'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'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’t vote in this poll because you’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’t vote in this poll because you’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: "not-a-uri" 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: "not-a-uri" 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 "http" 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 "http" 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 <email address hidden> 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 <email address hidden> 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 → 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 → 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