launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00346
[Merge] lp:~sinzui/launchpad/apocalypse-interface-imports-1 into lp:launchpad/devel
Curtis Hovey has proposed merging lp:~sinzui/launchpad/apocalypse-interface-imports-1 into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
This is my first branch to not import from canonical.launchpad so that we can
end the glob imports. This branch updated lp.answers and lp.blueprints to
import from true locations. Lint demanded that I clean up a lot in these often
neglected modules.
This could be the most boring branch of semi-mechanical changes you will
ever read. There was some small decisions made when ordering imports to
reconcile what I perceived to be cyclic import issues. I have great hope that
when I come to registry and soyuz, I will resolve the true origin of the
cyclic imports.
lp:~sinzui/launchpad/apocalypse-interface-imports-1
Diff size: 6031
Test command: ./bin/test -vv -t lp/answers -t lp/blueprints
I ran this though ec2 to be sure this did not introduce
cyclic import issues in other parts of the code.
Pre-implementation: no one
Target release: 10.08
Rules
-----
* Use many find and replace passes to change import from
canonical.launchpad to use the real location
* Manually fix spacing in python files to quiet lint
* Use the doctest reformatter to quiet lint.
QA
--
* Verify that answers and blueprints still appear to work.
* Watch the oopses for issues that may have escaped the test suite.
Lint
----
Please take my word that I ran link on blueprints and answers. I really did
not make the lint changes because I love formatting code and test.
Linting changed files:
lib/lp/answers/adapters.py
lib/lp/answers/configure.zcml
lib/lp/answers/karma.py
lib/lp/answers/testing.py
lib/lp/answers/browser/configure.zcml
lib/lp/answers/browser/faqtarget.py
lib/lp/answers/browser/question.py
lib/lp/answers/browser/questiontarget.py
lib/lp/answers/browser/tests/question-subscribe_me.txt
lib/lp/answers/browser/tests/views.txt
lib/lp/answers/doc/emailinterface.txt.disabled
lib/lp/answers/doc/expiration.txt
lib/lp/answers/doc/faq.txt
lib/lp/answers/doc/faqcollection.txt
lib/lp/answers/doc/faqtarget.txt
lib/lp/answers/doc/karma.txt
lib/lp/answers/doc/notifications.txt
lib/lp/answers/doc/projectgroup.txt
lib/lp/answers/doc/question.txt
lib/lp/answers/doc/questionsets.txt
lib/lp/answers/doc/workflow.txt
lib/lp/answers/model/answercontact.py
lib/lp/answers/model/faq.py
lib/lp/answers/model/question.py
lib/lp/answers/model/questionmessage.py
lib/lp/answers/model/questionreopening.py
lib/lp/answers/model/questionsubscription.py
lib/lp/answers/scripts/questionexpiration.py
lib/lp/answers/stories/distribution-package-answer-contact.txt
lib/lp/answers/tests/questiontarget-sourcepackage.txt
lib/lp/answers/tests/test_question_workflow.py
lib/lp/blueprints/adapters.py
lib/lp/blueprints/browser/configure.zcml
lib/lp/blueprints/browser/tests/sprintattendance-views.txt
lib/lp/blueprints/doc/spec-mail-exploder.txt
lib/lp/blueprints/doc/specgraph.txt
lib/lp/blueprints/doc/specification-branch.txt
lib/lp/blueprints/doc/specification-notifications.txt
lib/lp/blueprints/doc/specification.txt
lib/lp/blueprints/doc/specificationmessage.txt
lib/lp/blueprints/doc/sprint-meeting-export.txt
lib/lp/blueprints/doc/sprint.txt
lib/lp/blueprints/interfaces/specification.py
lib/lp/blueprints/model/specification.py
lib/lp/blueprints/stories/sprints/20-sprint-registration.txt
lib/lp/blueprints/stories/standalone/subscribing.txt
Test
----
No test logic was changed, though some long lines were changed.
Implementation
--------------
No code paths were changed, though some lines were wrapped to quiet lint.
--
https://code.launchpad.net/~sinzui/launchpad/apocalypse-interface-imports-1/+merge/31324
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/apocalypse-interface-imports-1 into lp:launchpad/devel.
=== modified file 'lib/lp/answers/adapters.py'
--- lib/lp/answers/adapters.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/adapters.py 2010-07-29 20:06:01 +0000
@@ -7,7 +7,7 @@
__all__ = []
-from canonical.launchpad.interfaces import IFAQTarget
+from lp.answers.interfaces.faqtarget import IFAQTarget
def question_to_questiontarget(question):
@@ -46,4 +46,3 @@
def faq_to_faqtarget(faq):
"""Adapts an `IFAQ` into an `IFAQTarget`."""
return faq.target
-
=== modified file 'lib/lp/answers/browser/configure.zcml'
--- lib/lp/answers/browser/configure.zcml 2010-07-16 16:58:55 +0000
+++ lib/lp/answers/browser/configure.zcml 2010-07-29 20:06:01 +0000
@@ -260,7 +260,7 @@
<browser:url
for="lp.answers.interfaces.questioncollection.IQuestionSet"
path_expression="string:questions"
- parent_utility="canonical.launchpad.interfaces.ILaunchpadRoot"
+ parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"
rootsite="answers"
/>
@@ -350,13 +350,13 @@
permission="zope.Public"/>
<browser:defaultView
- for="canonical.launchpad.interfaces.IPerson"
+ for="lp.registry.interfaces.person.IPerson"
layer="lp.answers.publisher.AnswersLayer"
name="+questions"
/>
<browser:page
- for="canonical.launchpad.interfaces.IPerson"
+ for="lp.registry.interfaces.person.IPerson"
name="+ask-a-question-button"
template="../templates/null.pt"
permission="zope.Public"
=== modified file 'lib/lp/answers/browser/faqtarget.py'
--- lib/lp/answers/browser/faqtarget.py 2009-09-18 12:15:42 +0000
+++ lib/lp/answers/browser/faqtarget.py 2010-07-29 20:06:01 +0000
@@ -11,11 +11,11 @@
]
from canonical.launchpad import _
-from canonical.launchpad.interfaces import IFAQ
from canonical.launchpad.webapp import (
action, canonical_url, custom_widget, LaunchpadFormView, stepthrough)
from canonical.launchpad.webapp.interfaces import NotFoundError
from canonical.widgets import TokensTextWidget
+from lp.answers.interfaces.faq import IFAQ
class FAQTargetNavigationMixin:
@@ -51,4 +51,3 @@
self.user, data['title'], data['content'],
keywords=data['keywords'])
self.next_url = canonical_url(faq)
-
=== modified file 'lib/lp/answers/browser/question.py'
--- lib/lp/answers/browser/question.py 2010-04-16 15:06:55 +0000
+++ lib/lp/answers/browser/question.py 2010-07-29 20:06:01 +0000
@@ -54,13 +54,8 @@
from canonical.launchpad.helpers import (
is_english_variant, preferred_or_request_languages)
-from canonical.launchpad.interfaces import (
- IAnswersFrontPageSearchForm, IFAQ, IFAQTarget,
- ILaunchpadCelebrities, ILaunchpadStatisticSet, IProjectGroup, IQuestion,
- IQuestionAddMessageForm, IQuestionChangeStatusForm, IQuestionLinkFAQForm,
- IQuestionSet, IQuestionTarget, QuestionAction, QuestionStatus,
- QuestionSort, NotFoundError, UnexpectedFormData)
-
+from canonical.launchpad.webapp.interfaces import (
+ NotFoundError, UnexpectedFormData)
from canonical.launchpad.webapp import (
ApplicationMenu, ContextMenu, Link, canonical_url,
enabled_with_permission, Navigation, LaunchpadView, action,
@@ -73,8 +68,22 @@
from canonical.widgets import LaunchpadRadioWidget, TokensTextWidget
from canonical.widgets.project import ProjectScopeWidget
from canonical.widgets.launchpadtarget import LaunchpadTargetWidget
-
from canonical.lazr.utils import smartquote
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.interfaces.launchpadstatistic import (
+ ILaunchpadStatisticSet)
+from lp.registry.interfaces.projectgroup import IProjectGroup
+from lp.answers.interfaces.faq import IFAQ
+from lp.answers.interfaces.faqtarget import IFAQTarget
+from lp.answers.interfaces.questionenums import (
+ QuestionAction, QuestionSort, QuestionStatus)
+from lp.answers.interfaces.question import (
+ IQuestion, IQuestionAddMessageForm, IQuestionChangeStatusForm,
+ IQuestionLinkFAQForm)
+from lp.answers.interfaces.questioncollection import (
+ IQuestionSet)
+from lp.answers.interfaces.questiontarget import (
+ IAnswersFrontPageSearchForm, IQuestionTarget)
class QuestionLinksMixin:
=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py 2010-02-17 11:13:06 +0000
+++ lib/lp/answers/browser/questiontarget.py 2010-07-29 20:06:01 +0000
@@ -38,12 +38,8 @@
from canonical.launchpad.fields import PublicPersonChoice
from canonical.launchpad.helpers import (
browserLanguages, is_english_variant, preferred_or_request_languages)
-from lp.answers.browser.faqcollection import FAQCollectionMenu
-from canonical.launchpad.interfaces import (
- IDistribution, IFAQCollection, ILanguageSet, ILaunchpadCelebrities,
- IProjectGroup, IQuestionCollection, IQuestionSet, IQuestionTarget,
- ISearchableByQuestionOwner, ISearchQuestionsForm, NotFoundError,
- QuestionStatus)
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.webapp.interfaces import NotFoundError
from canonical.launchpad.webapp import (
action, canonical_url, custom_widget, LaunchpadFormView, Link,
safe_action, stepto, stepthrough, urlappend)
@@ -51,6 +47,16 @@
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
from canonical.launchpad.webapp.menu import structured
from canonical.widgets import LabeledMultiCheckBoxWidget
+from lp.services.worlddata.interfaces.language import ILanguageSet
+from lp.registry.interfaces.projectgroup import IProjectGroup
+from lp.registry.interfaces.distribution import IDistribution
+from lp.answers.interfaces.questionenums import QuestionStatus
+from lp.answers.interfaces.faqcollection import IFAQCollection
+from lp.answers.interfaces.questioncollection import (
+ IQuestionCollection, IQuestionSet, ISearchableByQuestionOwner)
+from lp.answers.interfaces.questiontarget import (
+ IQuestionTarget, ISearchQuestionsForm)
+from lp.answers.browser.faqcollection import FAQCollectionMenu
class AskAQuestionButtonView:
@@ -655,7 +661,7 @@
answer_contacts).intersection(self.administrated_teams)
return {
'want_to_be_answer_contact': user in answer_contacts,
- 'answer_contact_teams': list(answer_contact_teams)
+ 'answer_contact_teams': list(answer_contact_teams),
}
@action(_('Continue'), name='update')
@@ -717,8 +723,8 @@
english = getUtility(ILaunchpadCelebrities).english
if person_or_team.isTeam():
person_or_team.addLanguage(english)
- team_mapping = {'name' : person_or_team.name,
- 'displayname' : person_or_team.displayname}
+ team_mapping = {'name': person_or_team.name,
+ 'displayname': person_or_team.displayname}
msgid = _("English was added to ${displayname}'s "
'<a href="/~${name}/+editlanguages">preferred '
'languages</a>.',
@@ -735,7 +741,7 @@
msgid = _('<a href="/people/+me/+editlanguages">Your preferred '
'languages</a> were updated to include your browser '
'languages: $languages.',
- mapping={'languages' : language_str})
+ mapping={'languages': language_str})
response.addNotification(structured(msgid))
@@ -771,7 +777,6 @@
raise NotFoundError(name)
return self.redirectSubTree(canonical_url(question))
-
@stepto('+ticket')
def redirect_ticket(self):
"""Use RedirectionNavigation to redirect to +question.
@@ -783,15 +788,13 @@
return self.redirectSubTree(target)
-
-# XXX flacoste 2007-07-08 bug=125851:
-# This menu shouldn't "extend" FAQCollectionMenu.
-# But this is needed because of limitations in the current menu architecture.
-# Menu should be built by merging all menus applying to the context object
-# (-based on the interfaces it provides).
class QuestionCollectionAnswersMenu(FAQCollectionMenu):
"""Base menu definition for QuestionCollection searchable by owner."""
-
+ # XXX flacoste 2007-07-08 bug=125851:
+ # This menu shouldn't "extend" FAQCollectionMenu.
+ # architecture. But this is needed because of limitations in the current
+ # menu Menu should be built by merging all menus applying to the context
+ # object (-based on the interfaces it provides).
usedfor = ISearchableByQuestionOwner
facet = 'answers'
links = FAQCollectionMenu.links + [
=== modified file 'lib/lp/answers/browser/tests/question-subscribe_me.txt'
--- lib/lp/answers/browser/tests/question-subscribe_me.txt 2009-03-24 12:43:49 +0000
+++ lib/lp/answers/browser/tests/question-subscribe_me.txt 2010-07-29 20:06:01 +0000
@@ -1,12 +1,13 @@
-= QuestionWorkflowView: Handling of the subscribe_me option =
+QuestionWorkflowView: Handling of the subscribe_me option
+=========================================================
This test makes sure that it is possible to subscribe to the question
whatever the action used.
>>> from canonical.launchpad.browser import QuestionWorkflowView
>>> from canonical.launchpad.ftests import LaunchpadFormHarness
- >>> from canonical.launchpad.interfaces import ILaunchBag, IProductSet
- >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+ >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
+ >>> from lp.registry.interfaces.product import IProductSet
>>> firefox = getUtility(IProductSet).getByName('firefox')
>>> login('test@xxxxxxxxxxxxx')
=== modified file 'lib/lp/answers/browser/tests/views.txt'
--- lib/lp/answers/browser/tests/views.txt 2010-07-16 16:35:11 +0000
+++ lib/lp/answers/browser/tests/views.txt 2010-07-29 20:06:01 +0000
@@ -1,11 +1,12 @@
-= Answer Tracker Pages =
+Answer Tracker Pages
+====================
Several views are used to handle the various operations on a question.
>>> from zope.component import getMultiAdapter
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
- >>> from canonical.launchpad.interfaces import (
- ... IDistributionSet, IProductSet)
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
+ >>> from lp.registry.interfaces.product import IProductSet
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> question_three = ubuntu.getQuestion(3)
>>> firefox = getUtility(IProductSet).getByName('firefox')
@@ -13,6 +14,7 @@
# The firefox_question doesn't have any subscribers, let's subscribe
# the owner.
+
>>> login('test@xxxxxxxxxxxxx')
>>> firefox_question.subscribe(firefox_question.owner)
<QuestionSubscription...>
@@ -20,6 +22,7 @@
# Let's define a helper function which commits the transaction, so
# that the notifications are queued in stub.test_emails and pops these
# notifications from the queue.
+
>>> from lp.services.mail import stub
>>> import email
>>> import transaction
@@ -33,7 +36,8 @@
... return notifications
-== QuestionSubscriptionView ==
+QuestionSubscriptionView
+------------------------
This view is used to subscribe and unsubscribe from a question.
Subscription is done when the user click on the 'Subscribe' button.
@@ -51,8 +55,8 @@
>>> question_three.isSubscribed(getUtility(ILaunchBag).user)
True
-A notification message is displayed and the view redirect to the question
-view page.
+A notification message is displayed and the view redirect to the
+question view page.
>>> for notice in view.request.notifications:
... print notice.message
@@ -86,14 +90,16 @@
0
-== QuestionWorkflowView ==
+QuestionWorkflowView
+--------------------
-QuestionWorkflowView is the view used to handle the comments submitted by
-users on the question. The actions available on it always depends on the
-current state of the question and the identify of the user viewing the
-form.
+QuestionWorkflowView is the view used to handle the comments submitted
+by users on the question. The actions available on it always depends on
+the current state of the question and the identify of the user viewing
+the form.
# Setup a harness to easily test the view.
+
>>> from canonical.launchpad.ftests import LaunchpadFormHarness
>>> from canonical.launchpad.browser import QuestionWorkflowView
>>> workflow_harness = LaunchpadFormHarness(
@@ -101,6 +107,7 @@
# Let's define a helper method that will return the names of the
# available actions.
+
>>> def getAvailableActionNames(view):
... names = [action.__name__.split('.')[-1]
... for action in view.actions
@@ -141,14 +148,16 @@
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your information request.
+
>>> print firefox_question.status.name
NEEDSINFO
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
Workflow actions like these will send out notifications to subscribers.
-(Complete notifications testing will be found in
-answer-tracker-notifications.txt)
+(Complete notifications testing will be found in answer-tracker-
+notifications.txt)
>>> len(pop_notifications())
1
@@ -169,15 +178,18 @@
If he replies with the requested information, the question is moved back
to the OPEN state.
- >>> workflow_harness.submit(
- ... 'giveinfo', {
- ... 'field.message': "The following SVG doesn't display properly:"
- ... "\nhttp://www.w3.org/2001/08/rdfweb/rdfweb-chaals-and-dan.svg"})
+ >>> form = {
+ ... 'field.message': "The following SVG doesn't display properly:"
+ ... "\nhttp://www.w3.org/2001/08/rdfweb/rdfweb-chaals-and-dan.svg"
+ ... }
+ >>> workflow_harness.submit('giveinfo', form)
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for adding more information to your question.
+
>>> print firefox_question.status.name
OPEN
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
@@ -192,8 +204,10 @@
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your answer.
+
>>> print firefox_question.status.name
ANSWERED
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
@@ -207,9 +221,9 @@
>>> getAvailableActionNames(workflow_harness.view)
['comment', 'confirm', 'reopen', 'selfanswer']
-Let's say he confirms the previous answer, in this case, the question will
-move to the 'SOLVED' state. Note that the UI doesn't enable the user to
-enter a confirmation message at that stage.
+Let's say he confirms the previous answer, in this case, the question
+will move to the 'SOLVED' state. Note that the UI doesn't enable the
+user to enter a confirmation message at that stage.
>>> answer_message_number = firefox_question.messages.count() - 1
>>> workflow_harness.submit(
@@ -218,8 +232,10 @@
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your feedback.
+
>>> print firefox_question.status.name
SOLVED
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
@@ -244,8 +260,10 @@
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your comment.
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
+
>>> print firefox_question.status.name
SOLVED
@@ -256,8 +274,8 @@
>>> getAvailableActionNames(workflow_harness.view)
['comment']
-If the question owner reopens the question, its status is changed back to
-'OPEN'.
+If the question owner reopens the question, its status is changed back
+to 'OPEN'.
>>> login('test@xxxxxxxxxxxxx')
>>> workflow_harness.submit(
@@ -269,14 +287,16 @@
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Your question was reopened.
+
>>> print firefox_question.status.name
OPEN
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
-When the question owner answers his own question, it is moved straight to
-the SOLVED state. The question owner is attributed as the answerer, but
-no answer message is assigned to the answer.
+When the question owner answers his own question, it is moved straight
+to the SOLVED state. The question owner is attributed as the answerer,
+but no answer message is assigned to the answer.
>>> workflow_harness.submit(
... 'selfanswer', {
@@ -286,20 +306,24 @@
... print notification.message
Your question is solved. If a particular message helped you solve the
problem, use the <em>'This solved my problem'</em> button.
+
>>> print firefox_question.status.name
SOLVED
+
>>> print firefox_question.answerer.displayname
Sample Person
+
>>> firefox_question.answer is None
True
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
-When the answerer is the question owner, the owner can still confirm
-an answer, in addition to adding a comment or reopening the question.
-This path permits the question owner to state how the problem was
-solved, then attribute an answerer as a contributor to the solution.
-The answerer's message is attributed as the answer in this case.
+When the answerer is the question owner, the owner can still confirm an
+answer, in addition to adding a comment or reopening the question. This
+path permits the question owner to state how the problem was solved,
+then attribute an answerer as a contributor to the solution. The
+answerer's message is attributed as the answer in this case.
>>> getAvailableActionNames(workflow_harness.view)
['comment', 'confirm', 'reopen']
@@ -309,21 +333,27 @@
... 'field.message': ''})
>>> print firefox_question.status.name
SOLVED
+
>>> print firefox_question.answerer.displayname
No Privileges Person
+
>>> print firefox_question.answer.owner.displayname
No Privileges Person
+
>>> answer_id = firefox_question.messages[answer_message_number].id
>>> firefox_question.answer.id == answer_id
True
+
>>> workflow_harness.redirectionTarget()
'.../+question/2'
# Clear all notifications.
+
>>> notifications = pop_notifications()
-== QuestionMakeBugView ==
+QuestionMakeBugView
+-------------------
The QuestionMakeBugView is used to handle the creation of a bug from a
question. In addition to creating a bug, this operation will also link
@@ -341,24 +371,32 @@
>>> makebug = getMultiAdapter((question_three, request), name='+makebug')
>>> question_three.bugs.count() == 0
True
+
>>> makebug.initialize()
>>> print question_three.bugs[0].title
Bug title
+
>>> print question_three.bugs[0].description
Bug description.
+
>>> print makebug.user.name
name16
+
>>> question_three.bugs[0].isSubscribed(makebug.user)
True
+
>>> new_bug_id = int(question_three.bugs[0].id)
>>> message = [n.message for n in request.notifications]
>>> message
[u'Thank you! Bug #... created.']
+
>>> 'Bug #%s created.' % new_bug_id in message[0]
True
+
>>> notifications = pop_notifications()
>>> len(notifications)
1
+
>>> print notifications[0].get_payload(decode=True)
Your question #3...
...
@@ -368,8 +406,8 @@
"Bug title"
...
-
-If the question already has bugs linked to it, no new bug can be created.
+If the question already has bugs linked to it, no new bug can be
+created.
>>> request = LaunchpadTestRequest(
... form={'field.actions.create': 'create'})
@@ -381,16 +419,18 @@
You cannot create a bug report...
-== BugLinkView and BugsUnlinkView ==
+BugLinkView and BugsUnlinkView
+------------------------------
-Linking bug (+linkbug) to the question is managed through the BugLinkView.
-Unlinking bugs from the question is managed through the BugsUnlinkView.
-See 'buglinktarget-pages.txt' for their documentation. The notifications
-sent along linking and unlinking bugs can be found in
+Linking bug (+linkbug) to the question is managed through the
+BugLinkView. Unlinking bugs from the question is managed through the
+BugsUnlinkView. See 'buglinktarget-pages.txt' for their documentation.
+The notifications sent along linking and unlinking bugs can be found in
'answer-tracker-notifications.txt'.
-== QuestionRejectView ==
+QuestionRejectView
+------------------
That view is used by administrator and answer contacts to reject a
question.
@@ -405,11 +445,13 @@
>>> for notice in request.notifications:
... print notice.message
You have rejected this question.
+
>>> print firefox_question.status.title
Invalid
-== QuestionChangeStatusView ==
+QuestionChangeStatusView
+------------------------
QuestionChangeStatusView is used by administrator to change the status
outside of the comment workflow.
@@ -425,19 +467,22 @@
>>> for notice in request.notifications:
... print notice.message
Question status updated.
+
>>> print firefox_question.status.title
Solved
# Clear the notification.
+
>>> notifications = pop_notifications()
-== QuestionEditView ==
+QuestionEditView
+----------------
-QuestionEditView available through '+edit' is used to edit most
-question fields. It can be used to edit the question title and
-description and also its metadata like language, assignee,
-distribution, source package, product and whiteboard.
+QuestionEditView available through '+edit' is used to edit most question
+fields. It can be used to edit the question title and description and
+also its metadata like language, assignee, distribution, source package,
+product and whiteboard.
>>> login('test@xxxxxxxxxxxxx')
>>> request = LaunchpadTestRequest(form={
@@ -456,12 +501,16 @@
>>> view.initialize()
>>> question_three.title
u'Better Title'
+
>>> question_three.description
u'A better description.'
+
>>> print question_three.distribution.name
ubuntu
+
>>> print question_three.sourcepackagename.name
mozilla-firefox
+
>>> print question_three.product
None
@@ -471,6 +520,7 @@
>>> question_three.assignee is None
True
+
>>> question_three.whiteboard is None
True
@@ -512,6 +562,7 @@
>>> view.initialize()
>>> print question_three.assignee.displayname
Foo Bar
+
>>> print question_three.whiteboard
Some note
@@ -541,23 +592,30 @@
>>> view.initialize()
>>> view.errors
[]
+
>>> question_three.sourcepackagename is None
True
+
>>> print question_three.distribution
None
+
>>> print question_three.sourcepackagename
None
+
>>> print question_three.product.name
firefox
# Clear out the pending notifications.
+
>>> notifications = pop_notifications()
# Reassign back the question to ubuntu
+
>>> question_three.target = ubuntu
-== The QuestionLanguage vocabulary ==
+The QuestionLanguage vocabulary
+-------------------------------
The QuestionLanguageVocabularyFactory is an IContextSourceBinder which
is used in browser forms to create a vocabulary containing only the
@@ -567,11 +625,11 @@
will contain languages from the HTTP request, or the most likely
interesting languages based on GeoIP information.
-For example, if the user doesn't log in and his browser is configured
-to accept brazilian Portuguese, the vocabulary will contain the
-languages spoken in South Africa (because the 127.0.0.1 IP address is
-mapped to South Africa in the tests).
-.
+For example, if the user doesn't log in and his browser is configured to
+accept brazilian Portuguese, the vocabulary will contain the languages
+spoken in South Africa (because the 127.0.0.1 IP address is mapped to
+South Africa in the tests).
+
>>> login(ANONYMOUS)
>>> request = LaunchpadTestRequest(
... HTTP_ACCEPT_LANGUAGE='pt_BR')
@@ -596,8 +654,7 @@
>>> sorted(lang.code for lang in languages)
[u'af', u'en', u'pt_BR', u'st', u'xh', u'zu']
-But if the user configured his preferred languages, only these
-are used:
+But if the user configured his preferred languages, only these are used:
>>> login('carlos@xxxxxxxxxxxxx')
>>> user = getUtility(ILaunchBag).user
@@ -620,8 +677,8 @@
>>> sorted(lang.code for lang in user.languages)
[u'cy', u'en_GB', u'ja']
-But the vocabulary made from this languages has substituted the
-English variant with English:
+But the vocabulary made from this languages has substituted the English
+variant with English:
>>> vocab = QuestionLanguageVocabularyFactory(view)(None)
>>> languages = [term.value for term in vocab]
@@ -632,7 +689,7 @@
language in the vocabulary, even if this language would not be selected
by the previous rules.
- >>> from canonical.launchpad.interfaces import ILanguageSet
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
>>> afar = getUtility(ILanguageSet)['aa_DJ']
>>> question_three.language = afar
>>> vocab = QuestionLanguageVocabularyFactory(view)(question_three)
@@ -640,10 +697,12 @@
True
# Clean up.
+
>>> question_three.language = getUtility(ILanguageSet)['en']
-== UserSupportLanguagesMixin ==
+UserSupportLanguagesMixin
+-------------------------
The UserSupportLanguagesMixin can be used by views that needs to
retrieve the set of languages in which the user is assumed to be
@@ -659,13 +718,13 @@
The set of languages to use for support is defined in the
'user_support_languages' attribute.
-Like all operations involving languages in the Answer Tracker, we
-ignore all other English variants.
+Like all operations involving languages in the Answer Tracker, we ignore
+all other English variants.
When the user is not logged in, or didn't define his preferred
languages, the set will be initialized from the request. That's the
-languages configured in the browser, plus other inferred from the
-GeoIP database.
+languages configured in the browser, plus other inferred from the GeoIP
+database.
>>> request = LaunchpadTestRequest(
... HTTP_ACCEPT_LANGUAGE='fr, en_CA')
@@ -673,9 +732,9 @@
>>> login(ANONYMOUS)
>>> view = UserSupportLanguagesView(None, request)
-For this request, the set of support languages contains French
-(from the request), and the languages spoken in South Africa
-(inferred from the GeoIP location of the request).
+For this request, the set of support languages contains French (from the
+request), and the languages spoken in South Africa (inferred from the
+GeoIP location of the request).
>>> sorted(language.code for language in view.user_support_languages)
[u'af', u'en', u'fr', u'st', u'xh', u'zu']
@@ -705,14 +764,16 @@
[u'cy', u'en', u'ja']
-== SearchQuestionsView ==
+SearchQuestionsView
+-------------------
-This view is used as a base class to search for questions. It is intended
-to be easily customizable to offer more specific reports, while
+This view is used as a base class to search for questions. It is
+intended to be easily customizable to offer more specific reports, while
keeping those searchable.
# Define a subclass to demonstrate the customizability of the base
# view.
+
>>> from canonical.launchpad.browser import SearchQuestionsView
>>> class MyCustomSearchQuestionsView(SearchQuestionsView):
...
@@ -722,6 +783,7 @@
... return dict(**self.default_filter)
# Set up a harness for easier testing.
+
>>> from canonical.launchpad.ftests import LaunchpadFormHarness
>>> search_view_harness = LaunchpadFormHarness(
... ubuntu, MyCustomSearchQuestionsView)
@@ -731,8 +793,10 @@
>>> search_view = search_view_harness.view
>>> search_view.widgets.get('search_text') is not None
True
+
>>> search_view.widgets.get('language') is not None
True
+
>>> search_view.widgets.get('status') is not None
True
@@ -747,6 +811,7 @@
>>> questions = search_view.searchResults()
>>> questions
<canonical.launchpad.webapp.batching.BatchNavigator ...>
+
>>> for question in questions.batch:
... print question.title.encode('us-ascii', 'backslashreplace')
Problema al recompilar kernel con soporte smp (doble-n\xfacleo)
@@ -755,8 +820,8 @@
mailto: problem in webpage
Installation of Java Runtime Environment for Mozilla
-These were the default results when no search is entered. The user
-can tweak the search and filter the results:
+These were the default results when no search is entered. The user can
+tweak the search and filter the results:
>>> search_view_harness.submit('search', {
... 'field.status': ['SOLVED', 'OPEN'],
@@ -772,7 +837,7 @@
Specific views can provide a default filter by returning the default
search parameters to use in the getDefaultFilter() method:
- >>> from canonical.launchpad.interfaces import QuestionStatus
+ >>> from lp.answers.interfaces.questionenums import QuestionStatus
>>> MyCustomSearchQuestionsView.default_filter = {
... 'status': [QuestionStatus.SOLVED, QuestionStatus.INVALID],
... 'language' : search_view.user_support_languages}
@@ -808,6 +873,7 @@
>>> for question in questions.batch:
... print question.title
mailto: problem in webpage
+
>>> for status in search_view.widgets['status']._getFormValue():
... print status.title
Solved
@@ -819,6 +885,7 @@
>>> search_view_harness.submit('', {})
>>> print translate(search_view_harness.view.page_title)
Questions for Ubuntu
+
>>> print translate(search_view_harness.view.empty_listing_message)
There are no questions for Ubuntu with the requested statuses.
@@ -827,6 +894,7 @@
>>> search_view_harness.submit('', {})
>>> print translate(search_view_harness.view.page_title)
Open questions matching "Firefox" for Ubuntu
+
>>> print translate(search_view_harness.view.empty_listing_message)
There are no open questions matching "Firefox" for Ubuntu.
@@ -839,6 +907,7 @@
... 'field.sort': 'by relevancy'})
>>> print translate(search_view_harness.view.page_title)
Expired questions for Ubuntu
+
>>> print translate(search_view_harness.view.empty_listing_message)
There are no expired questions for Ubuntu.
@@ -849,12 +918,14 @@
... 'field.sort': 'by relevancy'})
>>> print translate(search_view_harness.view.page_title)
Questions matching "evolution" for Ubuntu
+
>>> print translate(search_view_harness.view.empty_listing_message)
There are no questions matching "evolution" for Ubuntu with the
requested statuses.
-=== Question listing table ===
+Question listing table
+......................
The SearchQuestionsView has two attributes that control the columns of
the question listing table. Products display the default columns of
@@ -869,8 +940,10 @@
... principal=question_three.owner)
>>> view.display_sourcepackage_column
False
+
>>> view.display_target_column
False
+
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
@@ -885,8 +958,10 @@
... principal=question_three.owner)
>>> view.display_sourcepackage_column
True
+
>>> view.display_target_column
False
+
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
@@ -904,16 +979,18 @@
... principal=question_three.owner)
>>> view.display_sourcepackage_column
False
+
>>> view.display_target_column
True
+
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
Summary Created Submitter In Assignee Status
6 ... 2005-10-14 Sample Person Mozilla Firefox — Answered...
-The Assignee column is always displayed. It contains The person assigned to
-the question, or an m-dash if there is no assignee.
+The Assignee column is always displayed. It contains The person assigned
+to the question, or an m-dash if there is no assignee.
>>> question_six = firefox.getQuestion(6)
>>> question_six.assignee = factory.makePerson(
@@ -923,8 +1000,10 @@
... principal=question_three.owner)
>>> view.display_sourcepackage_column
False
+
>>> view.display_target_column
False
+
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
@@ -933,11 +1012,12 @@
4 ... 2005-09-05 Foo Bar — Open ...
-== QuestionCollectionOpenCountView ==
+QuestionCollectionOpenCountView
+-------------------------------
-There is a helper view that is available on all IQuestionCollection
-that returns the number of questions in the Open and Needs information
-states on that target.
+There is a helper view that is available on all IQuestionCollection that
+returns the number of questions in the Open and Needs information states
+on that target.
>>> view = getMultiAdapter(
... (firefox, LaunchpadTestRequest()), name='+open_questions_count')
@@ -945,20 +1025,22 @@
u'3'
-== ManageAnswerContactView ==
+ManageAnswerContactView
+-----------------------
That view is used by a user to register himself or any team he
administrates as an answer contact for the project.
-Jeff Waugh is an administrator for the Ubuntu Team. Thus he can
-register himself or the Ubuntu Team as answer contact for ubuntu:
+Jeff Waugh is an administrator for the Ubuntu Team. Thus he can register
+himself or the Ubuntu Team as answer contact for ubuntu:
>>> list(ubuntu.answer_contacts)
[]
+
>>> login('jeff.waugh@xxxxxxxxxxxxxxx')
>>> jeff_waugh = getUtility(ILaunchBag).user
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
>>> jeff_waugh in ubuntu_team.getDirectAdministrators()
True
@@ -1026,8 +1108,8 @@
... print notification.message
You have been removed as an answer contact for Ubuntu.
-It can also be used to remove a team registration when the user is a team
-administrator:
+It can also be used to remove a team registration when the user is a
+team administrator:
>>> login('jeff.waugh@xxxxxxxxxxxxxxx')
>>> request = LaunchpadTestRequest(
@@ -1045,3 +1127,5 @@
>>> for notification in request.notifications:
... print notification.message
Ubuntu Team has been removed as an answer contact for Ubuntu.
+
+
=== modified file 'lib/lp/answers/configure.zcml'
--- lib/lp/answers/configure.zcml 2010-07-16 16:58:55 +0000
+++ lib/lp/answers/configure.zcml 2010-07-29 20:06:01 +0000
@@ -148,28 +148,28 @@
/>
<adapter
provides=".interfaces.faqtarget.IFAQTarget"
- for="canonical.launchpad.interfaces.IDistributionSourcePackage"
+ for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
factory=".adapters.distrosourcepackage_to_faqtarget"
/>
<adapter
- for="canonical.launchpad.interfaces.IDistroSeries"
+ for="lp.registry.interfaces.distroseries.IDistroSeries"
provides=".interfaces.questiontarget.IQuestionTarget"
factory=".adapters.series_to_questiontarget"
/>
<adapter
- for="canonical.launchpad.interfaces.IProductSeries"
+ for="lp.registry.interfaces.productseries.IProductSeries"
provides=".interfaces.questiontarget.IQuestionTarget"
factory=".adapters.series_to_questiontarget"
/>
<adapter
- for="canonical.launchpad.interfaces.ISourcePackageRelease"
+ for="lp.soyuz.interfaces.sourcepackagerelease.ISourcePackageRelease"
provides=".interfaces.questiontarget.IQuestionTarget"
factory=".adapters.sourcepackagerelease_to_questiontarget"
/>
<adapter
provides=".interfaces.faqtarget.IFAQTarget"
- for="canonical.launchpad.interfaces.ISourcePackage"
+ for="lp.registry.interfaces.sourcepackage.ISourcePackage"
factory=".adapters.sourcepackage_to_faqtarget"
/>
=== modified file 'lib/lp/answers/doc/emailinterface.txt.disabled'
--- lib/lp/answers/doc/emailinterface.txt.disabled 2009-05-12 01:39:29 +0000
+++ lib/lp/answers/doc/emailinterface.txt.disabled 2010-07-29 20:06:01 +0000
@@ -47,7 +47,7 @@
... raw_msg = '\n'.join(lines)
... msg = signed_message_from_string(raw_msg)
... if handler.process(msg, msg['To']):
- ... # Ensures that the DB user has the correct permission to
+ ... # Ensures that the DB user has the correct permission to \
... # saves the changes.
... flush_database_updates()
... return msgid
@@ -98,8 +98,8 @@
# be the question owner and Sample Person who will play the role of
# answer contact. Foo Bar is used to change the status of the
# question.
- >>> from canonical.launchpad.interfaces import (
- ... IDistributionSet, IPersonSet)
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> login('no-priv@xxxxxxxxxxxxx')
>>> personset = getUtility(IPersonSet)
>>> sample_person = personset.getByEmail('test@xxxxxxxxxxxxx')
@@ -121,7 +121,7 @@
>>> LaunchpadZopelessLayer.switchDbUser(config.processmail.dbuser)
# We need to refetch the question, since a new transaction was started.
- >>> from canonical.launchpad.interfaces import IQuestionSet
+ >>> from lp.answers.interfaces.questioncollection import IQuestionSet
>>> question = getUtility(IQuestionSet).get(question_id)
# Define an helper to change the question status easily.
@@ -161,7 +161,7 @@
And from the Needs information state:
- >>> from canonical.launchpad.interfaces import QuestionStatus
+ >>> from lp.answers.interfaces.questionenums import QuestionStatus
>>> setQuestionStatus(question, QuestionStatus.NEEDSINFO)
>>> msgid = send_question_email(
=== modified file 'lib/lp/answers/doc/expiration.txt'
--- lib/lp/answers/doc/expiration.txt 2010-04-01 04:29:46 +0000
+++ lib/lp/answers/doc/expiration.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= Questions Expiration =
+Questions Expiration
+====================
It is not productive to have questions lying around forever in
the Answer Tracker. That's why we have a script which runs daily to
@@ -19,9 +20,10 @@
# Sanity check in case somebody modifies the question sampledata and
# forget to update this script.
>>> from lp.answers.model.question import Question
- >>> from canonical.launchpad.interfaces import QuestionStatus
+ >>> from lp.answers.interfaces.questionenums import QuestionStatus
>>> Question.select('status IN (%i,%i)' % (
- ... QuestionStatus.OPEN.value, QuestionStatus.NEEDSINFO.value)).count()
+ ... QuestionStatus.OPEN.value,
+ ... QuestionStatus.NEEDSINFO.value)).count()
9
# By default, all open and needs info question should expire. Make
@@ -40,8 +42,9 @@
>>> now = datetime.now(UTC)
>>> two_weeks_ago = now - timedelta(days=14)
>>> a_month_ago = now - timedelta(days=31)
- >>> from canonical.launchpad.interfaces import (
- ... ILaunchBag, IPersonSet, IQuestionSet)
+ >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from lp.answers.interfaces.questioncollection import IQuestionSet
>>> login('no-priv@xxxxxxxxxxxxx')
>>> no_priv = getUtility(ILaunchBag).user
@@ -94,7 +97,8 @@
>>> removeSecurityProxy(old_open_question).faq = faq
# A question linked to an non-Invalid bug is not expirable.
- >>> from canonical.launchpad.interfaces import BugTaskStatus, IBugSet
+ >>> from lp.bugs.interfaces.bug import IBugSet
+ >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
>>> fixed_bug = getUtility(IBugSet).get(9)
>>> bugtasks = fixed_bug.bugtasks
>>> bugtasks[1].transitionToStatus(BugTaskStatus.INVALID, no_priv)
=== modified file 'lib/lp/answers/doc/faq.txt'
--- lib/lp/answers/doc/faq.txt 2010-02-17 14:42:16 +0000
+++ lib/lp/answers/doc/faq.txt 2010-07-29 20:06:01 +0000
@@ -1,22 +1,27 @@
-= Answer Tracker FAQ Documents =
-
-== IFAQTarget ==
+Answer Tracker FAQ Documents
+============================
+
+
+IFAQTarget
+----------
The Answer Tracker offers features to manage answers to frequently asked
questions (also called FAQ). Like the regular questions, FAQ documents
-are associated to distributions or products. The IFAQTarget interface
-is provided by objects that can host FAQs.
+are associated to distributions or products. The IFAQTarget interface is
+provided by objects that can host FAQs.
>>> from canonical.launchpad.webapp.testing import verifyObject
>>> from zope.security.proxy import removeSecurityProxy
- >>> from canonical.launchpad.interfaces import (
- ... IDistributionSet, IProductSet, IFAQTarget)
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> from lp.answers.interfaces.faqtarget import IFAQTarget
>>> firefox = getUtility(IProductSet).getByName('firefox')
# removeSecurityProxy() is needed because not all interface
# attributes are available to everybody.
+
>>> verifyObject(IFAQTarget, removeSecurityProxy(firefox))
True
@@ -30,7 +35,7 @@
>>> login('test@xxxxxxxxxxxxx')
- >>> from canonical.launchpad.interfaces import ILaunchBag
+ >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
>>> sample_person = getUtility(ILaunchBag).user
>>> print firefox.owner.displayname
Sample Person
@@ -43,7 +48,8 @@
lib/canonical/launchpad/interfaces/ftests/faqtarget.txt)
-=== IFAQTarget adapters ===
+IFAQTarget adapters
+...................
Convenient adapters are available so that is possible to easily retrieve
a suitable IFAQTarget from objects that do not provide it directly.
@@ -54,6 +60,7 @@
>>> mozilla_firefox = ubuntu.getSourcePackage('mozilla-firefox')
>>> IFAQTarget.providedBy(mozilla_firefox)
False
+
>>> mozilla_firefox_faq_target = IFAQTarget(mozilla_firefox)
>>> verifyObject(
... IFAQTarget, removeSecurityProxy(mozilla_firefox_faq_target))
@@ -65,6 +72,7 @@
>>> hoary_mozilla_firefox = hoary.getSourcePackage('mozilla-firefox')
>>> IFAQTarget.providedBy(hoary_mozilla_firefox)
False
+
>>> hoary_firefox_faq_target = IFAQTarget(hoary_mozilla_firefox)
>>> verifyObject(
... IFAQTarget, removeSecurityProxy(hoary_firefox_faq_target))
@@ -76,6 +84,7 @@
>>> firefox_question = firefox.getQuestion(1)
>>> IFAQTarget.providedBy(firefox_question)
False
+
>>> question_faq_target = IFAQTarget(firefox_question)
>>> verifyObject(IFAQTarget, removeSecurityProxy(question_faq_target))
True
@@ -87,19 +96,22 @@
True
-== IFAQCollection ==
+IFAQCollection
+--------------
The IFAQCollection interface is provided by objects that represents a
collection of FAQs. This interface can be used to retrieve and search
for FAQs. It is provided by product, distribution, and projects.
- >>> from canonical.launchpad.interfaces import (
- ... IFAQCollection, IProjectGroupSet)
+ >>> from lp.answers.interfaces.faqcollection import IFAQCollection
+ >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
>>> gnome = getUtility(IProjectGroupSet).getByName('gnome')
>>> verifyObject(IFAQCollection, gnome)
True
+
>>> verifyObject(IFAQCollection, ubuntu)
True
+
>>> verifyObject(IFAQCollection, firefox)
True
@@ -107,11 +119,12 @@
lib/canonical/launchpad/interfaces/ftests/faqcollection.txt)
-== IFAQ ==
+IFAQ
+----
FAQ document provides the IFAQ interface.
- >>> from canonical.launchpad.interfaces import IFAQ
+ >>> from lp.answers.interfaces.faq import IFAQ
>>> verifyObject(IFAQ, firefox_faq)
True
@@ -119,6 +132,7 @@
>>> print firefox_faq.title
How can I see the Fnords?
+
>>> print firefox_faq.content
Install the Fnords highlighter extension and see the Fnords!
@@ -136,6 +150,7 @@
>>> print firefox_faq.last_updated_by
None
+
>>> print firefox_faq.date_last_updated
None
@@ -152,11 +167,13 @@
>>> print firefox_faq.last_updated_by.displayname
Sample Person
+
>>> firefox_faq.date_last_updated is not None
True
-=== IFAQ permissions ===
+IFAQ permissions
+................
Only the project owners or answer contacts can edit an IFAQ.
@@ -171,6 +188,7 @@
>>> login('test@xxxxxxxxxxxxx')
>>> print firefox.owner.displayname
Sample Person
+
>>> check_permission('launchpad.Edit', firefox_faq)
True
@@ -183,23 +201,26 @@
But if we make him an answer contact, he will:
# An answer contact needs a preferred language.
- >>> from canonical.launchpad.interfaces import (ILanguageSet)
+
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
>>> no_priv = getUtility(ILaunchBag).user
>>> no_priv.addLanguage(getUtility(ILanguageSet)['en'])
>>> firefox.addAnswerContact(no_priv)
True
+
>>> from canonical.launchpad.webapp.authorization import clear_cache
>>> clear_cache()
>>> check_permission('launchpad.Edit', firefox_faq)
True
-== IFAQSet ==
+IFAQSet
+-------
There is a global utility registered under the IFAQSet interface that
can be used to retrieve all FAQs posted on Launchpad.
- >>> from canonical.launchpad.interfaces import IFAQSet
+ >>> from lp.answers.interfaces.faq import IFAQSet
>>> faqset = getUtility(IFAQSet)
>>> verifyObject(IFAQSet, faqset)
True
@@ -216,7 +237,7 @@
The searchFAQs() method can be used to find FAQs by keywords or owner.
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
>>> for faq in faqset.searchFAQs(
... search_text='java | flash', owner=foo_bar):
@@ -225,11 +246,12 @@
How can I play MP3/Divx/DVDs/Quicktime/Realmedia files
or view Flash/Java web pages (Ubuntu)
-(See lib/canonical/launchpad/interfaces/ftests/faqcollection.txt for
-the full interface description.)
-
-
-== Linking a FAQ to a question ==
+(See lib/canonical/launchpad/interfaces/ftests/faqcollection.txt for the
+full interface description.)
+
+
+Linking a FAQ to a question
+---------------------------
An IFAQ can be used to answer a question. The linkFAQ() method on
IQuestion is used for that purpose. It takes as parameters the user
@@ -251,6 +273,7 @@
>>> print message.action.title
Answer
+
>>> print fnord_question.status.title
Answered
@@ -263,6 +286,7 @@
answered by the FAQ:
# Flush the faq attribute change.
+
>>> from canonical.launchpad.ftests import syncUpdate
>>> syncUpdate(fnord_question)
@@ -279,6 +303,7 @@
>>> print other_question.faq.title
How can I see the Fnords?
+
>>> print other_question.status.title
Answered
@@ -307,6 +332,7 @@
>>> print message.action.title
Answer
+
>>> print other_question.status.title
Answered
@@ -325,8 +351,8 @@
...
AssertionError: cannot call linkFAQ() with already linked FAQ
-A FAQ can be linked to a 'solved' question, in which case, the
-status is not changed.
+A FAQ can be linked to a 'solved' question, in which case, the status is
+not changed.
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> confirm_message = other_question.confirmAnswer(
@@ -340,5 +366,8 @@
... 'If you look carefully, you will find the fnords!')
>>> print message.action.title
Comment
+
>>> print other_question.status.title
Solved
+
+
=== modified file 'lib/lp/answers/doc/faqcollection.txt'
--- lib/lp/answers/doc/faqcollection.txt 2009-03-24 12:43:49 +0000
+++ lib/lp/answers/doc/faqcollection.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= IFAQCollection Interface =
+IFAQCollection Interface
+========================
The Launchpad Answer Tracker can be used to track answers to commonly
asked questions. Regular user questions can then be answered with a
@@ -6,27 +7,28 @@
Objects that represents collection of FAQs provides the IFAQCollection
interface. (The test harness is responsible for providing an object
-providing this interface in the 'collection' variable of the test
-name space. That way we can verify that multiple implementation provide
-the interface correctly.)
+providing this interface in the 'collection' variable of the test name
+space. That way we can verify that multiple implementation provide the
+interface correctly.)
>>> from zope.interface.verify import verifyObject
- >>> from canonical.launchpad.interfaces import IFAQCollection
+ >>> from lp.answers.interfaces.faqcollection import IFAQCollection
>>> verifyObject(IFAQCollection, collection)
True
-== Population FAQs collection ==
+Population FAQs collection
+--------------------------
The IFAQCollection interface is a read-only interface. The IFAQTarget
interface is used for creating FAQs (see faqtarget.txt for details).
-Since not all IFAQCollections are IFAQTarget, we rely on the harness
-to provide us with a newFAQ() function that can be used to add a FAQ to
-the collection.
+Since not all IFAQCollections are IFAQTarget, we rely on the harness to
+provide us with a newFAQ() function that can be used to add a FAQ to the
+collection.
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> personset = getUtility(IPersonSet)
>>> no_priv = personset.getByName('no-priv')
>>> foo_bar = personset.getByEmail('foo.bar@xxxxxxxxxxxxx')
@@ -48,7 +50,7 @@
... 'in Dallas on Nov 22nd 1963.', None),
... (no_priv, 'What were the famous last words?',
... 'Who turned off the light?', None)
- ... ]
+ ... ]
>>> from datetime import datetime, timedelta
>>> from pytz import UTC
@@ -62,7 +64,8 @@
>>> login(ANONYMOUS)
-== getFAQ() ==
+getFAQ()
+--------
It is possible to retrieve a FAQ in a collection using its id by using
the getFAQ() method.
@@ -79,11 +82,12 @@
It also returns None when using the ID of a FAQ that isn't in the
requested collection:
- >>> from canonical.launchpad.interfaces import (
- ... IDistributionSet, ILaunchBag)
+ >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> ubuntu != collection
True
+
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> foo_bar = getUtility(ILaunchBag).user
>>> ubuntu_faq = ubuntu.newFAQ(
@@ -96,7 +100,8 @@
None
-== searchFAQs ==
+searchFAQs
+----------
The searchFAQs() method is used to select a set of FAQs in the
collection matching various criteria.
@@ -114,7 +119,8 @@
How do I install Foo?
-=== search_text ===
+search_text
+...........
The first criteria is search_text. It will select FAQs matching the
keywords specified. Keywords are looked for in the title, content and
@@ -131,7 +137,8 @@
title, the second because it appears in the keywords.
-=== owner ===
+owner
+.....
The other filtering criteria is 'owner'. It will select only FAQs that
were created by the specified user.
@@ -145,26 +152,29 @@
Again, the default sort order is most recent first.
-=== Combination ===
+Combination
+...........
You can combine multiple criterias. Only FAQs matching all the criterias
+
will be returned.
>>> for faq in collection.searchFAQs(
... search_text='install', owner=no_priv):
... print faq.title
- How do I install Foo?
+ How do I install Foo?
How do I play the Game of Life?
-=== sort ===
+sort
+....
The sort parameter can be used to control the sort order of the results.
It takes a value from the FAQSort enumerated type. For example, the
-FAQSort.NEWEST_FIRST can be used to sort the results of a text
-search by date of creation (most recent first):
+FAQSort.NEWEST_FIRST can be used to sort the results of a text search by
+date of creation (most recent first):
- >>> from canonical.launchpad.interfaces import FAQSort
+ >>> from lp.answers.interfaces.faqcollection import FAQSort
>>> for faq in collection.searchFAQs(
... search_text='install', sort=FAQSort.NEWEST_FIRST):
... print faq.title
@@ -184,3 +194,4 @@
Who really shot JFK?
What were the famous last words?
+
=== modified file 'lib/lp/answers/doc/faqtarget.txt'
--- lib/lp/answers/doc/faqtarget.txt 2010-07-21 19:55:38 +0000
+++ lib/lp/answers/doc/faqtarget.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= IFAQTarget Interface =
+IFAQTarget Interface
+====================
The Launchpad Answer Tracker can be used to track answers to commonly
asked questions. Regular user questions can then be answered with a
@@ -8,15 +9,17 @@
>>> from zope.interface.verify import verifyObject
>>> from zope.security.proxy import removeSecurityProxy
- >>> from canonical.launchpad.interfaces import IFAQTarget
+ >>> from lp.answers.interfaces.faqtarget import IFAQTarget
# removeSecurityProxy() is needed because some attributes are
# protected.
+
>>> verifyObject(IFAQTarget, removeSecurityProxy(target))
True
-== newFAQ() ===
+newFAQ()
+--------
The newFAQ() method is used to create a new IFAQ object on the target.
@@ -24,7 +27,7 @@
the target.
>>> login('no-priv@xxxxxxxxxxxxx')
- >>> from canonical.launchpad.interfaces import ILaunchBag
+ >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
>>> from canonical.launchpad.webapp.authorization import check_permission
>>> check_permission('launchpad.Moderate', target)
False
@@ -41,19 +44,23 @@
>>> old_owner = target.owner
# Usually, only the previous owner can change this.
+
>>> removeSecurityProxy(target).owner = no_priv
>>> from canonical.launchpad.webapp.authorization import clear_cache
>>> clear_cache() # clear authorization cache for check_permission
>>> check_permission('launchpad.Moderate', target)
True
+
>>> removeSecurityProxy(target).owner = old_owner
# An answer contact must have a preferred language registered.
- >>> from canonical.launchpad.interfaces import ILanguageSet
+
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
>>> no_priv.addLanguage(getUtility(ILanguageSet)['en'])
>>> clear_cache() # clear authorization cache for check_permission
>>> target.addAnswerContact(no_priv)
True
+
>>> clear_cache() # clear authorization cache for check_permission
>>> check_permission('launchpad.Moderate', target)
True
@@ -62,14 +69,14 @@
The returned object provides the IFAQ interface:
- >>> from canonical.launchpad.interfaces import IFAQ
+ >>> from lp.answers.interfaces.faq import IFAQ
>>> verifyObject(IFAQ, faq)
True
The newFAQ() requires an owner, title, and content parameter. It also
-accepts an optional date_created attribute (which defaults to
-the current time), and an optional keywords parameter used to initialize
-the FAQ's keywords.
+accepts an optional date_created attribute (which defaults to the
+current time), and an optional keywords parameter used to initialize the
+FAQ's keywords.
>>> from datetime import datetime
>>> from pytz import UTC
@@ -81,12 +88,16 @@
>>> print faq.owner.displayname
No Privileges Person
+
>>> print faq.title
How to do something
+
>>> print faq.content
Explain how to do something.
+
>>> print faq.keywords
documentation howto
+
>>> faq.date_created == now
True
@@ -97,10 +108,11 @@
True
-== getFAQ() ==
+getFAQ()
+--------
-It is possible to retrieve the FAQ from its container when you know
-the id of the FAQ by using the get() method.
+It is possible to retrieve the FAQ from its container when you know the
+id of the FAQ by using the get() method.
>>> target.getFAQ(faq.id) == faq
True
@@ -114,11 +126,13 @@
requested target:
# Create a FAQ on Ubuntu.
- >>> from canonical.launchpad.interfaces import (
- ... IDistributionSet, ILaunchBag)
+
+ >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> ubuntu != target
True
+
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> foo_bar = getUtility(ILaunchBag).user
>>> ubuntu_faq = ubuntu.newFAQ(
@@ -131,18 +145,20 @@
None
-== findSimilarFAQs() ==
+findSimilarFAQs()
+-----------------
The method findSimilarFAQs() can be use to find FAQ document that are
likely to answer a particular question. The question's summary or a
sentence describing the issue should be given in parameter. The FAQ's
title, summary, keywords and content can be the source of the match.
-This method uses a "natural language" search algorithm
-(see lib/canonical/doc/textsearching.txt for the details) which ignore
-common words and stop words.
+This method uses a "natural language" search algorithm (see
+lib/canonical/doc/textsearching.txt for the details) which ignore common
+words and stop words.
# Create more FAQs.
+
>>> faq = target.newFAQ(
... no_priv, 'How to answer a question',
... 'Description on how to use the Answer Tracker can be found at: '
=== modified file 'lib/lp/answers/doc/karma.txt'
--- lib/lp/answers/doc/karma.txt 2010-02-16 20:36:48 +0000
+++ lib/lp/answers/doc/karma.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= Answer Tracker Karma =
+Answer Tracker Karma
+====================
To promote community contributions in the Launchpad Answer Tracker, it's
very important that we acknowledge their work and give them some karma
@@ -7,8 +8,8 @@
These karma points are assigned to a user when he performs one of the
actions we consider to be a reasonable contribution.
- >>> from canonical.launchpad.interfaces import (
- ... IPersonSet, IProductSet)
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from lp.registry.interfaces.product import IProductSet
>>> from lp.registry.model.karma import KarmaCategory
>>> answers_category = KarmaCategory.byName('answers')
>>> answers_karma_actions = answers_category.karmaactions
@@ -29,8 +30,9 @@
u'Requested for information on a question',
u'Solved own question']
- >>> sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
- >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
+ >>> person_set = getUtility(IPersonSet)
+ >>> sample_person = person_set.getByEmail('test@xxxxxxxxxxxxx')
+ >>> foo_bar = person_set.getByEmail('foo.bar@xxxxxxxxxxxxx')
Setup an event listener to help ensure karma is assigned when it should.
@@ -51,10 +53,12 @@
>>> now = timegenerator(datetime.now(UTC))
-== Karma Actions ==
-
-
-=== Creating a question ===
+Karma Actions
+-------------
+
+
+Creating a question
+...................
>>> login('test@xxxxxxxxxxxxx')
>>> from zope.event import notify
@@ -65,7 +69,8 @@
Karma added: action=questionasked, product=firefox, person=name12
-=== Expiring a question ===
+Expiring a question
+...................
The expireQuestion() workflow method doesn't grant any karma because it
will usually be called by an automated script.
@@ -76,7 +81,8 @@
... datecreated=now.next())
-=== Reopening a question ===
+Reopening a question
+....................
>>> msg = firefox_question.reopen(
... "Firefox doesn't have any 'Quick Searches' in its bookmarks.",
@@ -84,7 +90,8 @@
Karma added: action=questionreopened, product=firefox, person=name12
-=== Requesting for more information ===
+Requesting for more information
+...............................
>>> msg = firefox_question.requestInfo(
... foo_bar, 'What "Quick Searches" do you want?',
@@ -92,7 +99,8 @@
Karma added: action=questionrequestedinfo, product=firefox, person=name16
-=== Giving back more information ===
+Giving back more information
+............................
>>> msg = firefox_question.giveInfo(
... 'The same one than shipped upstreams.',
@@ -100,7 +108,8 @@
Karma added: action=questiongaveinfo, product=firefox, person=name12
-=== Giving an answer to a question ===
+Giving an answer to a question
+..............................
>>> msg = firefox_question.giveAnswer(
... foo_bar, "Ok, I see what you mean. You need to install them "
@@ -108,7 +117,8 @@
Karma added: action=questiongaveanswer, product=firefox, person=name16
-=== Adding a comment ===
+Adding a comment
+................
>>> msg = firefox_question.addComment(
... foo_bar, 'You could also fill a bug about that, if you like.',
@@ -116,11 +126,12 @@
Karma added: action=questioncommentadded, product=firefox, person=name16
-=== Confirming that the problem is solved ===
+Confirming that the problem is solved
+.....................................
When the user confirms that his problem is solved, karma will be given
-for accepting an answer. The person whose answer was accepted will
-also receives karma.
+for accepting an answer. The person whose answer was accepted will also
+receives karma.
>>> msg = firefox_question.confirmAnswer(
... "Ok, thanks. I'll open a bug about this then.",
@@ -129,25 +140,28 @@
Karma added: action=questionanswered, product=firefox, person=name16
-=== Rejecting a question ===
+Rejecting a question
+....................
>>> msg = firefox_question.reject(
... foo_bar, 'This should really be a bug report.')
Karma added: action=questionrejected, product=firefox, person=name16
-=== Changing the status ===
+Changing the status
+...................
We do not grant karma for status change made outside of workflow:
>>> login('foo.bar@xxxxxxxxxxxxx')
- >>> from canonical.launchpad.interfaces import QuestionStatus
+ >>> from lp.answers.interfaces.questionenums import QuestionStatus
>>> msg = firefox_question.setStatus(
... foo_bar, QuestionStatus.OPEN, 'That rejection was an error.',
... datecreated=now.next())
-=== Changing the title of a question ===
+Changing the title of a question
+................................
>>> from zope.interface import providedBy
>>> from lazr.lifecycle.event import ObjectModifiedEvent
@@ -161,7 +175,8 @@
Karma added: action=questiontitlechanged, product=firefox, person=name12
-=== Changing the description of a question ===
+Changing the description of a question
+......................................
>>> old_question = Snapshot(
... firefox_question, providing=providedBy(firefox_question))
@@ -174,36 +189,41 @@
person=name12
-=== Linking to a bug ===
+Linking to a bug
+................
>>> from canonical.launchpad.database import Bug
>>> questionbug = firefox_question.linkBug(Bug.get(5))
Karma added: action=questionlinkedtobug, product=firefox, person=name12
-=== Solving own problem ===
+Solving own problem
+...................
There is a special karma action to cover the case when the question
owner comes back to provide an answer to his own problem. We no longer
-award karma for the questionownersolved action. It remains among the
+award karma for the questionownersolved action. It remains among the
answers karma actions so that we can continue to calculate karma for
persons who were awarded it in the past.
# This test must have no output
+
>>> msg = firefox_question.giveAnswer(
... sample_person, "I was able to import some by following the "
... "instructions on http://tinyurl.com/cyus4",
... datecreated=now.next())
-=== Creating a FAQ ===
+Creating a FAQ
+..............
>>> firefox_faq = firefox.newFAQ(
... sample_person, 'A FAQ', 'About something important')
Karma added: action=faqcreated, product=firefox, person=name12
-=== Modifying a FAQ ===
+Modifying a FAQ
+...............
>>> old_faq = Snapshot(firefox_faq, providing=providedBy(firefox_faq))
>>> firefox_faq.title = 'How can I make the Fnord appears?'
@@ -213,10 +233,11 @@
Karma added: action=faqedited, product=firefox, person=name12
-== Final check and tear down ==
+Final check and tear down
+-------------------------
-Now we do a check to make sure all current Answer Tracker related
-karma actions have been tested. Only the obsolete methods remain.
+Now we do a check to make sure all current Answer Tracker related karma
+actions have been tested. Only the obsolete methods remain.
>>> added_karma_actions = karma_helper.added_karma_actions
>>> obsolete_actions = set(answers_karma_actions) - added_karma_actions
@@ -225,4 +246,7 @@
# Unregister the event listener to make sure we won't interfere in
# other tests.
+
>>> karma_helper.unregister_listener()
+
+
=== modified file 'lib/lp/answers/doc/notifications.txt'
--- lib/lp/answers/doc/notifications.txt 2009-07-01 13:16:44 +0000
+++ lib/lp/answers/doc/notifications.txt 2010-07-29 20:06:01 +0000
@@ -1,13 +1,14 @@
-= Answer Tracker Email Notifications =
+Answer Tracker Email Notifications
+==================================
-When a question is created or changed, an email notification is sent out,
-informing the subscribers and the answer contacts about the change.
+When a question is created or changed, an email notification is sent
+out, informing the subscribers and the answer contacts about the change.
Let's start with creating a question, and see what the resulting
notification looks like:
>>> from zope.event import notify
>>> from lazr.lifecycle.event import ObjectCreatedEvent
- >>> from canonical.launchpad.interfaces import IDistributionSet
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from lp.testing.mail_helpers import pop_notifications
>>> login('test@xxxxxxxxxxxxx')
>>> sample_person = getUtility(ILaunchBag).user
@@ -18,33 +19,37 @@
The notifications get sent to the question's subscribers, the question's
target answer contacts as well as to the question's assignee. Initially,
-only the submitter, Sample Person, is subscribed to the question and there
-is no answer contact registered on Ubuntu, so only 1 notification is
-sent:
+only the submitter, Sample Person, is subscribed to the question and
+there is no answer contact registered on Ubuntu, so only 1 notification
+is sent:
>>> ubuntu.answer_contacts
[]
+
>>> [sub.person.displayname for sub in ubuntu_question.subscriptions]
[u'Sample Person']
+
>>> notifications = pop_notifications()
>>> len(notifications)
1
-Note that the From address uses the submitter's name with the
-question's email address. Answer's uses real names to help the
-questioner and the answerers communicate. The Reply-To address
-is the question's address. When using the mail client's reply
-feature, the user should clearly see that he is replying to the
-question and not the user.
-[sic] Kiko and Danilo have a story worth telling.
+Note that the From address uses the submitter's name with the question's
+email address. Answer's uses real names to help the questioner and the
+answerers communicate. The Reply-To address is the question's address.
+When using the mail client's reply feature, the user should clearly see
+that he is replying to the question and not the user. [sic] Kiko and
+Danilo have a story worth telling.
>>> add_notification = notifications[0]
>>> add_notification['From']
'Sample Person <question...@xxxxxxxxxxxxxxxxxxxxx>'
+
>>> add_notification['Reply-To']
'question...@xxxxxxxxxxxxxxxxxxxxx'
+
>>> add_notification['To']
'test@xxxxxxxxxxxxx'
+
>>> add_notification['Subject']
"[Question #...]: Can't install Ubuntu"
@@ -78,7 +83,8 @@
Register the Ubuntu Team as Ubuntu's answer contact, so that they get
notified about the changes as well:
- >>> from canonical.launchpad.interfaces import ILanguageSet, IPersonSet
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
>>> ubuntu_team.addLanguage(getUtility(ILanguageSet)['en'])
>>> ubuntu.addAnswerContact(ubuntu_team)
@@ -90,7 +96,9 @@
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> ubuntu_question.assignee = getUtility(ILaunchBag).user
-== Edit Notifications ==
+
+Edit Notifications
+------------------
If we edit the title and description of the question, a notification
will be sent.
@@ -126,6 +134,7 @@
>>> notification_body = edit_notification.get_payload(decode=True)
>>> print edit_notification['Subject']
[Question #...]: Installer doesn't work on a Mac
+
>>> print notification_body #doctest: -NORMALIZE_WHITESPACE
Question #... libstdc++ in ubuntu changed:
http://.../ubuntu/+source/libstdc++/+question/...
@@ -145,10 +154,10 @@
You received this question notification because you are the assignee for
this question.
-# XXX flacoste 2006-09-19: Add checks for notification of change to
-# status whiteboard, priority. For example, if a question is
-# transferred to another QuestionTarget and priority is changed,
-# the notification does not include priority.
+# XXX flacoste 2006-09-19: Add checks for notification of change to #
+status whiteboard, priority. For example, if a question is # transferred
+to another QuestionTarget and priority is changed, # the notification
+does not include priority.
>>> unmodified_question = Snapshot(
... ubuntu_question, providing=providedBy(ubuntu_question))
@@ -195,8 +204,8 @@
You received this question notification because you are the assignee for
this question.
-If we trigger a modification event when no changes worth
-notifying about was made, no notification is sent:
+If we trigger a modification event when no changes worth notifying about
+was made, no notification is sent:
>>> unmodified_question = Snapshot(
... ubuntu_question, providing=providedBy(ubuntu_question))
@@ -211,14 +220,18 @@
>>> ubuntu_question.assignee = None
-== Bug Linking and Unlinking Notifications ==
-
-=== Bug link Notification ===
-
-If we create a bug from the question, it will be reported as a
-bug that has been linked to it:
-
- >>> from canonical.launchpad.interfaces import CreateBugParams
+
+Bug Linking and Unlinking Notifications
+---------------------------------------
+
+
+Bug link Notification
+.....................
+
+If we create a bug from the question, it will be reported as a bug that
+has been linked to it:
+
+ >>> from lp.bugs.interfaces.bug import CreateBugParams
>>> login('no-priv@xxxxxxxxxxxxx')
>>> unmodified_question = Snapshot(
@@ -229,12 +242,14 @@
>>> bug = ubuntu_question.target.createBug(params)
>>> ubuntu_question.linkBug(bug)
<QuestionBug...>
+
>>> notify(ObjectModifiedEvent(
... ubuntu_question, unmodified_question, ['bugs']))
>>> notifications = pop_notifications()
>>> len(notifications)
2
+
>>> edit_notification = notifications[0]
>>> notification_body = edit_notification.get_payload(decode=True)
>>> print notification_body #doctest: -NORMALIZE_WHITESPACE
@@ -249,7 +264,9 @@
You received this question notification because you are a member of
Ubuntu Team, which is an answer contact for Ubuntu.
-=== Bug Unlinked Notification ===
+
+Bug Unlinked Notification
+.........................
A notification is also sent when a bug is unlinked from the question:
@@ -257,6 +274,7 @@
... providing=providedBy(ubuntu_question))
>>> ubuntu_question.unlinkBug(bug)
<QuestionBug...>
+
>>> notify(ObjectModifiedEvent(
... ubuntu_question, unmodified_question, ['bugs']))
@@ -279,14 +297,16 @@
Ubuntu Team, which is an answer contact for Ubuntu.
-=== Linked Bug Status Changed Notification ===
+Linked Bug Status Changed Notification
+......................................
When a question is linked to a bug, the question's subscribers are
-notified of changes of the bug status. See
-answer-tracker-notifications-linked-bug.txt for more information.
-
-
-== Workflow Notifications ==
+notified of changes of the bug status. See answer-tracker-notifications-
+linked-bug.txt for more information.
+
+
+Workflow Notifications
+----------------------
Notifications are also sent when workflow actions are done on questions.
The content of the notification will be different depending on the
@@ -298,6 +318,7 @@
>>> notifications = pop_notifications()
>>> [email_msg['To'] for email_msg in notifications]
['support@xxxxxxxxxx', 'test@xxxxxxxxxxxxx']
+
>>> support_notification = notifications[0]
>>> support_notification['Subject']
"Re: [Question #...]: Installer doesn't work on a Mac"
@@ -322,8 +343,8 @@
You received this question notification because you are a member of
Ubuntu Team, which is an answer contact for Ubuntu.
-But the owner notification has a slightly different preamble and has
-an extra footer.
+But the owner notification has a slightly different preamble and has an
+extra footer.
>>> notification_body = notifications[1].get_payload(decode=True)
>>> print notification_body #doctest: -NORMALIZE_WHITESPACE
@@ -371,9 +392,11 @@
The notification for new messages on the question contain a 'References'
header to the previous message for threading purpose.
- >>> print notifications[0]['References']
+ >>> references = notifications[0]['References']
+ >>> print references
<...>
- >>> notifications[0]['References'] == ubuntu_question.messages[-2].rfc822msgid
+
+ >>> references == ubuntu_question.messages[-2].rfc822msgid
True
We already saw the notifications sent for the requestInfo() and
@@ -381,10 +404,13 @@
# Subscribe the owner back, to compare the different notifications
# sent.
+
>>> ubuntu_question.subscribe(sample_person)
<QuestionSubscription ...>
-=== Notifications for expireQuestion() ===
+
+Notifications for expireQuestion()
+..................................
>>> login('no-priv@xxxxxxxxxxxxx')
>>> message = ubuntu_question.expireQuestion(
@@ -431,13 +457,15 @@
You received this question notification because you are a direct
subscriber of the question.
-=== Notifications for reopen() ===
+
+Notifications for reopen()
+..........................
(This example will also show that comments are wrapped for 72 columns
display.)
>>> login('test@xxxxxxxxxxxxx')
- >>> from canonical.launchpad.interfaces import IMessageSet
+ >>> from canonical.launchpad.interfaces.message import IMessageSet
>>> email_msg = getUtility(IMessageSet).fromText(
... subject=(
... "Re: [Question %d]: Installer doesn't work on "
@@ -496,7 +524,9 @@
You received this question notification because you are a direct
subscriber of the question.
-=== Notifications for giveAnswer() ===
+
+Notifications for giveAnswer()
+..............................
>>> login('no-priv@xxxxxxxxxxxxx')
>>> answer_message = ubuntu_question.giveAnswer(
@@ -561,7 +591,9 @@
You received this question notification because you are a direct
subscriber of the question.
-=== Notifications for confirm() ===
+
+Notifications for confirm()
+...........................
>>> login('test@xxxxxxxxxxxxx')
>>> message = ubuntu_question.confirmAnswer(
@@ -604,7 +636,9 @@
You received this question notification because you are a direct
subscriber of the question.
-=== Notifications for addComment() ===
+
+Notifications for addComment()
+..............................
>>> login('no-priv@xxxxxxxxxxxxx')
>>> message = ubuntu_question.addComment(
@@ -645,7 +679,9 @@
You received this question notification because you are a direct
subscriber of the question.
-=== Notifications for reject() ===
+
+Notifications for reject()
+..........................
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> foo_bar = getUtility(ILaunchBag).user
@@ -693,9 +729,11 @@
You received this question notification because you are a direct
subscriber of the question.
-=== Notifications for setStatus() ===
-
- >>> from canonical.launchpad.interfaces import QuestionStatus
+
+Notifications for setStatus()
+.............................
+
+ >>> from lp.answers.interfaces.questionenums import QuestionStatus
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> message = ubuntu_question.setStatus(
... foo_bar, QuestionStatus.SOLVED, "The rejection was a mistake.")
@@ -737,24 +775,27 @@
subscriber of the question.
-=== Notifications for linkFAQ() ===
+Notifications for linkFAQ()
+...........................
When a user links a FAQ to a question, the notification includes that
information before the message.
>>> login('no-priv@xxxxxxxxxxxxx')
- >>> from canonical.launchpad.interfaces import IProductSet
+ >>> from lp.registry.interfaces.product import IProductSet
>>> firefox = getUtility(IProductSet).getByName('firefox')
>>> firefox_question = firefox.newQuestion(
... no_priv, 'How can I play Flash?', 'I want Flash!')
# Discard notifications.
+
>>> notifications = pop_notifications()
>>> login('test@xxxxxxxxxxxxx')
>>> firefox_faq = firefox.getFAQ(10)
>>> print firefox_faq.title
How do I install plugins (Shockwave, QuickTime, etc.)?
+
>>> message = firefox_question.linkFAQ(
... sample_person, firefox_faq, "Read the FAQ.")
@@ -802,10 +843,11 @@
--...
-== Notifications for convertToQuestion() ==
+Notifications for convertToQuestion()
+-------------------------------------
-Answer contacts and the bug owner is notified when questions are
-created from bugs just like when a question is normally created.
+Answer contacts and the bug owner is notified when questions are created
+from bugs just like when a question is normally created.
>>> bug_question = ubuntu.createQuestionFromBug(bug)
>>> notifications = pop_notifications()
@@ -830,13 +872,14 @@
--...
-== Notifications and Teams ==
+Notifications and Teams
+-----------------------
-When a team is subscribed to a question, there are two cases two consider.
-The first one is if the team has an email address set, a notification
-will only be sent to that address. (That email address is assumed to
-be a mailing list reaching all the team members.) We already saw an
-example of that case with the Ubuntu Team in the examples above.
+When a team is subscribed to a question, there are two cases two
+consider. The first one is if the team has an email address set, a
+notification will only be sent to that address. (That email address is
+assumed to be a mailing list reaching all the team members.) We already
+saw an example of that case with the Ubuntu Team in the examples above.
The other case is when the team doesn't have an email address set. In
that case, all the team members will be notified individually.
@@ -852,8 +895,8 @@
>>> [email_msg['To'] for email_msg in notifications]
['foo.bar@xxxxxxxxxxxxx', 'support@xxxxxxxxxx', 'test@xxxxxxxxxxxxx']
-Of course, if the user is also individually subscribed to the question, he
-will receives only one notification:
+Of course, if the user is also individually subscribed to the question,
+he will receives only one notification:
>>> ubuntu_question.subscribe(foo_bar)
<QuestionSubscription...>
@@ -865,20 +908,23 @@
['foo.bar@xxxxxxxxxxxxx', 'support@xxxxxxxxxx', 'test@xxxxxxxxxxxxx']
-== Notifications and Localized Questions ==
+Notifications and Localized Questions
+-------------------------------------
In general, only subscribers speaking the language of the question will
receive notifications related to it.
# Register salgado as answer contact, this makes the pt_BR language
# supported in Ubuntu.
+
>>> salgado = getUtility(IPersonSet).getByName('salgado')
>>> ubuntu.addAnswerContact(salgado)
True
+
>>> sorted([lang.code for lang in ubuntu.getSupportedLanguages()])
[u'en', u'pt_BR']
- >>> from canonical.launchpad.interfaces import ILanguageSet
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
>>> login('test@xxxxxxxxxxxxx')
>>> pt_BR_question = ubuntu.newQuestion(
... sample_person, title=(
@@ -903,8 +949,10 @@
status changed, only the subscribers speaking that language will receive
the notifications.
- >>> pt_BR_question.giveInfo("Veja o screenshot: http://tinyurl.com/y8jq8z")
+ >>> pt_BR_question.giveInfo(
+ ... "Veja o screenshot: http://tinyurl.com/y8jq8z")
<QuestionMessage...>
+
>>> notifications = pop_notifications()
>>> [email_msg['To'] for email_msg in notifications]
['guilherme.salgado@xxxxxxxxxxxxx', 'test@xxxxxxxxxxxxx']
@@ -934,6 +982,7 @@
# Define a function that will replace non-ascii character with
# its unicoded encoded value.
# Effectively replace u'\xe9' by '\\e9'.
+
>>> def escape_utf8_payload(message):
... charset = message.get_content_charset()
... content = unicode(message.get_payload(decode=True), charset)
@@ -1000,11 +1049,13 @@
You received this question notification because you are a direct
subscriber of the question.
-=== Localized Questions and Teams ===
-
-We will notify the team only if the question language is in one of
-the team's preferred languages. The languages spoken by the team
-members is unimportant.
+
+Localized Questions and Teams
+.............................
+
+We will notify the team only if the question language is in one of the
+team's preferred languages. The languages spoken by the team members is
+unimportant.
For example, the rosetta admins team becomes an Answer contact for
English questions. Carlos speaks Spanish, and he is an answer contact
@@ -1015,18 +1066,22 @@
>>> rosetta_admins = getUtility(IPersonSet).getByName('rosetta-admins')
>>> [lang.code for lang in rosetta_admins.languages]
[]
+
>>> rosetta_admins.addLanguage(getUtility(ILanguageSet)['en'])
>>> carlos = getUtility(IPersonSet).getByName('carlos')
>>> carlos.inTeam(rosetta_admins)
True
+
>>> spanish = getUtility(ILanguageSet)['es']
>>> spanish in carlos.languages
True
>>> ubuntu.addAnswerContact(carlos)
True
+
>>> ubuntu.addAnswerContact(rosetta_admins)
True
+
>>> spanish_question = ubuntu.newQuestion(
... sample_person, title="Necesidad ayuda con Firefox",
... description="No puedo acceso al Internet en Firefox.",
@@ -1045,29 +1100,34 @@
>>> rosetta_admins.addLanguage(french)
# Resend the new message notification
+
>>> notify(ObjectCreatedEvent(french_question))
>>> notifications = pop_notifications()
>>> [email_msg['To'] for email_msg in notifications]
['rosetta@xxxxxxxxxxxxx', 'test@xxxxxxxxxxxxx']
-When the team doesn't use an explicit address. All
-team members will be contacted if the question language is supported.
-For example, the Launchpad Developers team doesn't have any preferred
-email address set. Its only member, Foo Bar will receive a notification
-if the team supported languages includes the question language:
+When the team doesn't use an explicit address. All team members will be
+contacted if the question language is supported. For example, the
+Launchpad Developers team doesn't have any preferred email address set.
+Its only member, Foo Bar will receive a notification if the team
+supported languages includes the question language:
>>> launchpad_devs = getUtility(IPersonSet).getByName('launchpad')
>>> list(launchpad_devs.languages)
[]
+
>>> [member.name for member in launchpad_devs.activemembers]
[u'name16']
+
>>> launchpad_devs.addLanguage(spanish)
>>> ubuntu.addAnswerContact(launchpad_devs)
True
# Resend the new message notification
+
>>> notify(ObjectCreatedEvent(spanish_question))
>>> notifications = pop_notifications()
>>> [email_msg['To'] for email_msg in notifications]
['foo.bar@xxxxxxxxxxxxx', 'test@xxxxxxxxxxxxx']
+
=== modified file 'lib/lp/answers/doc/projectgroup.txt'
--- lib/lp/answers/doc/projectgroup.txt 2010-04-16 15:06:55 +0000
+++ lib/lp/answers/doc/projectgroup.txt 2010-07-29 20:06:01 +0000
@@ -24,8 +24,8 @@
You can search for all questions filed against projects in a project using the
project group's searchQuestions() method.
+ >>> from lp.registry.interfaces.product import IProductSet
>>> from lp.registry.interfaces.person import IPersonSet
- >>> from lp.registry.interfaces.product import IProductSet
>>> login('test@xxxxxxxxxxxxx')
>>> thunderbird = getUtility(IProductSet).getByName('thunderbird')
@@ -67,7 +67,7 @@
questions in the project group's projects.
# The Firefox project group has one question created in Brazilian
- # Portuguese.
+ # Portuguese.
>>> print ', '.join(
... sorted(language.code
... for language in mozilla_project.getQuestionLanguages()))
=== modified file 'lib/lp/answers/doc/question.txt'
--- lib/lp/answers/doc/question.txt 2010-02-04 03:19:25 +0000
+++ lib/lp/answers/doc/question.txt 2010-07-29 20:06:01 +0000
@@ -11,14 +11,14 @@
>>> login('test@xxxxxxxxxxxxx')
>>> from canonical.launchpad.webapp.testing import verifyObject
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from lp.answers.interfaces.questiontarget import IQuestionTarget
>>> from lp.registry.interfaces.product import IProductSet
>>> firefox = getUtility(IProductSet)['firefox']
>>> verifyObject(IQuestionTarget, firefox)
True
-
- >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> verifyObject(IQuestionTarget, ubuntu)
True
@@ -51,7 +51,8 @@
SourcePackages are also QuestionTargets.
XXX sinzui 2007-04-24. #108240 SourcePackages should not be QuestionTargets
- >>> evolution_in_hoary = ubuntu.currentseries.getSourcePackage('evolution')
+ >>> evolution_in_hoary = ubuntu.currentseries.getSourcePackage(
+ ... 'evolution')
>>> questiontarget = IQuestionTarget(evolution_in_hoary)
>>> verifyObject(IQuestionTarget, questiontarget)
True
@@ -59,8 +60,8 @@
You create a new question by calling the newQuestion() method of an
IQuestionTarget attribute.
- >>> from lp.registry.interfaces.person import IPersonSet
- >>> sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+ >>> sample_person = getUtility(IPersonSet).getByEmail(
+ ... 'test@xxxxxxxxxxxxx')
>>> firefox_question = firefox.newQuestion(
... sample_person, "Firefox question", "Unable to use Firefox")
@@ -217,7 +218,8 @@
That method returns an INotificationRecipientSet, containing the direct
subscribers along with the rationale for contacting them.
- >>> from canonical.launchpad.interfaces import INotificationRecipientSet
+ >>> from canonical.launchpad.interfaces.launchpad import (
+ ... INotificationRecipientSet)
>>> verifyObject(INotificationRecipientSet, subscribers)
True
>>> def print_reason(subscribers):
@@ -348,7 +350,7 @@
Question implements the IBugLinkTarget interface which makes it possible
to link bug report to question.
- >>> from canonical.launchpad.interfaces import IBugLinkTarget
+ >>> from lp.bugs.interfaces.buglink import IBugLinkTarget
>>> verifyObject(IBugLinkTarget, firefox_question)
True
=== modified file 'lib/lp/answers/doc/questionsets.txt'
--- lib/lp/answers/doc/questionsets.txt 2010-02-05 21:25:23 +0000
+++ lib/lp/answers/doc/questionsets.txt 2010-07-29 20:06:01 +0000
@@ -51,13 +51,13 @@
>>> from canonical.encoding import ascii_smash
>>> for question in question_set.searchQuestions(search_text='firefox'):
... print ascii_smash(question.title), question.target.displayname
- Problemas de Impressao no Firefox Mozilla Firefox
- Firefox loses focus and gets stuck Mozilla Firefox
- Firefox cannot render Bank Site Mozilla Firefox
- mailto: problem in webpage mozilla-firefox in ubuntu
- Newly installed plug-in doesn't seem to be used Mozilla Firefox
- Problem showing the SVG demo on W3C site Mozilla Firefox
- AINKAFSEEN ALEFLAMTEHGHAINYEHYEHREHALEFTEH ... Ubuntu
+ Problemas de Impressao no Firefox Mozilla Firefox
+ Firefox loses focus and gets stuck Mozilla Firefox
+ Firefox cannot render Bank Site Mozilla Firefox
+ mailto: problem in webpage mozilla-firefox in ubuntu
+ Newly installed plug-in doesn't seem to be used Mozilla Firefox
+ Problem showing the SVG demo on W3C site Mozilla Firefox
+ AINKAFSEEN ALEFLAMTEHGHAINYEHYEHREHALEFTEH ... Ubuntu
Status
@@ -72,7 +72,7 @@
... status=QuestionStatus.INVALID):
... print question.title, question.status.title, (
... question.target.displayname)
- Firefox is slow and consumes too much RAM Invalid mozilla-firefox in ubuntu
+ Firefox is slow and consumes too ... Invalid mozilla-firefox in ubuntu
The status parameter can also take a list of statuses.
@@ -80,8 +80,8 @@
... status=[QuestionStatus.SOLVED, QuestionStatus.INVALID]):
... print question.title, question.status.title, (
... question.target.displayname)
- mailto: problem in webpage Solved mozilla-firefox in ubuntu
- Firefox is slow and consumes too much RAM Invalid mozilla-firefox in ubuntu
+ mailto: problem in webpage Solved mozilla-firefox in ubuntu
+ Firefox is slow and consumes too ... Invalid mozilla-firefox in ubuntu
Language
@@ -108,12 +108,12 @@
... status=(QuestionStatus.OPEN, QuestionStatus.INVALID)):
... print ascii_smash(question.title), question.status.title, (
... question.target.displayname)
- Problemas de Impressao no Firefox Open Mozilla Firefox
- Firefox is slow and consumes too much RAM Invalid mozilla-firefox in ubuntu
- Firefox loses focus and gets stuck Open Mozilla Firefox
- Firefox cannot render Bank Site Open Mozilla Firefox
- Problem showing the SVG demo on W3C site Open Mozilla Firefox
- AINKAFSEEN ALEFLAMTEHGHAINYEHYEHREHALEFTEH ... Ubuntu
+ Problemas de Impressao no Firefox Open Mozilla Firefox
+ Firefox is slow and consumes too much ... mozilla-firefox in ubuntu
+ Firefox loses focus and gets stuck Open Mozilla Firefox
+ Firefox cannot render Bank Site Open Mozilla Firefox
+ Problem showing the SVG demo on W3C site Open Mozilla Firefox
+ AINKAFSEEN ALEFLAMTEHGHAINYEHYEHREHALEFTEH ... Ubuntu
Sort order
@@ -177,10 +177,10 @@
Then some recent questions are created on a number of projects.
- >>> from lp.answers.testing import QuestionFactory
>>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from lp.registry.interfaces.person import IPersonSet
>>> from lp.registry.interfaces.product import IProductSet
+ >>> from lp.answers.testing import QuestionFactory
>>> firefox = getUtility(IProductSet).getByName('firefox')
>>> landscape = getUtility(IProductSet).getByName('landscape')
@@ -260,7 +260,6 @@
... closed_question.owner, QuestionStatus.SOLVED, 'no comment')
<QuestionMessage at ...>
- >>> from operator import itemgetter
>>> packages = (
... ubuntu_evolution, ubuntu_pmount, debian_evolution, debian_pmount)
>>> package_counts = question_set.getOpenQuestionCountByPackages(packages)
=== modified file 'lib/lp/answers/doc/workflow.txt'
--- lib/lp/answers/doc/workflow.txt 2010-02-04 05:58:57 +0000
+++ lib/lp/answers/doc/workflow.txt 2010-07-29 20:06:01 +0000
@@ -234,8 +234,8 @@
>>> login('no-priv@xxxxxxxxxxxxx')
>>> two_weeks_from_now = now + timedelta(days=14)
>>> confirm_message = question.confirmAnswer(
- ... "I upgraded to 512M of RAM (found on eBay) and I've "
- ... "successfully managed to install Ubuntu. Thanks for all the help.",
+ ... "I upgraded to 512M of RAM (found on eBay) and I've successfully "
+ ... "managed to install Ubuntu. Thanks for all the help.",
... datecreated=two_weeks_from_now, answer=answer_message)
>>> print confirm_message.action.name
CONFIRM
@@ -739,7 +739,7 @@
In all the workflow methods, it is possible to pass an IMessage instead of
a string.
- >>> from canonical.launchpad.interfaces import IMessageSet
+ >>> from canonical.launchpad.interfaces.message import IMessageSet
>>> login('test@xxxxxxxxxxxxx')
>>> messageset = getUtility(IMessageSet)
>>> question = ubuntu.newQuestion(**new_question_args)
=== modified file 'lib/lp/answers/karma.py'
--- lib/lp/answers/karma.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/karma.py 2010-07-29 20:06:01 +0000
@@ -9,9 +9,12 @@
]
from canonical.database.sqlbase import block_implicit_flushes
-from canonical.launchpad.interfaces import (
- IDistribution, IProduct, QuestionAction)
+
from lp.registry.interfaces.person import IPerson
+from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.distribution import IDistribution
+from lp.answers.interfaces.questionenums import QuestionAction
+
def assignKarmaUsingQuestionContext(person, question, actionname):
"""Assign Karma with the given actionname to the given person.
@@ -69,10 +72,10 @@
questionmessage.owner, question, karma_action)
-# XXX flacoste 2007-07-13 bug=125849:
-# This should go away once bug #125849 is fixed.
def get_karma_context_parameters(context):
"""Return the proper karma context parameters based on the object."""
+ # XXX flacoste 2007-07-13 bug=125849:
+ # This should go away once bug #125849 is fixed.
params = dict(product=None, distribution=None)
if IProduct.providedBy(context):
params['product'] = context
@@ -99,4 +102,3 @@
context = get_karma_context_parameters(faq.target)
if old_faq.content != faq.content or old_faq.title != faq.title:
user.assignKarma('faqedited', **context)
-
=== modified file 'lib/lp/answers/model/answercontact.py'
--- lib/lp/answers/model/answercontact.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/model/answercontact.py 2010-07-29 20:06:01 +0000
@@ -14,8 +14,8 @@
from sqlobject import ForeignKey
from canonical.database.sqlbase import SQLBase
-from canonical.launchpad.interfaces import IAnswerContact
from lp.registry.interfaces.person import validate_public_person
+from lp.answers.interfaces.answercontact import IAnswerContact
class AnswerContact(SQLBase):
=== modified file 'lib/lp/answers/model/faq.py'
--- lib/lp/answers/model/faq.py 2010-02-17 11:13:06 +0000
+++ lib/lp/answers/model/faq.py 2010-07-29 20:06:01 +0000
@@ -26,10 +26,12 @@
from canonical.database.datetimecol import UtcDateTimeCol
from canonical.database.nl_search import nl_phrase_search
from canonical.database.sqlbase import quote, SQLBase, sqlvalues
-
-from canonical.launchpad.interfaces import (
- IDistribution, IFAQ, IFAQSet, FAQSort, IPerson, IProduct, IProjectGroup)
-from lp.registry.interfaces.person import validate_public_person
+from lp.registry.interfaces.person import IPerson, validate_public_person
+from lp.registry.interfaces.projectgroup import IProjectGroup
+from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.distribution import IDistribution
+from lp.answers.interfaces.faq import IFAQ, IFAQSet
+from lp.answers.interfaces.faqcollection import FAQSort
class FAQ(SQLBase):
@@ -279,4 +281,3 @@
"""See `IFAQSet`."""
return FAQSearch(
search_text=search_text, owner=owner, sort=sort).getResults()
-
=== modified file 'lib/lp/answers/model/question.py'
--- lib/lp/answers/model/question.py 2009-07-17 00:26:05 +0000
+++ lib/lp/answers/model/question.py 2010-07-29 20:06:01 +0000
@@ -36,16 +36,29 @@
from lazr.lifecycle.event import ObjectCreatedEvent, ObjectModifiedEvent
from lazr.lifecycle.snapshot import Snapshot
-from canonical.launchpad.interfaces import (
- BugTaskStatus, IBugLinkTarget, IDistribution, IDistributionSet,
- IDistributionSourcePackage, IFAQ, InvalidQuestionStateError, ILanguage,
- ILaunchpadCelebrities, IMessage, IPerson, IProduct, IProductSet,
- IQuestion, IQuestionSet, IQuestionTarget, ISourcePackage,
- QUESTION_STATUS_DEFAULT_SEARCH, QuestionAction, QuestionParticipation,
- QuestionPriority, QuestionSort, QuestionStatus)
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.interfaces.message import IMessage
+from lp.services.worlddata.interfaces.language import ILanguage
+from lp.registry.interfaces.person import IPerson, validate_public_person
+from lp.registry.interfaces.product import IProduct, IProductSet
+from lp.registry.interfaces.distribution import (
+ IDistribution, IDistributionSet)
+from lp.registry.interfaces.distributionsourcepackage import (
+ IDistributionSourcePackage)
from lp.registry.interfaces.sourcepackagename import (
ISourcePackageNameSet)
-from lp.registry.interfaces.person import validate_public_person
+from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.bugs.interfaces.buglink import IBugLinkTarget
+from lp.bugs.interfaces.bugtask import BugTaskStatus
+from lp.answers.interfaces.faq import IFAQ
+from lp.answers.interfaces.questionenums import (
+ QuestionAction, QuestionParticipation, QuestionPriority, QuestionSort,
+ QuestionStatus)
+from lp.answers.interfaces.question import (
+ InvalidQuestionStateError, IQuestion)
+from lp.answers.interfaces.questioncollection import (
+ IQuestionSet, QUESTION_STATUS_DEFAULT_SEARCH)
+from lp.answers.interfaces.questiontarget import IQuestionTarget
from canonical.database.sqlbase import cursor, quote, SQLBase, sqlvalues
from canonical.database.constants import DEFAULT, UTC_NOW
@@ -83,6 +96,7 @@
def __call__(self, func):
"""Return the ObjectModifiedEvent decorator."""
+
def notify_question_modified(self, *args, **kwargs):
"""Create the ObjectModifiedEvent decorator."""
old_question = Snapshot(self, providing=providedBy(self))
@@ -509,7 +523,8 @@
When update_question_dates is True, the question's datelastquery or
datelastresponse attribute is updated to the message creation date.
The datelastquery attribute is updated when the message owner is the
- same than the question owner, otherwise the datelastresponse is updated.
+ same than the question owner, otherwise the datelastresponse is
+ updated.
:owner: An IPerson.
:content: A string or an IMessage. When it's an IMessage, the owner
@@ -907,7 +922,7 @@
elif sort is QuestionSort.RECENT_OWNER_ACTIVITY:
return ['-Question.datelastquery']
else:
- raise AssertionError, "Unknown QuestionSort value: %s" % sort
+ raise AssertionError("Unknown QuestionSort value: %s" % sort)
def getResults(self):
"""Return the questions that match this query."""
=== modified file 'lib/lp/answers/model/questionmessage.py'
--- lib/lp/answers/model/questionmessage.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/model/questionmessage.py 2010-07-29 20:06:01 +0000
@@ -15,13 +15,13 @@
from sqlobject import ForeignKey
+from lazr.delegates import delegates
+
from canonical.database.sqlbase import SQLBase
from canonical.database.enumcol import EnumCol
-
-from canonical.launchpad.interfaces import (
- IMessage, IQuestionMessage, QuestionAction, QuestionStatus)
-
-from lazr.delegates import delegates
+from canonical.launchpad.interfaces.message import IMessage
+from lp.answers.interfaces.questionenums import QuestionAction, QuestionStatus
+from lp.answers.interfaces.questionmessage import IQuestionMessage
class QuestionMessage(SQLBase):
=== modified file 'lib/lp/answers/model/questionreopening.py'
--- lib/lp/answers/model/questionreopening.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/model/questionreopening.py 2010-07-29 20:06:01 +0000
@@ -22,8 +22,9 @@
from canonical.database.constants import DEFAULT
from canonical.database.datetimecol import UtcDateTimeCol
from canonical.database.enumcol import EnumCol
-from canonical.launchpad.interfaces import IQuestionReopening, QuestionStatus
from lp.registry.interfaces.person import validate_public_person
+from lp.answers.interfaces.questionenums import QuestionStatus
+from lp.answers.interfaces.questionreopening import IQuestionReopening
class QuestionReopening(SQLBase):
@@ -45,15 +46,17 @@
date_solved = UtcDateTimeCol(notNull=False, default=None)
priorstate = EnumCol(schema=QuestionStatus, notNull=True)
-# XXX flacoste 2006-10-25 The QuestionReopening is probably not that useful
-# anymore since the question history is nearly completely tracked in the
-# question message trails. (Only missing information is the previous recorded
-# answer.) If we decide to still keep that class, this subscriber should
-# probably be moved outside of database code.
+
def create_questionreopening(question, event):
- """Event subscriber that creates a QuestionReopening whenever a question
- with an answer changes back to the OPEN state.
+ """Event subscriber that creates a QuestionReopening event.
+
+ A QuestionReopening is created question with an answer changes back to the
+ OPEN state.
"""
+ # XXX flacoste 2006-10-25 The QuestionReopening is probably not that
+ # useful anymore since the question history is nearly complete.
+ # If we decide to still keep that class, this subscriber should
+ # probably be moved outside of database code.
if question.status != QuestionStatus.OPEN:
return
@@ -82,4 +85,3 @@
reopening = ProxyFactory(reopening)
notify(ObjectCreatedEvent(reopening, user=reopen_msg.owner))
-
=== modified file 'lib/lp/answers/model/questionsubscription.py'
--- lib/lp/answers/model/questionsubscription.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/model/questionsubscription.py 2010-07-29 20:06:01 +0000
@@ -13,10 +13,9 @@
from sqlobject import ForeignKey
-from canonical.launchpad.interfaces import IQuestionSubscription
-
from canonical.database.sqlbase import SQLBase
from lp.registry.interfaces.person import validate_public_person
+from lp.answers.interfaces.questionsubscription import IQuestionSubscription
class QuestionSubscription(SQLBase):
@@ -32,5 +31,3 @@
person = ForeignKey(
dbName='person', foreignKey='Person',
storm_validator=validate_public_person, notNull=True)
-
-
=== modified file 'lib/lp/answers/scripts/questionexpiration.py'
--- lib/lp/answers/scripts/questionexpiration.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/scripts/questionexpiration.py 2010-07-29 20:06:01 +0000
@@ -10,10 +10,11 @@
from zope.component import getUtility
from canonical.config import config
-from canonical.launchpad.interfaces import ILaunchpadCelebrities, IQuestionSet
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.webapp.interfaces import IPlacelessAuthUtility
from canonical.launchpad.webapp.interaction import (
setupInteraction, endInteraction)
+from lp.answers.interfaces.questioncollection import IQuestionSet
class QuestionJanitor:
@@ -79,7 +80,6 @@
self._logout()
self.log.info('Finished expiration run.')
-
def _login(self):
"""Setup an interaction as the Launchpad Janitor."""
auth_utility = getUtility(IPlacelessAuthUtility)
=== modified file 'lib/lp/answers/stories/distribution-package-answer-contact.txt'
--- lib/lp/answers/stories/distribution-package-answer-contact.txt 2009-09-18 12:42:56 +0000
+++ lib/lp/answers/stories/distribution-package-answer-contact.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= Answer Contacts for Distribution Source Package =
+Answer Contacts for Distribution Source Package
+===============================================
Support on source packages is handled both by the source package answer
contacts as well as the distribution answer contacts.
@@ -6,8 +7,9 @@
# Register a Sample Person as an answer contact for the distribution.
>>> from zope.component import getUtility
>>> from canonical.launchpad.ftests import login, logout
- >>> from canonical.launchpad.interfaces import (
- ... IDistributionSet, ILanguageSet, ILaunchBag)
+ >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
+ >>> from lp.services.worlddata.interfaces.language import ILanguageSet
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from canonical.database.sqlbase import flush_database_updates
>>> login('test@xxxxxxxxxxxxx')
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
@@ -114,7 +116,9 @@
... print answer_contact.renderContents()
Landscape Developers
-== Distribution Release Source Package Answer Contacts ==
+
+Distribution Release Source Package Answer Contacts
+---------------------------------------------------
The 'Set answer contacts' action is also available on distribution
release source package. Altough in this case, it's exactly like working
@@ -127,7 +131,7 @@
... "http://answers.launchpad.dev/ubuntu/hoary/+source/evolution")
>>> browser.getLink('Set answer contact').click()
>>> print browser.url
- http://answers.launchpad.dev/ubuntu/hoary/+source/evolution/+answer-contact
+ http://.../ubuntu/hoary/+source/evolution/+answer-contact
The Landscape developers team is selected as answer contact, like for
the distribution source package.
=== modified file 'lib/lp/answers/testing.py'
--- lib/lp/answers/testing.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/testing.py 2010-07-29 20:06:01 +0000
@@ -5,13 +5,14 @@
__metaclass__ = type
__all__ = [
- 'QuestionFactory'
+ 'QuestionFactory',
]
from zope.component import getUtility
-from canonical.launchpad.interfaces import (
- ILaunchBag, IQuestionTarget, IPillarNameSet)
+from canonical.launchpad.webapp.interfaces import ILaunchBag
+from lp.registry.interfaces.pillar import IPillarNameSet
+from lp.answers.interfaces.questiontarget import IQuestionTarget
class QuestionFactory:
@@ -63,7 +64,7 @@
owner = getUtility(ILaunchBag).user
created_questions = []
for index in range(question_count):
- replacements = {'index' : index, 'target': target.displayname}
+ replacements = {'index': index, 'target': target.displayname}
created_questions.append(target.newQuestion(
owner,
'Question %(index)s on %(target)s' % replacements,
=== modified file 'lib/lp/answers/tests/questiontarget-sourcepackage.txt'
--- lib/lp/answers/tests/questiontarget-sourcepackage.txt 2009-03-24 12:43:49 +0000
+++ lib/lp/answers/tests/questiontarget-sourcepackage.txt 2010-07-29 20:06:01 +0000
@@ -1,58 +1,68 @@
-= IQuestionTarget for SourcePackage =
+IQuestionTarget for SourcePackage
+=================================
The implementation of IQuestionTarget by SourcePackage and
-DistributionSourcePackage contain some small differences with
-the other ones. (See questiontarget.txt for the generic interface
-description.)
-
-=== ISourcePackage.target ===
-
-The target attribute of questions created on SourcePackage will be
-that of the DistributionSourcePackage (and not the SourcePackage):
+DistributionSourcePackage contain some small differences with the other
+ones. (See questiontarget.txt for the generic interface description.)
+
+
+ISourcePackage.target
+.....................
+
+The target attribute of questions created on SourcePackage will be that
+of the DistributionSourcePackage (and not the SourcePackage):
>>> login('no-priv@xxxxxxxxxxxxx')
- >>> from canonical.launchpad.interfaces import (
- ... IPersonSet, IDistributionSet)
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> firefox = ubuntu.currentseries.getSourcePackage('mozilla-firefox')
>>> question = firefox.getQuestion(3)
>>> question.target == firefox
False
+
>>> question.target == ubuntu.getSourcePackage('mozilla-firefox')
True
-=== get() ===
+
+get()
+.....
The question created on a SourcePackage or DistributionSourcePackage can
also be retrieved via the distribution.
>>> ubuntu.getQuestion(3) == question
True
-
+
# Create a new question on the evolution source package.
+
>>> sample_person = getUtility(IPersonSet).getByName('name16')
>>> evolution = ubuntu.getSourcePackage('evolution')
>>> evolution_question = evolution.newQuestion(
... sample_person, 'Evolution crashed',
... 'Surprise, surprise! Evolution crashed... again...'
... 'while clicking on a thread expander.')
-
+
>>> ubuntu.getQuestion(evolution_question.id) == evolution_question
True
-=== Support Contacts ===
+
+Support Contacts
+................
The support contacts of a SourcePackage contain all the support contacts
from its containing distribution.
>>> list(ubuntu.answer_contacts)
[]
+
>>> list(evolution.answer_contacts)
[]
>>> ubuntu.addAnswerContact(sample_person)
True
+
>>> [person.name for person in evolution.answer_contacts]
[u'name16']
@@ -68,6 +78,7 @@
>>> evolution.addAnswerContact(sample_person)
True
+
>>> [person.name for person in evolution.direct_answer_contacts]
[u'name16']
@@ -82,12 +93,16 @@
>>> evolution.removeAnswerContact(sample_person)
True
+
>>> [person.name for person in evolution.direct_answer_contacts]
[]
+
>>> [person.name for person in evolution.answer_contacts]
[u'name16']
+
>>> evolution.removeAnswerContact(sample_person)
False
+
>>> [person.name for person in evolution.answer_contacts]
[u'name16']
@@ -95,21 +110,32 @@
>>> [person.name for person in evolution.direct_answer_contacts]
[]
+
>>> [person.name for person in evolution.answer_contacts]
[u'name16']
+
>>> evolution.addAnswerContact(sample_person)
True
+
>>> [person.name for person in evolution.direct_answer_contacts]
[u'name16']
+
>>> [person.name for person in evolution.answer_contacts]
[u'name16']
+
>>> evolution.removeAnswerContact(sample_person)
True
+
>>> [person.name for person in evolution.answer_contacts]
[u'name16']
+
>>> evolution.removeAnswerContact(sample_person)
False
+
>>> [person.name for person in evolution.direct_answer_contacts]
[]
+
>>> [person.name for person in evolution.answer_contacts]
[u'name16']
+
+
=== modified file 'lib/lp/answers/tests/test_question_workflow.py'
--- lib/lp/answers/tests/test_question_workflow.py 2009-06-24 23:10:46 +0000
+++ lib/lp/answers/tests/test_question_workflow.py 2010-07-29 20:06:01 +0000
@@ -25,14 +25,19 @@
from lazr.lifecycle.interfaces import (
IObjectCreatedEvent, IObjectModifiedEvent)
-from canonical.launchpad.interfaces import (
- IDistributionSet, ILanguageSet, ILaunchBag, InvalidQuestionStateError,
- IQuestion, IQuestionMessage, QuestionAction, QuestionStatus)
-from lp.registry.interfaces.person import IPerson, IPersonSet
+
from canonical.launchpad.ftests import login, login_person, ANONYMOUS
from canonical.launchpad.ftests.event import TestEventListener
from canonical.testing.layers import DatabaseFunctionalLayer
from canonical.launchpad.webapp.authorization import clear_cache
+from canonical.launchpad.webapp.interfaces import ILaunchBag
+from lp.services.worlddata.interfaces.language import ILanguageSet
+from lp.registry.interfaces.person import IPerson, IPersonSet
+from lp.registry.interfaces.distribution import IDistributionSet
+from lp.answers.interfaces.questionenums import QuestionAction, QuestionStatus
+from lp.answers.interfaces.question import (
+ InvalidQuestionStateError, IQuestion)
+from lp.answers.interfaces.questionmessage import IQuestionMessage
class BaseAnswerTrackerWorkflowTestCase(unittest.TestCase):
@@ -106,8 +111,8 @@
"""Helper for transition guard tests.
Helper that verifies that the Question guard_name attribute
- is True when the question status is one listed in statuses_expected_true
- and False otherwise.
+ is True when the question status is one listed in
+ statuses_expected_true and False otherwise.
"""
for status in QuestionStatus.items:
if status != self.question.status:
@@ -240,8 +245,10 @@
was created and that an IObjectModifiedEvent was also sent.
The event object and edited_fields attribute are checked.
"""
+
def failure_msg(msg):
return "From status %s: %s" % (status_name, msg)
+
self.failUnless(
len(self.collected_events) >= 1,
failure_msg('failed to trigger an IObjectCreatedEvent'))
@@ -385,7 +392,7 @@
expected_action=QuestionAction.GIVEINFO,
expected_status=QuestionStatus.OPEN,
transition_method=self.question.giveInfo,
- transition_method_args=("That's that.",),
+ transition_method_args=("That's that.", ),
edited_fields=None)
def test_giveInfoPermission(self):
@@ -432,7 +439,7 @@
expected_status=QuestionStatus.ANSWERED,
transition_method=self.question.giveAnswer,
transition_method_args=(
- self.answerer, "It looks like a real problem.",),
+ self.answerer, "It looks like a real problem.", ),
edited_fields=None)
def test_giveAnswerByOwner(self):
@@ -452,7 +459,7 @@
expected_status=QuestionStatus.ANSWERED,
transition_method=self.question.giveAnswer,
transition_method_args=(
- self.answerer, "It looks like a real problem.",),
+ self.answerer, "It looks like a real problem.", ),
edited_fields=None)
# When the owner gives the answer, the question moves straight to
@@ -474,7 +481,7 @@
extra_message_check=checkAnswerMessage,
transition_method=self.question.giveAnswer,
transition_method_args=(
- self.owner, "I found the solution.",),
+ self.owner, "I found the solution.", ),
transition_method_kwargs={'datecreated': self.nowPlus(3)},
edited_fields=['status', 'messages', 'date_solved', 'answerer',
'datelastquery'])
@@ -523,7 +530,7 @@
extra_message_check=checkFAQ,
transition_method=self.question.linkFAQ,
transition_method_args=(
- self.answerer, self.faq, "Check the FAQ!",),
+ self.answerer, self.faq, "Check the FAQ!", ),
edited_fields=None)
# When the owner links the FAQ, the question moves straight to
@@ -545,7 +552,7 @@
extra_message_check=checkAnswerMessage,
transition_method=self.question.linkFAQ,
transition_method_args=(
- self.owner, self.faq, "I found the solution in that FAQ.",),
+ self.owner, self.faq, "I found the solution in that FAQ.", ),
transition_method_kwargs={'datecreated': self.nowPlus(3)},
edited_fields=['status', 'messages', 'date_solved', 'answerer',
'datelastquery'])
@@ -628,9 +635,9 @@
expected_status=QuestionStatus.SOLVED,
extra_message_check=checkAnswerMessage,
transition_method=self.question.confirmAnswer,
- transition_method_args=("That was very useful.",),
+ transition_method_args=("That was very useful.", ),
transition_method_kwargs={'answer': answer_message,
- 'datecreated' : self.nowPlus(2)},
+ 'datecreated': self.nowPlus(2)},
edited_fields=['status', 'messages', 'date_solved', 'answerer',
'answer', 'datelastquery'])
@@ -661,9 +668,9 @@
expected_status=QuestionStatus.SOLVED,
extra_message_check=checkAnswerMessage,
transition_method=self.question.confirmAnswer,
- transition_method_args=("The space bar also works.",),
+ transition_method_args=("The space bar also works.", ),
transition_method_kwargs={'answer': answer_message,
- 'datecreated' : self.nowPlus(2)},
+ 'datecreated': self.nowPlus(2)},
edited_fields=['messages', 'date_solved', 'answerer',
'answer', 'datelastquery'])
@@ -725,7 +732,7 @@
expected_action=QuestionAction.REOPEN,
expected_status=QuestionStatus.OPEN,
transition_method=self.question.reopen,
- transition_method_args=('I still have this problem.',),
+ transition_method_args=('I still have this problem.', ),
edited_fields=['status', 'messages', 'datelastquery'])
def test_reopenFromSOLVEDByOwner(self):
=== modified file 'lib/lp/blueprints/adapters.py'
--- lib/lp/blueprints/adapters.py 2009-06-25 00:00:26 +0000
+++ lib/lp/blueprints/adapters.py 2010-07-29 20:06:01 +0000
@@ -11,8 +11,9 @@
class SpecificationDelta:
- """See canonical.launchpad.interfaces.ISpecificationDelta."""
+ """See lp.blueprints.interfaces.specification.ISpecificationDelta."""
implements(ISpecificationDelta)
+
def __init__(self, specification, user, title=None,
summary=None, whiteboard=None, specurl=None, productseries=None,
distroseries=None, milestone=None, name=None, priority=None,
@@ -36,4 +37,3 @@
self.drafter = drafter
self.bugs_linked = bugs_linked
self.bugs_unlinked = bugs_unlinked
-
=== modified file 'lib/lp/blueprints/browser/configure.zcml'
--- lib/lp/blueprints/browser/configure.zcml 2010-07-16 16:58:55 +0000
+++ lib/lp/blueprints/browser/configure.zcml 2010-07-29 20:06:01 +0000
@@ -133,7 +133,7 @@
<browser:url
for="lp.blueprints.interfaces.sprint.ISprintSet"
path_expression="string:sprints"
- parent_utility="canonical.launchpad.interfaces.ILaunchpadRoot"/>
+ parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
<browser:defaultView
for="lp.blueprints.interfaces.sprint.ISprintSet"
name="+index"/>
@@ -450,7 +450,7 @@
<browser:url
for="lp.blueprints.interfaces.specification.ISpecificationSet"
path_expression="string:"
- parent_utility="canonical.launchpad.interfaces.ILaunchpadRoot"
+ parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"
rootsite="blueprints"/>
<browser:defaultView
for="lp.blueprints.interfaces.specification.ISpecificationSet"
=== modified file 'lib/lp/blueprints/browser/tests/sprintattendance-views.txt'
--- lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2009-10-30 16:38:20 +0000
+++ lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2010-07-29 20:06:01 +0000
@@ -7,7 +7,7 @@
>>> from lp.blueprints.browser.sprintattendance import (
... BaseSprintAttendanceAddView)
- >>> from canonical.launchpad.interfaces import ISprintSet
+ >>> from lp.blueprints.interfaces.sprint import ISprintSet
>>> ubz = getUtility(ISprintSet)['ubz']
>>> sprint_attendance_view = create_view(ubz, name='+attend')
@@ -29,76 +29,82 @@
A helper function to test date handling.
- >>> def create_sprint_attendance_view(sprint, dates):
- ... time_starts, time_ends = dates
- ... form = {
- ... 'field.time_starts': time_starts,
- ... 'field.time_ends': time_ends,
- ... 'field.is_physical': 'yes',
- ... 'field.actions.register': 'Register'}
- ... return create_initialized_view(sprint, name='+attend', form=form)
-
-This sprint doesn't have any attendees. It dose have the required dates set.
-
- >>> [attendee.name for attendee in ubz.attendees]
- []
- >>> ubz.time_starts
- datetime.datetime(2005, 10, 7, 23, 30, tzinfo=<UTC>)
- >>> ubz.time_ends
- datetime.datetime(2005, 11, 17, 0, 11, tzinfo=<UTC>)
-
- >>> login('test@xxxxxxxxxxxxx')
-
-Choosing a starting date after the ending date returns a nice error message.
-
- >>> dates = ['2005-11-15', '2005-10-09']
- >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
- >>> print sprint_attendance_view.getFieldError('time_ends')
- The end time must be after the start time.
+ >>> def create_sprint_attendance_view(sprint, dates):
+ ... time_starts, time_ends = dates
+ ... form = {
+ ... 'field.time_starts': time_starts,
+ ... 'field.time_ends': time_ends,
+ ... 'field.is_physical': 'yes',
+ ... 'field.actions.register': 'Register'}
+ ... return create_initialized_view(sprint, name='+attend', form=form)
+
+This sprint doesn't have any attendees. It dose have the required dates
+set.
+
+ >>> [attendee.name for attendee in ubz.attendees]
+ []
+
+ >>> ubz.time_starts
+ datetime.datetime(2005, 10, 7, 23, 30, tzinfo=<UTC>)
+
+ >>> ubz.time_ends
+ datetime.datetime(2005, 11, 17, 0, 11, tzinfo=<UTC>)
+
+ >>> login('test@xxxxxxxxxxxxx')
+
+Choosing a starting date after the ending date returns a nice error
+message.
+
+ >>> dates = ['2005-11-15', '2005-10-09']
+ >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
+ >>> print sprint_attendance_view.getFieldError('time_ends')
+ The end time must be after the start time.
Choosing a starting date too far after the meeting's end returns an
error message.
- >>> dates = ['2006-01-01', '2006-02-01']
- >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
- >>> print sprint_attendance_view.getFieldError('time_starts')
- Please pick a date before 2005-11-16 19:11
-
-Choosing a ending date more than a day before the meeting's start returns
-an error message.
-
- >>> dates = ['2005-07-01', '2005-08-01']
- >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
- >>> print sprint_attendance_view.getFieldError('time_ends')
- Please pick a date after 2005-10-07 19:30
+ >>> dates = ['2006-01-01', '2006-02-01']
+ >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
+ >>> print sprint_attendance_view.getFieldError('time_starts')
+ Please pick a date before 2005-11-16 19:11
+
+Choosing a ending date more than a day before the meeting's start
+returns an error message.
+
+ >>> dates = ['2005-07-01', '2005-08-01']
+ >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
+ >>> print sprint_attendance_view.getFieldError('time_ends')
+ Please pick a date after 2005-10-07 19:30
Entering a starting date just before the meeting's start date or a
finishing date just after the meeting's end date works because we assume
you wanted the meeting's start and end dates respectively.
- >>> dates = ['2005-10-07 09:00', '2005-11-17 19:05']
- >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
- >>> sprint_attendance_view.errors
- []
+ >>> dates = ['2005-10-07 09:00', '2005-11-17 19:05']
+ >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
+ >>> sprint_attendance_view.errors
+ []
Sample Person is now listed as an attendee.
- >>> ubz = getUtility(ISprintSet)['ubz']
- >>> [attendee.name for attendee in ubz.attendees]
- [u'name12']
- >>> sprint_attendance = ubz.attendances[0]
-
- >>> ubz.time_starts == sprint_attendance.time_starts
- True
- >>> ubz.time_ends == sprint_attendance.time_ends
- True
+ >>> ubz = getUtility(ISprintSet)['ubz']
+ >>> [attendee.name for attendee in ubz.attendees]
+ [u'name12']
+
+ >>> sprint_attendance = ubz.attendances[0]
+
+ >>> ubz.time_starts == sprint_attendance.time_starts
+ True
+
+ >>> ubz.time_ends == sprint_attendance.time_ends
+ True
Physical attendance
-------------------
-The most common kind of attendance is that the user will be physically present
-at the sprint.
+The most common kind of attendance is that the user will be physically
+present at the sprint.
>>> person = factory.makePerson(name='brown')
>>> login_person(person)
@@ -116,8 +122,8 @@
>>> sprint_attendance.is_physical
True
-Some users attend the sprint virtually, such as via IRC, VOIP, or by using
-their psychotic powers :).
+Some users attend the sprint virtually, such as via IRC, VOIP, or by
+using their psychotic powers :).
>>> person = factory.makePerson(name='black')
>>> login_person(person)
@@ -160,7 +166,8 @@
>>> isinstance(view, BaseSprintAttendanceAddView)
True
-It also requires the attendee field so that a user can register someone else.
+It also requires the attendee field so that a user can register someone
+else.
>>> view.field_names
['attendee', 'time_starts', 'time_ends', 'is_physical']
@@ -194,6 +201,7 @@
>>> lines = view.render().strip().splitlines()
>>> print lines[0]
Launchpad username,Display name,...Timezone,...Physically present
+
>>> print lines[-1]
name12,Sample Person,...Australia/Perth,...True
@@ -211,5 +219,8 @@
>>> last_attendee = view.render().split('\n')[-2]
>>> print last_attendee
ubz-last-attendee,...
+
>>> 'Europe' in last_attendee
False
+
+
=== modified file 'lib/lp/blueprints/doc/spec-mail-exploder.txt'
--- lib/lp/blueprints/doc/spec-mail-exploder.txt 2010-03-02 16:29:43 +0000
+++ lib/lp/blueprints/doc/spec-mail-exploder.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= The Spec Mail Exploder =
+The Spec Mail Exploder
+======================
There's an address, notifications@xxxxxxxxxxxxxxxxxxx, which looks at
all incoming email to see if it looks like a change notifications from a
@@ -70,7 +71,8 @@
True
-== The Mail Handler ==
+The Mail Handler
+----------------
The mail handler that handles mail on notifications@xxxxxxxxxxxxxxxxxxx
is SpecificationHandler.
@@ -82,10 +84,10 @@
True
It has a helper method to help associate a URL with a specification. We
-use this method instead of simply ISpecificationSet.getByURL() since
-the Ubuntu wikis are special, they have more than one domain name
-mapping to the same wiki. So if we start with the correct URL, of
-course we get the specification with that URL.
+use this method instead of simply ISpecificationSet.getByURL() since the
+Ubuntu wikis are special, they have more than one domain name mapping to
+the same wiki. So if we start with the correct URL, of course we get the
+specification with that URL.
>>> spec = handler._getSpecByURL(
... 'https://wiki.ubuntu.com/MediaIntegrityCheck')
@@ -129,7 +131,7 @@
>>> log._log.setLevel(OLD_LOG_LEVEL)
- >>> from canonical.launchpad.interfaces import ISpecificationSet
+ >>> from lp.blueprints.interfaces.specification import ISpecificationSet
>>> spec = getUtility(ISpecificationSet).getByURL(
... 'https://wiki.ubuntu.com/MediaIntegrityCheck')
>>> spec.notificationRecipientAddresses()
@@ -160,8 +162,10 @@
>>> sent_msg['To'] == moin_change['To']
True
+
>>> sent_msg['From'] == moin_change['From']
True
+
>>> sent_msg['Subject'] == moin_change['Subject']
True
@@ -198,6 +202,7 @@
... filealias=FakeFileAlias(), log=log)
WARNING:...:Got back a notification we sent: http://librarian/foo.txt
True
+
>>> log._log.setLevel(OLD_LOG_LEVEL)
No emails were sent:
@@ -219,6 +224,7 @@
... filealias=FakeFileAlias(), log=log)
WARNING:...:We received an email from Launchpad: http://librarian/foo.txt
True
+
>>> log._log.setLevel(OLD_LOG_LEVEL)
Again, no emails were sent:
@@ -248,13 +254,12 @@
>>> to_addrs == spec.notificationRecipientAddresses()
True
-Lastly, let's simulate sending a moin notification, and use
-handleMail() instead to ensure that handler above handles the email.
-This will make sure that the handler is setup properly to handle
-unknown users, since webmaster@xxxxxxxxxx doesn't belong to any Person
-in Launchpad:
+Lastly, let's simulate sending a moin notification, and use handleMail()
+instead to ensure that handler above handles the email. This will make
+sure that the handler is setup properly to handle unknown users, since
+webmaster@xxxxxxxxxx doesn't belong to any Person in Launchpad:
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> getUtility(IPersonSet).getByEmail('webmaster@xxxxxxxxxx') is None
True
@@ -267,6 +272,7 @@
>>> from canonical.launchpad.mail.incoming import handleMail
>>> sendmail(moin_change, bulk=False)
'...'
+
>>> transaction.commit()
>>> handleMail()
@@ -282,3 +288,4 @@
>>> sent_msg['Message-Id'] == moin_change['Message-Id']
True
+
=== modified file 'lib/lp/blueprints/doc/specgraph.txt'
--- lib/lp/blueprints/doc/specgraph.txt 2009-05-11 18:19:21 +0000
+++ lib/lp/blueprints/doc/specgraph.txt 2010-07-29 20:06:01 +0000
@@ -1,17 +1,18 @@
-= Specification graphs =
+Specification graphs
+====================
-A SpecGraph object manages a set of SpecNodes and the edges that connect the
-nodes. It knows how to output itself in `dot` format, for use by Graphviz.
+A SpecGraph object manages a set of SpecNodes and the edges that connect
+the nodes. It knows how to output itself in `dot` format, for use by
+Graphviz.
A SpecGraph has a root node, and then various other nodes. You can make
-connections between nodes using its 'link' method. Each node is identified by
-its name, which is the database name of the specification that the node
-represents.
+connections between nodes using its 'link' method. Each node is
+identified by its name, which is the database name of the specification
+that the node represents.
Use the SpecGraph as a factory for creating new nodes. This allows the
SpecGraph to keep track of the nodes that have been added.
-
>>> from lp.blueprints.browser.specification import SpecGraph
>>> g = SpecGraph()
>>> g.url_pattern_for_testing = 'http://whatever/%s'
@@ -37,8 +38,8 @@
>>> print root
<foo>
-Note that the root DOT data doesn't have a URL. This is because we don't
-want a link to the spec we're currently looking at.
+Note that the root DOT data doesn't have a URL. This is because we
+don't want a link to the spec we're currently looking at.
>>> print root.getDOTNodeStatement()
"foo"
@@ -51,12 +52,16 @@
>>> print g.root_node
<foo>
+
>>> print root.name, root.label, root.URL, root.color
foo foo http://whatever/foo red
+
>>> g.getNode('no such name') is None
True
+
>>> g.getNode('foo') is root
True
+
>>> print g.listNodes()
Root is <foo>
<foo>:
@@ -73,7 +78,7 @@
>>> def make_graph(dependency, blocked):
... g = SpecGraph()
... g.url_pattern_for_testing = 'http://whatever/%s'
- ... root = g.newNode(foo, root=True)
+ ... g.newNode(foo, root=True)
... if dependency:
... g.addDependencyNodes(foo)
... if blocked:
@@ -98,73 +103,72 @@
<foo2>:
foo
- >>> print_graph_dot()
- digraph "deptree" {
- graph
- [
- "bgcolor"="#ffffff",
- "mode"="hier",
- "nodesep"="0.01",
- "ranksep"="0.25",
- "ratio"="compress",
- "size"="9.2,9"
- ]
- node
- [
- "fillcolor"="white",
- "fontname"="Sans",
- "fontsize"="11",
- "style"="filled"
- ]
- edge
- [
- "arrowhead"="normal"
- ]
- "foo"
- [
- "color"="red",
- "comment"="something with \" and \n in it",
- "label"="foo",
- "tooltip"="something with \" and \n in it"
- ]
- "foo1"
- [
- "URL"="http://whatever/foo1",
- "color"="black",
- "comment"="foo1",
- "label"="foo1",
- "tooltip"="foo1"
- ]
- "foo11"
- [
- "URL"="http://whatever/foo11",
- "color"="black",
- "comment"="foo11",
- "label"="foo11",
- "tooltip"="foo11"
- ]
- "foo111"
- [
- "URL"="http://whatever/foo111",
- "color"="black",
- "comment"="foo111",
- "label"="foo111",
- "tooltip"="foo111"
- ]
- "foo2"
- [
- "URL"="http://whatever/foo2",
- "color"="black",
- "comment"="foo2",
- "label"="foo2",
- "tooltip"="foo2"
- ]
- "foo1" -> "foo"
- "foo11" -> "foo1"
- "foo111" -> "foo11"
- "foo2" -> "foo"
- }
-
+ >>> print_graph_dot()
+ digraph "deptree" {
+ graph
+ [
+ "bgcolor"="#ffffff",
+ "mode"="hier",
+ "nodesep"="0.01",
+ "ranksep"="0.25",
+ "ratio"="compress",
+ "size"="9.2,9"
+ ]
+ node
+ [
+ "fillcolor"="white",
+ "fontname"="Sans",
+ "fontsize"="11",
+ "style"="filled"
+ ]
+ edge
+ [
+ "arrowhead"="normal"
+ ]
+ "foo"
+ [
+ "color"="red",
+ "comment"="something with \" and \n in it",
+ "label"="foo",
+ "tooltip"="something with \" and \n in it"
+ ]
+ "foo1"
+ [
+ "URL"="http://whatever/foo1",
+ "color"="black",
+ "comment"="foo1",
+ "label"="foo1",
+ "tooltip"="foo1"
+ ]
+ "foo11"
+ [
+ "URL"="http://whatever/foo11",
+ "color"="black",
+ "comment"="foo11",
+ "label"="foo11",
+ "tooltip"="foo11"
+ ]
+ "foo111"
+ [
+ "URL"="http://whatever/foo111",
+ "color"="black",
+ "comment"="foo111",
+ "label"="foo111",
+ "tooltip"="foo111"
+ ]
+ "foo2"
+ [
+ "URL"="http://whatever/foo2",
+ "color"="black",
+ "comment"="foo2",
+ "label"="foo2",
+ "tooltip"="foo2"
+ ]
+ "foo1" -> "foo"
+ "foo11" -> "foo1"
+ "foo111" -> "foo11"
+ "foo2" -> "foo"
+ }
Now, add a circle at the top.
@@ -182,7 +186,6 @@
<foo2>:
foo
-
Now add another circle at the bottom.
>>> foo111.dependencies.append(foo1)
@@ -222,9 +225,9 @@
foo
foo1
-
-And finally, try checking out the blocked specs too. Because of the hack
-earlier, we have a "mirror image" of the dependencies in the blocked speces.
+And finally, try checking out the blocked specs too. Because of the
+hack earlier, we have a "mirror image" of the dependencies in the
+blocked speces.
>>> print_graph(dependency=False, blocked=True)
Root is <foo>
@@ -244,7 +247,8 @@
foo1
-== SpecificationTreeImageTag and SpecificationView ==
+SpecificationTreeImageTag and SpecificationView
+-----------------------------------------------
The SpecificationTreeImageTag subclass will generate a HTML image map
tag when the render() method is called.
@@ -252,7 +256,7 @@
>>> from zope.component import getMultiAdapter
>>> from lp.blueprints.browser.specification import (
... SpecificationTreeImageTag)
- >>> from canonical.launchpad.interfaces import IProductSet
+ >>> from lp.registry.interfaces.product import IProductSet
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
>>> firefox = getUtility(IProductSet).getByName('firefox')
@@ -288,14 +292,16 @@
u'<map id="deptree" name="deptree">'
-== renderGraphvizGraph() error handling ==
+renderGraphvizGraph() error handling
+------------------------------------
The renderGraphvizGraph() method may raise a ProblemRenderingGraph error
-running the subprocess. The error could be caused because the
-data sent to the command is bad:
+running the subprocess. The error could be caused because the data sent
+to the command is bad:
# Replace getDotFileText() with a fake function that will return
# bad data.
+
>>> from lp.blueprints.browser import specification
>>> graph_view_class = specification.SpecificationTreeGraphView
>>> original_getDotFileText = graph_view_class.getDotFileText
@@ -311,9 +317,9 @@
...
ProblemRenderingGraph: (... syntax error in line 1 near 'bad'...)
-The SpecificationTreeImageTag.render() method captures the raised
-error and directly converts it into an oops report. The markup contains
-a message explaining that the image was not linked.
+The SpecificationTreeImageTag.render() method captures the raised error
+and directly converts it into an oops report. The markup contains a
+message explaining that the image was not linked.
>>> print graph_view.render()
<img src="deptree.png" usemap="#deptree" />
@@ -325,6 +331,7 @@
ProblemRenderingGraph (... syntax error in line 1 near 'bad'...)
# Restore the getDotFileText() method.
+
>>> graph_view_class.getDotFileText = original_getDotFileText
The renderGraphvizGraph() pipes data to a subprocess. That subprocess
@@ -332,6 +339,7 @@
# Replace the Popen object with a fake function that will raise
# an OSError.
+
>>> original_popen = specification.Popen
>>> def fake_popen(*args, **kwargs):
... raise OSError(12, 'Cannot allocate memory')
@@ -346,8 +354,8 @@
OSError: [Errno 12] Cannot allocate memory
The OSError raised creating the image map does not break the spec index
-page. Again, the image map was replaced with an suggestion to reload
-the page to link the image.
+page. Again, the image map was replaced with an suggestion to reload the
+page to link the image.
>>> print graph_view.render()
<img src="deptree.png" usemap="#deptree" />
@@ -358,9 +366,9 @@
>>> print oops_report.type, oops_report.value
OSError [Errno 12] Cannot allocate memory
-If an error occurs during the render of the PNG image, the fail
-over image (icing/blueprints-deptree-error.png) is returned. It's size
-is 3092 bytes.
+If an error occurs during the render of the PNG image, the fail over
+image (icing/blueprints-deptree-error.png) is returned. It's size is
+3092 bytes.
>>> graph_view = getMultiAdapter(
... (svg_support, request), name="deptree.png")
@@ -378,10 +386,10 @@
>>> fail_over_image_length
3092
-The dependency graph image is rendered correctly when Popen is
-restored.
+The dependency graph image is rendered correctly when Popen is restored.
# Restore the Popen object.
+
>>> specification.Popen = original_popen
>>> graph_view = getMultiAdapter(
=== modified file 'lib/lp/blueprints/doc/specification-branch.txt'
--- lib/lp/blueprints/doc/specification-branch.txt 2009-07-23 17:49:31 +0000
+++ lib/lp/blueprints/doc/specification-branch.txt 2010-07-29 20:06:01 +0000
@@ -1,7 +1,9 @@
-= Specification Branch Links =
+Specification Branch Links
+==========================
>>> from canonical.launchpad.webapp.testing import verifyObject
- >>> from canonical.launchpad.interfaces import ISpecificationBranch
+ >>> from lp.blueprints.interfaces.specificationbranch import (
+ ... ISpecificationBranch)
>>> from lp.blueprints.model.specification import SpecificationBranch
>>> verifyObject(ISpecificationBranch, SpecificationBranch.get(1))
True
@@ -10,9 +12,11 @@
the Ubuntu media-integrity-check specifcation has a branch link:
>>> from zope.component import getUtility
- >>> from canonical.launchpad.interfaces import (
- ... ILaunchpadCelebrities, IPersonSet)
+ >>> from canonical.launchpad.interfaces.launchpad import (
+ ... ILaunchpadCelebrities)
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> from lp.code.interfaces.branchlookup import IBranchLookup
+
>>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
>>> spec = ubuntu.getSpecification('media-integrity-check')
=== modified file 'lib/lp/blueprints/doc/specification-notifications.txt'
--- lib/lp/blueprints/doc/specification-notifications.txt 2009-11-24 22:58:53 +0000
+++ lib/lp/blueprints/doc/specification-notifications.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= Email Notifications for Specifications =
+Email Notifications for Specifications
+======================================
When a specification is edited, an email notification is sent out to
all the related people. We send out notifications only on certain
@@ -7,7 +8,7 @@
Changing the status:
>>> from zope.component import getMultiAdapter
- >>> from canonical.launchpad.interfaces import IProductSet
+ >>> from lp.registry.interfaces.product import IProductSet
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
>>> login('foo.bar@xxxxxxxxxxxxx')
@@ -33,7 +34,8 @@
the approver, Cprov, and the drafter, Robert.
>>> related_people = [
- ... svg_support.owner, svg_support.assignee, svg_support.drafter, svg_support.approver]
+ ... svg_support.owner, svg_support.assignee, svg_support.drafter,
+ ... svg_support.approver]
>>> related_people += [
... subscription.person for subscription in svg_support.subscriptions]
>>> related_people_addresses = [
@@ -69,9 +71,10 @@
Let's set a different approver and add a subscriber.
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> mark = getUtility(IPersonSet).getByEmail('mark@xxxxxxxxxxx')
- >>> sample_person = getUtility(IPersonSet).getByEmail('test@xxxxxxxxxxxxx')
+ >>> sample_person = getUtility(IPersonSet).getByEmail(
+ ... 'test@xxxxxxxxxxxxx')
>>> svg_support.approver = mark
>>> svg_support.subscribe(sample_person, sample_person, False)
<...>
@@ -94,7 +97,7 @@
The added subscriber will also receive a notification that they
are now subscribed.
- >>> x = sorted([toaddrs for fromaddr, toaddrs, message in stub.test_emails])
+ >>> x = sorted(toaddrs for fromaddr, toaddrs, message in stub.test_emails)
>>> for addr in x: print addr
['andrew.bennetts@xxxxxxxxxxxxxxx']
['carlos@xxxxxxxxxxxxx']
@@ -120,12 +123,12 @@
>>> status_notification['Subject']
'[Blueprint svg-support] Support Native SVG Objects'
>>> body = status_notification.get_payload(decode=True)
- >>> print body #doctest: -NORMALIZE_WHITESPACE
+ >>> print body
Blueprint changed by Foo Bar:
<BLANKLINE>
Definition Status: Drafting => Pending Approval
<BLANKLINE>
- --
+ --
Support Native SVG Objects
http://blueprints.launchpad.dev/firefox/+spec/svg-support
<BLANKLINE>
@@ -160,7 +163,7 @@
>>> status_notification['Subject']
'[Blueprint svg-support] Support Native SVG Objects'
>>> body = status_notification.get_payload(decode=True)
- >>> print body #doctest: -NORMALIZE_WHITESPACE
+ >>> print body
Blueprint changed by Foo Bar:
<BLANKLINE>
Whiteboard set to:
@@ -169,7 +172,7 @@
<BLANKLINE>
Another paragraph
<BLANKLINE>
- --
+ --
Support Native SVG Objects
http://blueprints.launchpad.dev/firefox/+spec/svg-support
<BLANKLINE>
@@ -200,7 +203,7 @@
>>> status_notification['Subject']
'[Blueprint svg-support] Support Native SVG Objects'
>>> body = status_notification.get_payload(decode=True)
- >>> print body #doctest: -NORMALIZE_WHITESPACE
+ >>> print body
Blueprint changed by Foo Bar:
<BLANKLINE>
Definition Status: Pending Approval => Approved
@@ -208,11 +211,11 @@
Whiteboard changed:
- This is a long line, which will be wrapped in the email, since it's
- longer than 72 characters.
- -
+ -
- Another paragraph
+ Excellent work.
<BLANKLINE>
- --
+ --
Support Native SVG Objects
http://blueprints.launchpad.dev/firefox/+spec/svg-support
<BLANKLINE>
@@ -240,12 +243,12 @@
>>> status_notification['Subject']
'[Blueprint svg-support] Support Native SVG Objects'
>>> body = status_notification.get_payload(decode=True)
- >>> print body #doctest: -NORMALIZE_WHITESPACE
+ >>> print body
Blueprint changed by Foo Bar:
<BLANKLINE>
Priority: High => Essential
<BLANKLINE>
- --
+ --
Support Native SVG Objects
http://blueprints.launchpad.dev/firefox/+spec/svg-support
<BLANKLINE>
@@ -274,14 +277,14 @@
>>> status_notification['Subject']
'[Blueprint svg-support] Support Native SVG Objects'
>>> body = status_notification.get_payload(decode=True)
- >>> print body #doctest: -NORMALIZE_WHITESPACE
+ >>> print body
Blueprint changed by Foo Bar:
<BLANKLINE>
Approver: Mark Shuttleworth => (none)
Assignee: (none) => Mark Shuttleworth
Drafter: Robert Collins => Foo Bar
<BLANKLINE>
- --
+ --
Support Native SVG Objects
http://blueprints.launchpad.dev/firefox/+spec/svg-support
<BLANKLINE>
=== modified file 'lib/lp/blueprints/doc/specification.txt'
--- lib/lp/blueprints/doc/specification.txt 2010-04-21 16:15:36 +0000
+++ lib/lp/blueprints/doc/specification.txt 2010-07-29 20:06:01 +0000
@@ -1,22 +1,24 @@
-= Specifications =
+Specifications
+==============
A feature specification is a document that describes an idea for an
enhancement to a product. Launchpad allows you to register your
-specification and then walk it through the approval process. You can have
-specifications for products, and also for distributions.
+specification and then walk it through the approval process. You can
+have specifications for products, and also for distributions.
All Milestone creation and retrieval is done through IMilestoneSet.
IMilestoneSet can be accessed as a utility.
>>> from zope.component import getUtility
- >>> from canonical.launchpad.interfaces import ISpecificationSet
+ >>> from lp.blueprints.interfaces.specification import (
+ ... ISpecificationSet, SpecificationDefinitionStatus,
+ ... SpecificationImplementationStatus, SpecificationPriority)
>>> specset = getUtility(ISpecificationSet)
To create a new Specification, use ISpecificationSet.new:
- >>> from canonical.launchpad.interfaces import (
- ... IProductSet, SpecificationDefinitionStatus,
- ... SpecificationImplementationStatus, SpecificationPriority)
+ >>> from lp.registry.interfaces.product import IProductSet
+
>>> productset = getUtility(IProductSet)
>>> upstream_firefox = productset.get(4)
>>> from lp.registry.model.person import Person
@@ -34,30 +36,36 @@
True
It should be possible to retrieve a specification by its name
+
>>> upstream_firefox.getSpecification('mng').name
u'mng'
-And if we try to retrieve a non-existent specification we should get None
+And if we try to retrieve a non-existent specification we should get
+None
+
>>> print upstream_firefox.getSpecification('nonexistentspec')
None
It's also possible to retrieve a specification by its URL
+
>>> specset.getByURL('http://developer.mozilla.org/en/docs/SVG').specurl
u'http://developer.mozilla.org/en/docs/SVG'
And if there's no specification with the given URL we should get None
+
>>> print specset.getByURL('http://no-url.com')
None
-A specification could be attached to a distribution, or a product. We call
-this the specification target.
+A specification could be attached to a distribution, or a product. We
+call this the specification target.
>>> print newspec.target.name
firefox
We attach now a spec to a distribution.
- >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
+ >>> from canonical.launchpad.interfaces.launchpad import (
+ ... ILaunchpadCelebrities)
>>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
>>> mark = Person.byName('mark')
>>> ubuspec = specset.new('fix-spec-permissions',
@@ -70,19 +78,22 @@
>>> print ubuspec.name
fix-spec-permissions
-The Ubuntu distro is owned by the Ubuntu team, ubuntu-team. jdub is a member,
-and therefore should be able to edit any spec attached to it (but not
-specs attached to mozilla-firefox).
+The Ubuntu distro is owned by the Ubuntu team, ubuntu-team. jdub is a
+member, and therefore should be able to edit any spec attached to it
+(but not specs attached to mozilla-firefox).
>>> from canonical.launchpad.webapp.authorization import check_permission
>>> print ubuntu.owner.name
ubuntu-team
+
>>> jdub = Person.byName('jdub')
>>> jdub.inTeam(ubuntu.owner)
True
+
>>> login(jdub.preferredemail.email)
>>> check_permission('launchpad.Edit', ubuspec)
True
+
>>> check_permission('launchpad.Edit', newspec)
False
@@ -90,6 +101,7 @@
>>> ubuspec in specset.all_specifications
True
+
>>> specset.has_any_specifications
True
@@ -98,13 +110,14 @@
True
-== SpecificationDelta ==
+SpecificationDelta
+------------------
When we modify a specification, we can get a delta of the changes using
ISpecification.getDelta(). If there are no changes, None will be
returned:
- >>> from canonical.launchpad.interfaces import IBugSet
+ >>> from lp.bugs.interfaces.bug import IBugSet
>>> from lazr.lifecycle.snapshot import Snapshot
>>> from zope.interface import providedBy
>>> unmodified_spec = Snapshot(ubuspec, providing=providedBy(ubuspec))
@@ -130,50 +143,61 @@
>>> delta = ubuspec.getDelta(unmodified_spec, jdub)
>>> delta.specification == ubuspec
True
+
>>> delta.user == jdub
True
>>> print delta.title
New Title
+
>>> print delta.summary
New summary.
+
>>> print delta.specurl
http://www.ubuntu.com/NewSpec
+
>>> print delta.distroseries.name
hoary
>>> print delta.name['old']
fix-spec-permissions
+
>>> print delta.name['new']
new-spec
>>> print delta.priority['old'].title
Undefined
+
>>> print delta.priority['new'].title
Low
>>> print delta.definition_status['old'].title
Approved
+
>>> print delta.definition_status['new'].title
Drafting
>>> print delta.approver['old'] is None
True
+
>>> print delta.approver['new'] == mark
True
>>> print delta.assignee['old'] is None
True
+
>>> print delta.assignee['new'] == jdub
True
>>> print delta.drafter['old'] is None
True
+
>>> print delta.drafter['new'] == jdub
True
>>> print delta.whiteboard['old'] is None
True
+
>>> print delta.whiteboard['new']
New whiteboard comments.
@@ -182,26 +206,30 @@
>>> delta.bugs_unlinked is None
True
+
>>> delta.milestone is None
True
+
>>> delta.productseries is None
True
+
>>> delta.target is None
True
-== Specification Searching ==
+Specification Searching
+-----------------------
The "SpecificationSet" can be used to search across all specifications.
We can filter for specifications that contain specific text, across all
specifications:
- >>> for spec in specset.specifications(filter=['install']):
- ... print spec.name, spec.target.name
- cluster-installation kubuntu
- extension-manager-upgrades firefox
- media-integrity-check ubuntu
+ >>> for spec in specset.specifications(filter=['install']):
+ ... print spec.name, spec.target.name
+ cluster-installation kubuntu
+ extension-manager-upgrades firefox
+ media-integrity-check ubuntu
Specs from inactive products are filtered out.
@@ -209,6 +237,7 @@
>>> login('mark@xxxxxxxxxxx')
# Unlink the source packages so the project can be deactivated.
+
>>> from lp.testing import unlink_source_packages
>>> unlink_source_packages(upstream_firefox)
>>> upstream_firefox.active = False
@@ -218,41 +247,49 @@
cluster-installation kubuntu
media-integrity-check ubuntu
-
Reset firefox so we don't mess up later tests.
- >>> upstream_firefox.active = True
- >>> flush_database_updates()
-
-
-== Specification Blockers and Dependencies ==
-
-We keep track of specification blocking and dependencies. For each spec, you
-can ask for its dependencies, or the specs which it blocks. And you can ask
-for the full set of dependencies-and-their-dependencies, as well as the full
-set of specs-which-block-this-one-and-all-the-specs-that-block-them-too.
-
- >>> from canonical.launchpad.interfaces import IProductSet
- >>> efourx = getUtility(IProductSet).getByName('firefox').getSpecification('e4x')
- >>> for spec in efourx.dependencies: print spec.name
- svg-support
- >>> for spec in efourx.all_deps: print spec.name
- svg-support
- >>> for spec in efourx.blocked_specs: print spec.name
- canvas
- >>> for spec in efourx.all_blocked: print spec.name
- canvas
- >>> canvas = efourx.blocked_specs[0]
- >>> svg = efourx.dependencies[0]
- >>> for spec in svg.all_blocked: print spec.name
- e4x
- canvas
- >>> for spec in canvas.all_deps: print spec.name
- svg-support
- e4x
-
-
-=== Dependency mapping - `ISpecificationSet.getDependencyDict` ===
+ >>> upstream_firefox.active = True
+ >>> flush_database_updates()
+
+
+Specification Blockers and Dependencies
+---------------------------------------
+
+We keep track of specification blocking and dependencies. For each spec,
+you can ask for its dependencies, or the specs which it blocks. And you
+can ask for the full set of dependencies-and-their-dependencies, as well
+as the full set of specs-which-block-this-one-and-all-the-specs-that-
+block-them-too.
+
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> efourx = getUtility(IProductSet).getByName(
+ ... 'firefox').getSpecification('e4x')
+ >>> for spec in efourx.dependencies: print spec.name
+ svg-support
+
+ >>> for spec in efourx.all_deps: print spec.name
+ svg-support
+
+ >>> for spec in efourx.blocked_specs: print spec.name
+ canvas
+
+ >>> for spec in efourx.all_blocked: print spec.name
+ canvas
+
+ >>> canvas = efourx.blocked_specs[0]
+ >>> svg = efourx.dependencies[0]
+ >>> for spec in svg.all_blocked: print spec.name
+ e4x
+ canvas
+
+ >>> for spec in canvas.all_deps: print spec.name
+ svg-support
+ e4x
+
+
+Dependency mapping - `ISpecificationSet.getDependencyDict`
+..........................................................
In order to implement the specification plan page efficiently,
`ISpecificationSet` provides a utility method that returns a mapping
@@ -289,10 +326,13 @@
>>> spec_a.createDependency(spec_b)
<SpecificationDependency at ...>
+
>>> spec_a.createDependency(spec_c)
<SpecificationDependency at ...>
+
>>> spec_c.createDependency(spec_d)
<SpecificationDependency at ...>
+
>>> deps_dict = specset.getDependencyDict(
... [spec_a, spec_b, spec_c, spec_d])
>>> spec_deps = [(specset.get(key).name, value) for
@@ -304,234 +344,288 @@
spec-a --> spec-b, spec-c
spec-c --> spec-d
-
Passing in an empty sequences returns an empty dict:
>>> specset.getDependencyDict([])
{}
-== Specification Subscriptions ==
+Specification Subscriptions
+---------------------------
-You can subscribe to a specification, which means that you will be notified
-of changes to that spec (and changes to the wiki page for that spec will be
-passed on to you too!).
+You can subscribe to a specification, which means that you will be
+notified of changes to that spec (and changes to the wiki page for that
+spec will be passed on to you too!).
It is possible to indicate that some subscribers are essential to the
discussion of the spec.
- >>> for subscriber in canvas.subscribers: print subscriber.name
-
- >>> from canonical.launchpad.interfaces import IPersonSet
- >>> jdub = getUtility(IPersonSet).getByName('jdub')
- >>> sub = canvas.subscribe(jdub, jdub, False)
- >>> print sub.essential
- False
-
- >>> samesub = canvas.getSubscriptionByName('jdub')
- >>> print samesub.essential
- False
-
-
-
-== Specification Goals ==
+ >>> for subscriber in canvas.subscribers: print subscriber.name
+
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> jdub = getUtility(IPersonSet).getByName('jdub')
+ >>> sub = canvas.subscribe(jdub, jdub, False)
+ >>> print sub.essential
+ False
+
+ >>> samesub = canvas.getSubscriptionByName('jdub')
+ >>> print samesub.essential
+ False
+
+
+Specification Goals
+-------------------
We can propose a specification as a feature goal for a particular series
-or distroseries. That spec can then be approved or declined by the series
-drivers.
+or distroseries. That spec can then be approved or declined by the
+series drivers.
First, we will show how to propose a goal, and what metadata is recorded
when we do.
- >>> e4x = upstream_firefox.getSpecification('e4x')
- >>> onezero = upstream_firefox.getSeries('1.0')
- >>> e4x.goal is not None
- False
- >>> e4x.goal_proposer is not None
- False
- >>> e4x.date_goal_proposed is not None
- False
- >>> e4x.proposeGoal(onezero, jdub)
- >>> e4x.goal is not None
- True
- >>> e4x.goal_proposer.name
- u'jdub'
- >>> e4x.date_goal_proposed is not None
- True
- >>> e4x.goalstatus.title
- 'Proposed'
+ >>> e4x = upstream_firefox.getSpecification('e4x')
+ >>> onezero = upstream_firefox.getSeries('1.0')
+ >>> e4x.goal is not None
+ False
+
+ >>> e4x.goal_proposer is not None
+ False
+
+ >>> e4x.date_goal_proposed is not None
+ False
+
+ >>> e4x.proposeGoal(onezero, jdub)
+ >>> e4x.goal is not None
+ True
+
+ >>> e4x.goal_proposer.name
+ u'jdub'
+
+ >>> e4x.date_goal_proposed is not None
+ True
+
+ >>> e4x.goalstatus.title
+ 'Proposed'
At this stage, the feature goal is not approved.
- >>> e4x.goal_decider is not None
- False
- >>> e4x.date_goal_decided is not None
- False
+ >>> e4x.goal_decider is not None
+ False
+
+ >>> e4x.date_goal_decided is not None
+ False
We can then accept the goal.
- >>> e4x.acceptBy(mark)
- >>> e4x.goalstatus.title
- 'Accepted'
- >>> e4x.goal_decider.name
- u'mark'
- >>> e4x.date_goal_decided is not None
- True
+ >>> e4x.acceptBy(mark)
+ >>> e4x.goalstatus.title
+ 'Accepted'
+
+ >>> e4x.goal_decider.name
+ u'mark'
+
+ >>> e4x.date_goal_decided is not None
+ True
We can change our mind, and decline the goal now.
- >>> e4x.declineBy(mark)
- >>> e4x.goalstatus.title
- 'Declined'
+ >>> e4x.declineBy(mark)
+ >>> e4x.goalstatus.title
+ 'Declined'
And finally, if we propose a new goal, then the decision status is
invalidated.
- >>> trunk = upstream_firefox.getSeries('trunk')
- >>> e4x.proposeGoal(trunk, mark)
- >>> e4x.goalstatus.title
- 'Proposed'
- >>> e4x.goal_decider is not None
- False
- >>> e4x.date_goal_decided is not None
- False
-
-
-== Specification Lifecycle ==
+ >>> trunk = upstream_firefox.getSeries('trunk')
+ >>> e4x.proposeGoal(trunk, mark)
+ >>> e4x.goalstatus.title
+ 'Proposed'
+
+ >>> e4x.goal_decider is not None
+ False
+
+ >>> e4x.date_goal_decided is not None
+ False
+
+
+Specification Lifecycle
+-----------------------
We keep track of the progress of the specification, from being "not
-started", to "started", to "complete", and we track who started it and who
-finished it, and when they updated the relevant status bits. Currently this
-is done by setting the statuses, then calling a method which examines the
-state of the spec and updates any lifecycle metadata that needs updating.
+started", to "started", to "complete", and we track who started it and
+who finished it, and when they updated the relevant status bits.
+Currently this is done by setting the statuses, then calling a method
+which examines the state of the spec and updates any lifecycle metadata
+that needs updating.
We will use the "canvas" spec to show of this lifecycle tracking.
First, lets show that canvas has not really progressed very far.
- >>> canvas.definition_status.title
- 'New'
- >>> canvas.implementation_status.title
- 'Unknown'
- >>> print canvas.starter
- None
- >>> canvas.informational
- False
+ >>> canvas.definition_status.title
+ 'New'
+
+ >>> canvas.implementation_status.title
+ 'Unknown'
+
+ >>> print canvas.starter
+ None
+
+ >>> canvas.informational
+ False
Now, we want to show that setting the states can update the relevant
metadata. First we will make the spec "started".
- >>> canvas.implementation_status = SpecificationImplementationStatus.STARTED
- >>> newstate = canvas.updateLifecycleStatus(jdub)
- >>> newstate is None
- False
- >>> print newstate.title
- Started
- >>> canvas.starter is not None # update should have set starter
- True
- >>> canvas.date_started is not None # and date started
- True
- >>> canvas.completer is not None # but this is still incomplete
- False
- >>> canvas.date_completed is not None
- False
+ >>> canvas.implementation_status = (
+ ... SpecificationImplementationStatus.STARTED)
+ >>> newstate = canvas.updateLifecycleStatus(jdub)
+ >>> newstate is None
+ False
+
+ >>> print newstate.title
+ Started
+
+ >>> canvas.starter is not None # update should have set starter
+ True
+
+ >>> canvas.date_started is not None # and date started
+ True
+
+ >>> canvas.completer is not None # but this is still incomplete
+ False
+
+ >>> canvas.date_completed is not None
+ False
Now we are making slow progress. We want to show that, from a lifecycle
point of view, nothing has changed, so we expect the lifecycle update to
return None.
- >>> canvas.implementation_status = SpecificationImplementationStatus.SLOW
- >>> newstate = canvas.updateLifecycleStatus(jdub)
- >>> newstate is None
- True
+ >>> canvas.implementation_status = SpecificationImplementationStatus.SLOW
+ >>> newstate = canvas.updateLifecycleStatus(jdub)
+ >>> newstate is None
+ True
Oops! Let's say that was a mistake, we instead want to DEFER the start
of this work.
- >>> canvas.implementation_status = SpecificationImplementationStatus.DEFERRED
- >>> newstate = canvas.updateLifecycleStatus(jdub)
- >>> newstate is None
- False
- >>> print newstate.title
- Not started
- >>> canvas.starter is not None # update should have reset starter
- False
- >>> canvas.date_started is not None # and date started
- False
- >>> canvas.completer is not None # but this is still incomplete
- False
- >>> canvas.date_completed is not None
- False
+ >>> canvas.implementation_status = (
+ ... SpecificationImplementationStatus.DEFERRED)
+ >>> newstate = canvas.updateLifecycleStatus(jdub)
+ >>> newstate is None
+ False
+
+ >>> print newstate.title
+ Not started
+
+ >>> canvas.starter is not None # update should have reset starter
+ False
+
+ >>> canvas.date_started is not None # and date started
+ False
+
+ >>> canvas.completer is not None # but this is still incomplete
+ False
+
+ >>> canvas.date_completed is not None
+ False
Now, let's say that we have actually completed this spec.
- >>> canvas.implementation_status = SpecificationImplementationStatus.IMPLEMENTED
- >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
- >>> newstate = canvas.updateLifecycleStatus(jdub)
- >>> newstate is None
- False
- >>> print newstate.title
- Complete
- >>> canvas.starter is not None # update should have set starter
- True
- >>> canvas.date_started is not None # and date started
- True
- >>> canvas.completer is not None # but this is still incomplete
- True
- >>> canvas.date_completed is not None
- True
+ >>> canvas.implementation_status = (
+ ... SpecificationImplementationStatus.IMPLEMENTED)
+ >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
+ >>> newstate = canvas.updateLifecycleStatus(jdub)
+ >>> newstate is None
+ False
+
+ >>> print newstate.title
+ Complete
+
+ >>> canvas.starter is not None # update should have set starter
+ True
+
+ >>> canvas.date_started is not None # and date started
+ True
+
+ >>> canvas.completer is not None # but this is still incomplete
+ True
+
+ >>> canvas.date_completed is not None
+ True
Hmm... now we want to roll back. We can roll back either to "started" or
all the way to "not started".
- >>> canvas.implementation_status = SpecificationImplementationStatus.NOTSTARTED
- >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
- >>> newstate = canvas.updateLifecycleStatus(jdub)
- >>> newstate is None
- False
- >>> print newstate.title
- Not started
- >>> canvas.starter is not None # update should have reset starter
- False
- >>> canvas.date_started is not None # and date started
- False
- >>> canvas.completer is not None # but this is still incomplete
- False
- >>> canvas.date_completed is not None
- False
+ >>> canvas.implementation_status = (
+ ... SpecificationImplementationStatus.NOTSTARTED)
+ >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
+ >>> newstate = canvas.updateLifecycleStatus(jdub)
+ >>> newstate is None
+ False
+
+ >>> print newstate.title
+ Not started
+
+ >>> canvas.starter is not None # update should have reset starter
+ False
+
+ >>> canvas.date_started is not None # and date started
+ False
+
+ >>> canvas.completer is not None # but this is still incomplete
+ False
+
+ >>> canvas.date_completed is not None
+ False
OK. Let's make it complete again.
- >>> canvas.implementation_status = SpecificationImplementationStatus.IMPLEMENTED
- >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
- >>> newstate = canvas.updateLifecycleStatus(jdub)
- >>> newstate is None
- False
- >>> print newstate.title
- Complete
- >>> canvas.starter is not None # update should have set starter
- True
- >>> canvas.date_started is not None # and date started
- True
- >>> canvas.completer is not None # this is complete
- True
- >>> canvas.date_completed is not None
- True
+ >>> canvas.implementation_status = (
+ ... SpecificationImplementationStatus.IMPLEMENTED)
+ >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
+ >>> newstate = canvas.updateLifecycleStatus(jdub)
+ >>> newstate is None
+ False
+
+ >>> print newstate.title
+ Complete
+
+ >>> canvas.starter is not None # update should have set starter
+ True
+
+ >>> canvas.date_started is not None # and date started
+ True
+
+ >>> canvas.completer is not None # this is complete
+ True
+
+ >>> canvas.date_completed is not None
+ True
And finally show the rollback to "started".
- >>> canvas.implementation_status = SpecificationImplementationStatus.STARTED
- >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
- >>> newstate = canvas.updateLifecycleStatus(jdub)
- >>> newstate is None
- False
- >>> print newstate.title
- Started
- >>> canvas.starter is not None # update should have set starter
- True
- >>> canvas.date_started is not None # and date started
- True
- >>> canvas.completer is not None # but this is still incomplete
- False
- >>> canvas.date_completed is not None
- False
+ >>> canvas.implementation_status = (
+ ... SpecificationImplementationStatus.STARTED)
+ >>> canvas.definition_status = SpecificationDefinitionStatus.APPROVED
+ >>> newstate = canvas.updateLifecycleStatus(jdub)
+ >>> newstate is None
+ False
+
+ >>> print newstate.title
+ Started
+
+ >>> canvas.starter is not None # update should have set starter
+ True
+
+ >>> canvas.date_started is not None # and date started
+ True
+
+ >>> canvas.completer is not None # but this is still incomplete
+ False
+
+ >>> canvas.date_completed is not None
+ False
+
+
=== modified file 'lib/lp/blueprints/doc/specificationmessage.txt'
--- lib/lp/blueprints/doc/specificationmessage.txt 2009-08-10 12:34:20 +0000
+++ lib/lp/blueprints/doc/specificationmessage.txt 2010-07-29 20:06:01 +0000
@@ -1,11 +1,13 @@
-= Specification Messages =
+Specification Messages
+======================
Specification messages are messages associated with blueprints. A
specifiction message is described by the ISpecificationMessage
interface.
-== Creating specification messages ==
+Creating specification messages
+-------------------------------
To create a specification message, use
ISpecificationMessageSet.createMessage:
@@ -17,7 +19,7 @@
>>> specset = getUtility(ISpecificationSet)
>>> specmessageset = getUtility(ISpecificationMessageSet)
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> sample_person = getUtility(IPersonSet).get(12)
>>> spec = specset.getByURL(
... 'http://developer.mozilla.org/en/docs/SVG')
@@ -30,7 +32,8 @@
u'test message subject'
-== Retrieving specification messages ==
+Retrieving specification messages
+---------------------------------
ISpecificationMessageSet represents the set of all messages in the
system. An individual ISpecificationMessage can be retrieved with
@@ -39,3 +42,5 @@
>>> specmessage_one = specmessageset.get(1)
>>> specmessage_one.message.subject
u'test message subject'
+
+
=== modified file 'lib/lp/blueprints/doc/sprint-meeting-export.txt'
--- lib/lp/blueprints/doc/sprint-meeting-export.txt 2009-10-28 21:51:18 +0000
+++ lib/lp/blueprints/doc/sprint-meeting-export.txt 2010-07-29 20:06:01 +0000
@@ -1,140 +1,147 @@
-
The sprint meeting export view exports information about the
-specifications to be discussed at a given sprint. The view is
-primarily designed for use with the sprint scheduler tool.
+specifications to be discussed at a given sprint. The view is primarily
+designed for use with the sprint scheduler tool.
While the data could be used by other tools, it is not a stable
-interface and may change in the future. The name of the view
-('+temp-meeting-export') and the comment at the top of the XML are
-intended to communicate this.
-
+interface and may change in the future. The name of the view ('+temp-
+meeting-export') and the comment at the top of the XML are intended to
+communicate this.
First we import the classes required to test the view:
- >>> from datetime import datetime
- >>> from pytz import timezone
- >>> from zope.component import getUtility, getMultiAdapter
- >>> from canonical.launchpad.interfaces import (
- ... ISprintSet, IPersonSet, IProductSet)
- >>> from lp.blueprints.browser.sprint import SprintMeetingExportView
- >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
-
+ >>> from datetime import datetime
+ >>> from pytz import timezone
+ >>> from zope.component import getUtility, getMultiAdapter
+ >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> from lp.blueprints.interfaces.sprint import ISprintSet
+ >>> from lp.blueprints.browser.sprint import SprintMeetingExportView
Look up a few Launchpad objects to be used in the tests
- >>> ubz = getUtility(ISprintSet)['ubz']
- >>> carlos = getUtility(IPersonSet).getByName('carlos')
- >>> mark = getUtility(IPersonSet).getByName('mark')
- >>> sampleperson = getUtility(IPersonSet).getByName('name12')
- >>> firefox = getUtility(IProductSet).getByName('firefox')
- >>> svg_support = firefox.getSpecification('svg-support')
- >>> ext_spec = firefox.getSpecification('extension-manager-upgrades')
- >>> js_spec = firefox.getSpecification('e4x')
-
+ >>> ubz = getUtility(ISprintSet)['ubz']
+ >>> carlos = getUtility(IPersonSet).getByName('carlos')
+ >>> mark = getUtility(IPersonSet).getByName('mark')
+ >>> sampleperson = getUtility(IPersonSet).getByName('name12')
+ >>> firefox = getUtility(IProductSet).getByName('firefox')
+ >>> svg_support = firefox.getSpecification('svg-support')
+ >>> ext_spec = firefox.getSpecification('extension-manager-upgrades')
+ >>> js_spec = firefox.getSpecification('e4x')
Create a view for the UBZ sprint meeting export:
- >>> request = LaunchpadTestRequest()
- >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
+ >>> request = LaunchpadTestRequest()
+ >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
Verify that the view is a SprintMeetingExportView:
- >>> isinstance(view, SprintMeetingExportView)
- True
+ >>> isinstance(view, SprintMeetingExportView)
+ True
There are currently no people registered for the sprint:
- >>> view.initialize()
- >>> view.attendees
- []
+ >>> view.initialize()
+ >>> view.attendees
+ []
While there are three sprints registered for the ubz sprint, only one
-has been accepted. So that is the only one that is exposed by the
-view:
-
- >>> ubz.specificationLinks().count()
- 3
- >>> len(view.specifications)
- 1
- >>> print view.specifications[0]['spec'].name
- extension-manager-upgrades
-
+has been accepted. So that is the only one that is exposed by the view:
+
+ >>> ubz.specificationLinks().count()
+ 3
+
+ >>> len(view.specifications)
+ 1
+
+ >>> print view.specifications[0]['spec'].name
+ extension-manager-upgrades
We now subscribe Sample Person to the Extension Manager Upgrades spec
and check the list of interested people:
- >>> essential=False
- >>> ext_spec.subscribe(sampleperson, sampleperson, essential)
- <SpecificationSubscription at ...>
- >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
- >>> view.initialize()
+ >>> essential=False
+ >>> ext_spec.subscribe(sampleperson, sampleperson, essential)
+ <SpecificationSubscription at ...>
+
+ >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
+ >>> view.initialize()
The person does not show up as interested in the spec though. Only the
specification assignee is listed (the drafter would be too if one was
assigned).
- >>> sorted(person['name'] for person in view.specifications[0]['interested'])
- [u'carlos']
+ >>> sorted(person['name']
+ ... for person in view.specifications[0]['interested'])
+ [u'carlos']
This is because sample person has not registered as an attendee of the
sprint. If we add them as an attendee, then they will be available:
- >>> time_starts = datetime(2005, 10, 8, 7, 0, 0, tzinfo=timezone('UTC'))
- >>> time_ends = datetime(2005, 11, 17, 20, 0, 0, tzinfo=timezone('UTC'))
- >>> ubz.attend(sampleperson, time_starts, time_ends, True)
- <SprintAttendance at ...>
+ >>> time_starts = datetime(2005, 10, 8, 7, 0, 0, tzinfo=timezone('UTC'))
+ >>> time_ends = datetime(2005, 11, 17, 20, 0, 0, tzinfo=timezone('UTC'))
+ >>> ubz.attend(sampleperson, time_starts, time_ends, True)
+ <SprintAttendance at ...>
- >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
- >>> view.initialize()
- >>> sorted(person['name'] for person in view.specifications[0]['interested'])
- [u'carlos', u'name12']
+ >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
+ >>> view.initialize()
+ >>> sorted(person['name']
+ ... for person in view.specifications[0]['interested'])
+ [u'carlos', u'name12']
The person is also included in the list of attendees:
- >>> len(view.attendees)
- 1
- >>> print view.attendees[0]['name']
- name12
- >>> print view.attendees[0]['displayname']
- Sample Person
- >>> print view.attendees[0]['start']
- 2005-10-08T07:00:00Z
- >>> print view.attendees[0]['end']
- 2005-11-17T20:00:00Z
-
-
-If a specification's priority is undefined or marked as not for us,
-then it is not included in the meeting list for the sprint. The
-javascript spec is one such spec. First we will accept it for the
-sprint:
-
- >>> print js_spec.priority.name
- NOTFORUS
- >>> link = js_spec.sprint_links[0]
- >>> link.sprint == ubz
- True
- >>> ubz.acceptSpecificationLinks([link.id], mark)
- 0
-
-Even though the Javascript spec has now been accepted for the sprint now,
-it is not listed by the view because of its priority:
-
- >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
- >>> view.initialize()
- >>> spec_names = [spec['spec'].name for spec in view.specifications]
- >>> js_spec.name not in spec_names
- True
-
-If we decline the extension manager spec, it disapears from the list
-of specs:
-
- >>> link = ext_spec.sprint_links[0]
- >>> link.sprint == ubz
- True
- >>> ubz.declineSpecificationLinks([link.id], mark)
- 0
-
- >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
- >>> view.initialize()
- >>> view.specifications
- []
+ >>> len(view.attendees)
+ 1
+
+ >>> print view.attendees[0]['name']
+ name12
+
+ >>> print view.attendees[0]['displayname']
+ Sample Person
+
+ >>> print view.attendees[0]['start']
+ 2005-10-08T07:00:00Z
+
+ >>> print view.attendees[0]['end']
+ 2005-11-17T20:00:00Z
+
+If a specification's priority is undefined or marked as not for us, then
+it is not included in the meeting list for the sprint. The javascript
+spec is one such spec. First we will accept it for the sprint:
+
+ >>> print js_spec.priority.name
+ NOTFORUS
+
+ >>> link = js_spec.sprint_links[0]
+ >>> link.sprint == ubz
+ True
+
+ >>> ubz.acceptSpecificationLinks([link.id], mark)
+ 0
+
+Even though the Javascript spec has now been accepted for the sprint
+now, it is not listed by the view because of its priority:
+
+ >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
+ >>> view.initialize()
+ >>> spec_names = [spec['spec'].name for spec in view.specifications]
+ >>> js_spec.name not in spec_names
+ True
+
+If we decline the extension manager spec, it disapears from the list of
+specs:
+
+ >>> link = ext_spec.sprint_links[0]
+ >>> link.sprint == ubz
+ True
+
+ >>> ubz.declineSpecificationLinks([link.id], mark)
+ 0
+
+ >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
+ >>> view.initialize()
+ >>> view.specifications
+ []
+
+
=== modified file 'lib/lp/blueprints/doc/sprint.txt'
--- lib/lp/blueprints/doc/sprint.txt 2010-04-21 16:15:36 +0000
+++ lib/lp/blueprints/doc/sprint.txt 2010-07-29 20:06:01 +0000
@@ -1,20 +1,24 @@
-= Sprints / Meetings =
+Sprints / Meetings
+==================
Sprints or meetings can be coordinated using Launchpad.
>>> from zope.component import getUtility
- >>> from canonical.launchpad.interfaces import ISprintSet, IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> from lp.blueprints.interfaces.sprint import ISprintSet
>>> sprintset = getUtility(ISprintSet)
To find a sprint by name, use:
>>> gentoo = sprintset["gentoo"]
-The major pillars, product, distribution and project, have some properties
-which give us the sprints relevant to them.
-
- >>> from canonical.launchpad.interfaces import (
- ... IProductSet, IProjectGroupSet, IDistributionSet)
+The major pillars, product, distribution and project, have some
+properties which give us the sprints relevant to them.
+
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
+
>>> productset = getUtility(IProductSet)
>>> projectset = getUtility(IProjectGroupSet)
>>> distroset = getUtility(IDistributionSet)
@@ -22,29 +26,33 @@
>>> ubuntu = distroset.getByName('ubuntu')
>>> mozilla = projectset.getByName('mozilla')
-We have coming_sprints, giving us up to 5 relevant events that are
-up-and-coming (sorted by the starting date):
+We have coming_sprints, giving us up to 5 relevant events that are up-
+and-coming (sorted by the starting date):
>>> for sprint in firefox.coming_sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
futurista 2015-08-16
+
>>> for sprint in ubuntu.coming_sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
futurista 2015-08-16
+
>>> for sprint in mozilla.coming_sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
futurista 2015-08-16
-And we have sprints, giving us all sprints relevant to that pillar (sorted
-descending by the starting date):
+And we have sprints, giving us all sprints relevant to that pillar
+(sorted descending by the starting date):
>>> for sprint in firefox.sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
futurista 2015-08-16
ubz 2005-10-07
+
>>> for sprint in ubuntu.sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
futurista 2015-08-16
+
>>> for sprint in mozilla.sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
futurista 2015-08-16
@@ -56,6 +64,7 @@
>>> for sprint in firefox.past_sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
ubz 2005-10-07
+
>>> for sprint in ubuntu.past_sprints:
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
@@ -68,11 +77,12 @@
the specs related to the Ubuntu "futurista" sprint to "proposed", and
then check the coming sprints and all sprints.
- >>> from canonical.launchpad.interfaces import SprintSpecificationStatus
+ >>> from lp.blueprints.interfaces.sprintspecification import (
+ ... SprintSpecificationStatus)
We're directly using the database classes here, bypassing the security
-proxies because this is just set-up for the next step, it's not the exact
-functionality we're testing.
+proxies because this is just set-up for the next step, it's not the
+exact functionality we're testing.
>>> from lp.blueprints.model.sprint import SprintSet
>>> futurista = SprintSet()["futurista"]
@@ -93,70 +103,67 @@
... print sprint.name, sprint.time_starts.strftime('%Y-%m-%d')
-== Specification Listings ==
+Specification Listings
+----------------------
We should be able to get lists of specifications in different states
related to a sprint.
-Basically, we can filter by completeness, and by whether or not the spec is
-informational.
-
- >>> ubz = sprintset["ubz"]
-
- >>> from canonical.launchpad.interfaces import SpecificationFilter
-
+Basically, we can filter by completeness, and by whether or not the spec
+is informational.
+
+ >>> ubz = sprintset["ubz"]
+
+ >>> from lp.blueprints.interfaces.specification import SpecificationFilter
First, there should be no informational specs for ubz:
- >>> filter = [SpecificationFilter.INFORMATIONAL]
- >>> ubz.specifications(filter=filter).count()
- 1
-
+ >>> filter = [SpecificationFilter.INFORMATIONAL]
+ >>> ubz.specifications(filter=filter).count()
+ 1
There are 0 completed specs for UBZ:
- >>> filter = [SpecificationFilter.COMPLETE]
- >>> ubz.specifications(filter=filter).count()
- 0
-
+ >>> filter = [SpecificationFilter.COMPLETE]
+ >>> ubz.specifications(filter=filter).count()
+ 0
And there are three incomplete specs:
- >>> filter = [SpecificationFilter.INCOMPLETE]
- >>> for spec in ubz.specifications(filter=filter):
- ... print spec.name, spec.is_complete
- svg-support False
- extension-manager-upgrades False
- e4x False
-
+ >>> filter = [SpecificationFilter.INCOMPLETE]
+ >>> for spec in ubz.specifications(filter=filter):
+ ... print spec.name, spec.is_complete
+ svg-support False
+ extension-manager-upgrades False
+ e4x False
If we ask for all specs, we get them in the order of priority.
- >>> filter = [SpecificationFilter.ALL]
- >>> for spec in ubz.specifications(filter=filter):
- ... print spec.priority.title, spec.name
- High svg-support
- Medium extension-manager-upgrades
- Not e4x
-
+ >>> filter = [SpecificationFilter.ALL]
+ >>> for spec in ubz.specifications(filter=filter):
+ ... print spec.priority.title, spec.name
+ High svg-support
+ Medium extension-manager-upgrades
+ Not e4x
And if we ask just for specs, we get them all
- >>> for spec in ubz.specifications():
- ... print spec.name, spec.is_complete
- svg-support False
- extension-manager-upgrades False
- e4x False
-
+ >>> for spec in ubz.specifications():
+ ... print spec.name, spec.is_complete
+ svg-support False
+ extension-manager-upgrades False
+ e4x False
Inactive products are excluded from the listings.
- >>> from canonical.launchpad.interfaces import IProductSet
+ >>> from lp.registry.interfaces.product import IProductSet
>>> from canonical.launchpad.ftests import login
+
>>> firefox = getUtility(IProductSet).getByName('firefox')
>>> login("foo.bar@xxxxxxxxxxxxx")
# Unlink the source packages so the project can be deactivated.
+
>>> from lp.testing import unlink_source_packages
>>> unlink_source_packages(firefox)
>>> firefox.active = False
@@ -166,11 +173,12 @@
Reset firefox so we don't mess up later tests.
- >>> firefox.active = True
- >>> flush_database_updates()
-
-
-== Sprint Driver ==
+ >>> firefox.active = True
+ >>> flush_database_updates()
+
+
+Sprint Driver
+-------------
Each sprint had a driver - the person (or team) that can decide on the
list of blueprints for discussion. The driver is stored in the `driver`
@@ -223,10 +231,13 @@
>>> print sprint_attendance.attendee.name
mustard
+
>>> print sprint_attendance.time_starts
2005-10-07 09:00:00+00:00
+
>>> print sprint_attendance.time_ends
2005-10-17 19:05:00+00:00
+
>>> print sprint_attendance.is_physical
True
@@ -235,14 +246,18 @@
>>> new_attendance = ubz.attend(person, time_starts, time_ends, False)
>>> print new_attendance.attendee.name
mustard
+
>>> print new_attendance.time_starts
2005-10-08 09:00:00+00:00
+
>>> print new_attendance.time_ends
2005-10-16 19:05:00+00:00
+
>>> print new_attendance.is_physical
False
-The sprint attendances property returns a list of SprintAttendance objects.
+The sprint attendances property returns a list of SprintAttendance
+objects.
>>> ubz.attendances
[<SprintAttendance ...>]
@@ -250,3 +265,5 @@
>>> for attendance in ubz.attendances:
... print attendance.attendee.name
mustard
+
+
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py 2010-02-19 12:05:10 +0000
+++ lib/lp/blueprints/interfaces/specification.py 2010-07-29 20:06:01 +0000
@@ -22,14 +22,12 @@
'SpecificationImplementationStatus',
'SpecificationLifecycleStatus',
'SpecificationPriority',
- 'SpecificationSort'
+ 'SpecificationSort',
]
from lazr.restful.declarations import (
- REQUEST_USER, call_with, export_as_webservice_entry,
- export_write_operation, operation_parameters, operation_returns_entry)
-from lazr.restful.fields import Reference
+ export_as_webservice_entry)
from zope.interface import Interface, Attribute
from zope.component import getUtility
@@ -40,7 +38,6 @@
ContentNameField, PublicPersonChoice, Summary, Title)
from canonical.launchpad.validators import LaunchpadValidationError
from lp.registry.interfaces.role import IHasOwner
-from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.branchlink import IHasLinkedBranches
from lp.registry.interfaces.mentoringoffer import ICanBeMentored
from canonical.launchpad.interfaces.validation import valid_webref
@@ -125,7 +122,8 @@
GOOD = DBItem(70, """
Good progress
- The feature is considered on track for delivery in the targeted release.
+ The feature is considered on track for delivery in the targeted
+ release.
""")
BETA = DBItem(75, """
@@ -148,8 +146,8 @@
AWAITINGDEPLOYMENT = DBItem(85, """
Deployment
- The implementation has been done, and can be deployed in the production
- environment, but this has not yet been done by the system
+ The implementation has been done, and can be deployed in the
+ production environment, but this has not yet been done by the system
administrators. (This status is typically used for Web services where
code is not released but instead is pushed into production.
""")
@@ -441,8 +439,8 @@
NEW = DBItem(40, """
New
- No thought has yet been given to implementation strategy, dependencies,
- or presentation/UI issues.
+ No thought has yet been given to implementation strategy,
+ dependencies, or presentation/UI issues.
""")
SUPERSEDED = DBItem(60, """
@@ -559,8 +557,7 @@
description=_(
"May contain lower-case letters, numbers, and dashes. "
"It will be used in the specification url. "
- "Examples: mozilla-type-ahead-find, postgres-smart-serial.")
- )
+ "Examples: mozilla-type-ahead-find, postgres-smart-serial."))
title = Title(
title=_('Title'), required=True, description=_(
"Describe the feature as clearly as possible in up to 70 "
@@ -880,7 +877,6 @@
"""Return the SpecificationBranch link for the branch, or None."""
-# Interfaces for containers
class ISpecificationSet(IHasSpecifications):
"""A container for specifications."""
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2009-09-21 14:56:07 +0000
+++ lib/lp/blueprints/model/specification.py 2010-07-29 20:06:01 +0000
@@ -229,7 +229,7 @@
# and make sure there is no leftover distroseries goal
self.productseries = None
else:
- raise AssertionError, 'Inappropriate goal.'
+ raise AssertionError('Inappropriate goal.')
# record who made the proposal, and when
self.goal_proposer = proposer
self.date_goal_proposed = UTC_NOW
@@ -333,7 +333,7 @@
# NB NB NB if you change this definition PLEASE update the db constraint
# Specification.specification_completion_recorded_chk !!!
- completeness_clause = ("""
+ completeness_clause = ("""
Specification.implementation_status = %s OR
Specification.definition_status IN ( %s, %s ) OR
(Specification.implementation_status = %s AND
@@ -372,7 +372,7 @@
# than a threshold" and to comment the dbschema that "anything not
# started should be less than the threshold". We'll see how maintainable
# this is.
- started_clause = """
+ started_clause = """
Specification.implementation_status NOT IN (%s, %s, %s, %s) OR
(Specification.implementation_status = %s AND
Specification.definition_status = %s)
@@ -520,7 +520,7 @@
return
def isSubscribed(self, person):
- """See canonical.launchpad.interfaces.ISpecification."""
+ """See lp.blueprints.interfaces.specification.ISpecification."""
if person is None:
return False
@@ -678,7 +678,7 @@
def latest_completed_specifications(self):
"""See IHasSpecifications."""
return self.specifications(sort=SpecificationSort.DATE, quantity=5,
- filter=[SpecificationFilter.COMPLETE,])
+ filter=[SpecificationFilter.COMPLETE, ])
@property
def specification_count(self):
@@ -790,7 +790,7 @@
# filter based on completion. see the implementation of
# Specification.is_complete() for more details
- completeness = Specification.completeness_clause
+ completeness = Specification.completeness_clause
if SpecificationFilter.COMPLETE in filter:
query += ' AND ( %s ) ' % completeness
@@ -859,7 +859,8 @@
FROM SpecificationDependency, Specification
WHERE SpecificationDependency.specification IN %s
AND SpecificationDependency.dependency = Specification.id
- ORDER BY Specification.priority DESC, Specification.name, Specification.id
+ ORDER BY Specification.priority DESC, Specification.name,
+ Specification.id
""" % sqlvalues(specification_ids)).get_all()
dependencies = {}
@@ -872,5 +873,5 @@
return dependencies
def get(self, spec_id):
- """See canonical.launchpad.interfaces.ISpecificationSet."""
+ """See lp.blueprints.interfaces.specification.ISpecificationSet."""
return Specification.get(spec_id)
=== modified file 'lib/lp/blueprints/stories/sprints/20-sprint-registration.txt'
--- lib/lp/blueprints/stories/sprints/20-sprint-registration.txt 2009-10-30 20:50:27 +0000
+++ lib/lp/blueprints/stories/sprints/20-sprint-registration.txt 2010-07-29 20:06:01 +0000
@@ -1,4 +1,5 @@
-= Sprint Registration =
+Sprint Registration
+===================
It should be possible to register yourself to attend the sprint:
@@ -13,22 +14,23 @@
>>> print browser.title
Register your attendance : Ubuntu Below Zero : Meetings
-Invalid dates, for instance entering a starting date after the ending date,
-are reported as errors to the users. (See also the tests in
+Invalid dates, for instance entering a starting date after the ending
+date, are reported as errors to the users. (See also the tests in
lib/canonical/launchpad/doc/sprintattendance-pages.txt)
-By default, the form will be pre-filled out with arrival and departure dates
-that correspond to the full length of the conference and imply the user will
-be available to participate in any session.
+By default, the form will be pre-filled out with arrival and departure
+dates that correspond to the full length of the conference and imply the
+user will be available to participate in any session.
>>> browser.getControl('From').value
'2006-01-10 08:30'
+
>>> browser.getControl('To').value
'2006-02-12 17:00'
-We accept a starting date up to one day before the sprint starts (which we
-will map to starting at the start of the sprint), and a departure date up to
-one day after the sprint ends.
+We accept a starting date up to one day before the sprint starts (which
+we will map to starting at the start of the sprint), and a departure
+date up to one day after the sprint ends.
>>> browser.getControl('From').value = '2006-01-10 10:30:00'
>>> browser.getControl('To').value = '2005-02-04 20:11:00'
@@ -36,6 +38,7 @@
>>> print browser.url
http://launchpad.dev/sprints/ubz/+attend
+
>>> for tag in find_tags_by_class(browser.contents, 'message'):
... print tag.renderContents()
There is 1 error.
@@ -49,13 +52,13 @@
>>> print browser.url
http://launchpad.dev/sprints/ubz/+attend
+
>>> for tag in find_tags_by_class(browser.contents, 'message'):
... print tag.renderContents()
There are 2 errors.
Please pick a date before 2006-02-12 17:00
Please pick a date before 2006-02-13 17:00
-
Similarly, an attendance that ends before the start of a sprint is an
error:
@@ -65,15 +68,16 @@
>>> print browser.url
http://launchpad.dev/sprints/ubz/+attend
+
>>> for tag in find_tags_by_class(browser.contents, 'message'):
... print tag.renderContents()
There are 2 errors.
Please pick a date after 2006-01-09 08:30
Please pick a date after 2006-01-10 08:30
-With the dates fixed, Sample person can attend the sprint. The user
-is staying an extra week past the end of the sprint, which is fine
-since the date range overlaps that of the sprint.
+With the dates fixed, Sample person can attend the sprint. The user is
+staying an extra week past the end of the sprint, which is fine since
+the date range overlaps that of the sprint.
>>> browser.getControl('From').value = '2006-01-10 10:30:00'
>>> browser.getControl('To').value = '2006-02-12 20:11:00'
@@ -98,6 +102,7 @@
>>> browser.getLink('Register yourself').click()
>>> print browser.getControl('From').value
2006-01-10 10:30
+
>>> print browser.getControl('To').value
2006-02-12 17:00
@@ -113,6 +118,7 @@
>>> browser.getControl('From').value
'2006-01-10 08:30'
+
>>> browser.getControl('To').value
'2006-02-12 17:00'
@@ -132,15 +138,17 @@
>>> browser.getLink('Register someone else').click()
>>> browser.url
'http://launchpad.dev/sprints/ubz/+register'
- >>> browser.getControl('Attendee').value = 'guilherme.salgado@xxxxxxxxxxxxx'
+
+ >>> browser.getControl('Attendee').value = (
+ ... 'guilherme.salgado@xxxxxxxxxxxxx')
>>> browser.getControl('Register').click()
And verifies that Carlos and Salgado are now listed:
- >>> print_attendees(browser.contents)
- Carlos Perelló Marín
- Sample Person
- Guilherme Salgado
+ >>> print_attendees(browser.contents)
+ Carlos Perelló Marín
+ Sample Person
+ Guilherme Salgado
In order to make it easy to organize a meeting, we provide a facility
for exporting the list of attendees in CSV format to registered users,
@@ -149,17 +157,20 @@
First, we add a couple of IRC nicknames for Carlos.
>>> from canonical.launchpad.ftests import login, logout
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> from lp.registry.model.person import IrcID
>>> from zope.component import getUtility
>>> login('carlos@xxxxxxxxxxxxx')
>>> carlos = getUtility(IPersonSet).getByName('carlos')
>>> IrcID(person=carlos, network='freenode', nickname='carlos')
<IrcID at ...>
+
>>> IrcID(person=carlos, network='QuakeNet', nickname='qarlos')
<IrcID at ...>
+
>>> sorted([ircid.nickname for ircid in carlos.ircnicknames])
[u'carlos', u'qarlos']
+
>>> logout()
>>> browser.getLink('Export attendees to CSV').click()
@@ -189,3 +200,5 @@
Traceback (most recent call last):
...
Unauthorized:...
+
+
=== modified file 'lib/lp/blueprints/stories/standalone/subscribing.txt'
--- lib/lp/blueprints/stories/standalone/subscribing.txt 2009-11-11 15:54:20 +0000
+++ lib/lp/blueprints/stories/standalone/subscribing.txt 2010-07-29 20:06:01 +0000
@@ -1,10 +1,12 @@
-
-= Subscribing to Specifications =
-
-== Subscribing oneself ==
-
-Just for fun, let's subscribe to one of the specifications. We'll subscribe
-to the spec about E4X.
+Subscribing to Specifications
+=============================
+
+
+Subscribing oneself
+-------------------
+
+Just for fun, let's subscribe to one of the specifications. We'll
+subscribe to the spec about E4X.
First, let's make sure we can see the link called "Subscribe..."
@@ -24,7 +26,8 @@
Carlos.
>>> browser.addHeader('Authorization', 'Basic carlos@xxxxxxxxxxxxx:test')
- >>> browser.open("http://blueprints.launchpad.dev/firefox/+spec/e4x/+subscribe")
+ >>> browser.open(
+ ... "http://blueprints.launchpad.dev/firefox/+spec/e4x/+subscribe")
>>> print browser.title
Subscribe to blueprint : Support E4X in EcmaScript :
Blueprints : Mozilla Firefox
@@ -35,14 +38,14 @@
>>> back_link.url
'http://blueprints.launchpad.dev/firefox/+spec/e4x'
-
-There should be a control to set whether or not participation in discussions
-of this feature is essential. We will say we want to be essential to this
-feature planning:
+There should be a control to set whether or not participation in
+discussions of this feature is essential. We will say we want to be
+essential to this feature planning:
>>> essential = browser.getControl('essential')
>>> essential.selected
False
+
>>> essential.selected = True
Now, we'll POST the form. We should see a message that we have just
@@ -51,6 +54,7 @@
>>> browser.getControl('Subscribe').click()
>>> 'You have subscribed to this blueprint' in browser.contents
True
+
>>> 'subscriber-essential' in browser.contents
True
@@ -60,8 +64,8 @@
>>> submod_link is not None
True
-OK. Now, let's say we want to change the essential field. Let's follow the
-link to modify the subscription. It should currently be checked.
+OK. Now, let's say we want to change the essential field. Let's follow
+the link to modify the subscription. It should currently be checked.
>>> submod_link.click()
>>> print browser.title
@@ -78,11 +82,12 @@
>>> browser.getControl('Update').click()
>>> 'Your subscription has been updated' in browser.contents
True
+
>>> 'subscriber-inessential' in browser.contents
True
-It's also possible to change the essential flag clicking on the star icon in
-the Subscribers portlet.
+It's also possible to change the essential flag clicking on the star
+icon in the Subscribers portlet.
>>> browser.getLink(url='/+subscription/carlos').click()
>>> browser.getControl('Participation essential').selected = True
@@ -93,22 +98,28 @@
>>> 'subscriber-essential' in browser.contents
True
-We don't really want to be subscribed, so lets unsubscribe from that spec.
-We load the subscription page, and now the button says "Unsubscribe".
+We don't really want to be subscribed, so lets unsubscribe from that
+spec. We load the subscription page, and now the button says
+"Unsubscribe".
>>> browser.getLink('Unsubscribe').click()
>>> unsubit = browser.getControl(name='unsubscribe')
>>> unsubit.value
'Unsubscribe'
+
>>> unsubit.click()
>>> 'You have unsubscribed from this blueprint.' in browser.contents
True
+
>>> 'subscriber-inessential' in browser.contents
False
+
>>> 'subscriber-essential' in browser.contents
False
-== Subscribing other users ==
+
+Subscribing other users
+-----------------------
When we want other users to track a specification we can subscribe them.
@@ -121,21 +132,26 @@
>>> browser.getControl('Subscriber').value = 'stub'
>>> browser.getControl('Continue').click()
-When we subscribe someone else to a blueprint, they get notified by email.
+When we subscribe someone else to a blueprint, they get notified by
+email.
>>> from lp.testing.mail_helpers import pop_notifications
>>> last_email = pop_notifications()[-1]
>>> last_email['To']
'...stuart.bishop@xxxxxxxxxxxxx...'
+
>>> last_email['From']
'...carlos@xxxxxxxxxxxxx...'
+
>>> last_email['Subject']
'...[Blueprint e4x]...'
+
>>> last_email.get_payload()
'...You are now subscribed to the blueprint e4x...'
-To change the same user's subscription to 'Participation essential' we simply
-go through the process again, this time ticking the relevant checkbox.
+To change the same user's subscription to 'Participation essential' we
+simply go through the process again, this time ticking the relevant
+checkbox.
>>> browser.open("http://blueprints.launchpad.dev/firefox/+spec/e4x")
>>> browser.getLink('Subscribe someone else').click()
@@ -150,19 +166,21 @@
>>> '[Participation essential]' in last_email.get_payload()
True
-== Subscribing teams ==
+
+Subscribing teams
+-----------------
Users can subscribe any team to any spec. If the subscribed team has a
-contact email address, a notification is sent to that address, but if the team
-has no contact address we'll send one notification for each active member of
-the team.
+contact email address, a notification is sent to that address, but if
+the team has no contact address we'll send one notification for each
+active member of the team.
-The Launchpad Admins team has no contact address, so subscribing it to
-a spec will cause email notifications to be sent to each of its members.
+The Launchpad Admins team has no contact address, so subscribing it to a
+spec will cause email notifications to be sent to each of its members.
>>> from zope.component import getUtility
>>> from canonical.launchpad.helpers import get_contact_email_addresses
- >>> from canonical.launchpad.interfaces import IPersonSet
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> login('foo.bar@xxxxxxxxxxxxx')
>>> person_set = getUtility(IPersonSet)
>>> admins = person_set.getByName('admins')
@@ -173,15 +191,17 @@
... get_contact_email_addresses(ubuntu_team))
>>> logout()
- >>> browser.open("http://blueprints.launchpad.dev/kubuntu/+spec/krunch-desktop-plan")
+ >>> browser.open(
+ ... "http://blueprints.launchpad.dev/"
+ ... "kubuntu/+spec/krunch-desktop-plan")
>>> browser.getLink('Subscribe someone else').click()
>>> browser.getControl('Subscriber').value = 'admins'
>>> browser.getControl(name='field.essential').value = None
>>> browser.getControl('Continue').click()
-We created a subscription for the Launchpad Admins, but because the
-team does not have a preferred email address, an email is sent to
-each active member who has a preferred email registered.
+We created a subscription for the Launchpad Admins, but because the team
+does not have a preferred email address, an email is sent to each active
+member who has a preferred email registered.
>>> print admins.preferredemail
None
@@ -195,8 +215,8 @@
>>> browser.getControl(name='field.essential').value = 'yes'
>>> browser.getControl('Continue').click()
-We modified the Launchpad Admins team's subscription and again,
-an email is sent to each active member.
+We modified the Launchpad Admins team's subscription and again, an email
+is sent to each active member.
>>> admins_contact_email_addresses == sorted(
... [message['To'] for message in pop_notifications()])
@@ -219,6 +239,7 @@
>>> ([message['To'] for message in pop_notifications()] ==
... [str(ubuntu_team.preferredemail.email)])
True
+
>>> logout()
>>> browser.getLink('Subscribe someone else').click()
@@ -226,17 +247,19 @@
>>> browser.getControl(name='field.essential').value = 'yes'
>>> browser.getControl('Continue').click()
-We modified the Ubuntu Team's subscription and again,
-an email is sent to the team's preferred email address.
+We modified the Ubuntu Team's subscription and again, an email is sent
+to the team's preferred email address.
>>> login(ANONYMOUS)
>>> ([message['To'] for message in pop_notifications()] ==
... [str(ubuntu_team.preferredemail.email)])
True
+
>>> logout()
-== Viewing the subscribers ==
+Viewing the subscribers
+-----------------------
The subcribers portlet lists each subscriber with the appropriate icon
representing whether the person is essential to the specification or
@@ -255,3 +278,5 @@
/@@/subscriber-essential Andrew Bennetts
/@@/subscriber-inessential Stuart Bishop
/@@/subscriber-inessential Dafydd Harries
+
+