launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03612
[Merge] lp:~sinzui/launchpad/answers-api-2 into lp:launchpad
Curtis Hovey has proposed merging lp:~sinzui/launchpad/answers-api-2 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~sinzui/launchpad/answers-api-2/+merge/60999
Export IQuestionCollection methods and IQuestionMessage.
Launchpad bug: https://bugs.launchpad.net/bugs/782093
Pre-implementation: no one
Export the methods need to search for a question target's questions.
Export IQuestionMessage and update IQuestion to make messages accessible.
--------------------------------------------------------------------
RULES
* Export searchQuestions() and findSimilarQuestions().
* Export the IQuestion dates, people, and messages fields.
* Provide IQuestionMessage a URL.
* Export IQuestionMessage
QA
* This script will exercise the items exported:
from launchpadlib.launchpad import Launchpad
lp = Launchpad.login_with(
'test', 'https://api.qastaging.launchpad.dev/', version='devel')
project = lp.projects['launchpad']
indent = " "
print '! Testing findSimilarQuestions'
for question in project.findSimilarQuestions(phrase='import'):
print indent, question.title
print indent, question.owner.name
print indent, question.date_created
print '! Testing searchQuestions'
language = lp.load('+languages/en')
questions = project.searchQuestions(
status=['Open', 'Needs information'], language=language,
sort='oldest first')
for question in questions:
print indent, question.title
print '! Testing question.messages'
question = questions[-1]
for message in question.messages:
print indent, message.index, message.action, message.new_status
print indent, message.content[0:40]
LINT
lib/canonical/launchpad/security.py
lib/lp/answers/browser/configure.zcml
lib/lp/answers/interfaces/question.py
lib/lp/answers/interfaces/questioncollection.py
lib/lp/answers/interfaces/questionmessage.py
lib/lp/answers/interfaces/questiontarget.py
lib/lp/answers/interfaces/webservice.py
lib/lp/answers/model/question.py
lib/lp/answers/stories/webservice.txt
TEST
./bin/test -vv -t answers.*/webservice.txt
IMPLEMENTATION
Allow anonymous users to see questions and messages over the API.
lib/canonical/launchpad/security.py
Defined a URL based on BugMessages so the IQuestionEntries has a self_link.
Exported IQuestionMessage
lib/lp/answers/browser/configure.zcml
lib/lp/answers/interfaces/questionmessage.py
Exported two methods to search for questions. Rewrote the documentation for
apidocs. Instead of renaming the 'title' arg for findSimilarQuestions in the
export command, I renamed it to 'phrase' because that is what we mean in
the core app.
lib/lp/answers/interfaces/questioncollection.py
lib/lp/answers/interfaces/questiontarget.py
lib/lp/answers/model/question.py
Exported the question properties for dates, people, and messages. Most of
the exported attributes are made read-only because only workflow methods
should change them. I did not use doNotSnapshot for IQuestion.message because
the are in fact used by the snapshot, we need to change the event object to
factor out messages from snapshots.
lib/lp/answers/interfaces/question.py
Patched the correct return types of the exported methods and collections.
lib/lp/answers/interfaces/webservice.py
--
https://code.launchpad.net/~sinzui/launchpad/answers-api-2/+merge/60999
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/answers-api-2 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2011-05-03 04:39:43 +0000
+++ lib/canonical/launchpad/security.py 2011-05-14 17:43:39 +0000
@@ -35,6 +35,7 @@
from lp.answers.interfaces.faq import IFAQ
from lp.answers.interfaces.faqtarget import IFAQTarget
from lp.answers.interfaces.question import IQuestion
+from lp.answers.interfaces.questionmessage import IQuestionMessage
from lp.answers.interfaces.questionsperson import IQuestionsPerson
from lp.answers.interfaces.questiontarget import IQuestionTarget
from lp.app.interfaces.security import IAuthorization
@@ -223,6 +224,7 @@
"""Any authenticated user can see this object."""
return True
+
class AdminByAdminsTeam(AuthorizationBase):
permission = 'launchpad.Admin'
usedfor = Interface
@@ -1685,6 +1687,14 @@
return user.inTeam(self.obj.owner)
+class ViewQuestion(AnonymousAuthorization):
+ usedfor = IQuestion
+
+
+class ViewQuestionMessage(AnonymousAuthorization):
+ usedfor = IQuestionMessage
+
+
class AppendFAQTarget(EditByOwnersOrAdmins):
permission = 'launchpad.Append'
usedfor = IFAQTarget
=== modified file 'lib/lp/answers/browser/configure.zcml'
--- lib/lp/answers/browser/configure.zcml 2010-10-03 15:30:06 +0000
+++ lib/lp/answers/browser/configure.zcml 2011-05-14 17:43:39 +0000
@@ -243,6 +243,13 @@
template="../../app/templates/generic-edit.pt"
/>
+ <browser:url
+ for="lp.answers.interfaces.question.IQuestionMessage"
+ path_expression="string:+message/${index}"
+ attribute_to_parent="question"
+ rootsite="answers"
+ />
+
<browser:page
name="+display"
for="lp.answers.interfaces.questionmessage.IQuestionMessage"
=== modified file 'lib/lp/answers/interfaces/question.py'
--- lib/lp/answers/interfaces/question.py 2011-04-27 13:59:57 +0000
+++ lib/lp/answers/interfaces/question.py 2011-05-14 17:43:39 +0000
@@ -25,6 +25,12 @@
operation_parameters,
REQUEST_USER,
)
+from lazr.restful.fields import (
+ CollectionField,
+ Reference,
+ ReferenceChoice,
+ )
+
from zope.interface import (
Attribute,
Interface,
@@ -34,7 +40,6 @@
Choice,
Datetime,
Int,
- List,
Object,
Text,
TextLine,
@@ -50,6 +55,7 @@
from lp.answers.interfaces.questiontarget import IQuestionTarget
from lp.registry.interfaces.role import IHasOwner
from lp.services.fields import PublicPersonChoice
+from lp.services.worlddata.interfaces.language import ILanguage
class InvalidQuestionStateError(Exception):
@@ -73,11 +79,12 @@
title=_('Summary'), required=True, description=_(
"A one-line summary of the issue or problem.")),
as_of="devel")
- description = Text(
+ description = exported(Text(
title=_('Description'), required=True, description=_(
"Include as much detail as possible: what "
u"you\N{right single quotation mark}re trying to achieve, what steps "
- "you take, what happens, and what you think should happen instead."))
+ "you take, what happens, and what you think should happen instead.")),
+ as_of="devel")
status = exported(Choice(
title=_('Status'), vocabulary=QuestionStatus,
default=QuestionStatus.OPEN, readonly=True),
@@ -87,9 +94,10 @@
default=QuestionPriority.NORMAL)
# XXX flacoste 2006-10-28: It should be more precise to define a new
# vocabulary that excludes the English variants.
- language = Choice(
- title=_('Language'), vocabulary='Language',
- description=_('The language in which this question is written.'))
+ language = exported(ReferenceChoice(
+ title=_('Language'), vocabulary='Language', schema=ILanguage,
+ description=_('The language in which this question is written.')),
+ as_of="devel")
owner = exported(PublicPersonChoice(
title=_('Owner'), required=True, readonly=True,
vocabulary='ValidPersonOrTeam'),
@@ -107,31 +115,37 @@
vocabulary='ValidPersonOrTeam'),
as_of="devel",
readonly=True)
- answer = Object(
+ answer = exported(Reference(
title=_('Answer'), required=False,
description=_("The IQuestionMessage that contains the answer "
"confirmed by the owner as providing a solution to his problem."),
- schema=IQuestionMessage)
- datecreated = Datetime(
- title=_('Date Created'), required=True, readonly=True)
- datedue = Datetime(
+ schema=IQuestionMessage),
+ readonly=True, as_of="devel")
+ datecreated = exported(Datetime(
+ title=_('Date Created'), required=True, readonly=True),
+ exported_as='date_created', readonly=True, as_of="devel")
+ datedue = exported(Datetime(
title=_('Date Due'), required=False, default=None,
description=_(
- "The date by which we should have resolved this question."))
- datelastquery = Datetime(
+ "The date by which we should have resolved this question.")),
+ exported_as='date_due', readonly=True, as_of="devel")
+ datelastquery = exported(Datetime(
title=_("Date Last Queried"), required=True,
description=_("The date on which we last heard from the "
- "customer (owner)."))
- datelastresponse = Datetime(
+ "customer (owner).")),
+ exported_as='date_last_query', readonly=True, as_of="devel")
+ datelastresponse = exported(Datetime(
title=_("Date last Responded"),
required=False,
description=_("The date on which we last communicated "
"with the customer. The combination of datelastquery and "
- "datelastresponse tells us in whose court the ball is."))
- date_solved = Datetime(title=_("Date Answered"), required=False,
+ "datelastresponse tells us in whose court the ball is.")),
+ exported_as='date_last_response', readonly=True, as_of="devel")
+ date_solved = exported(Datetime(title=_("Date Answered"), required=False,
description=_(
"The date on which the question owner confirmed that the "
- "question is Solved."))
+ "question is Solved.")),
+ exported_as='date_solved', readonly=True, as_of="devel")
product = Choice(
title=_('Upstream Project'), required=False,
vocabulary='Product',
@@ -150,10 +164,11 @@
title=_('Status Whiteboard'), required=False,
description=_('Up-to-date notes on the status of the question.'))
# other attributes
- target = Object(title=_('Project'), required=True, schema=IQuestionTarget,
+ target = exported(Reference(
+ title=_('Project'), required=True, schema=IQuestionTarget,
description=_('The distribution, source package, or product the '
- 'question pertains to.'))
-
+ 'question pertains to.')),
+ as_of="devel")
faq = Object(
title=_('Linked FAQ'),
description=_('The FAQ document containing the long answer to this '
@@ -165,13 +180,14 @@
'The set of subscriptions to this question.')
reopenings = Attribute(
"Records of times when this question was reopened.")
- messages = List(
+ messages = exported(CollectionField(
title=_("Messages"),
description=_(
"The list of messages that were exchanged as part of this "
"question , sorted from first to last."),
- value_type=Object(schema=IQuestionMessage),
- required=True, default=[], readonly=True)
+ value_type=Reference(schema=IQuestionMessage),
+ required=True, default=[], readonly=True),
+ as_of='devel')
# Workflow methods
def setStatus(user, new_status, comment, datecreated=None):
=== modified file 'lib/lp/answers/interfaces/questioncollection.py'
--- lib/lp/answers/interfaces/questioncollection.py 2011-05-10 07:18:36 +0000
+++ lib/lp/answers/interfaces/questioncollection.py 2011-05-14 17:43:39 +0000
@@ -17,7 +17,12 @@
Attribute,
Interface,
)
-from zope.schema import Int
+from zope.schema import (
+ Choice,
+ Int,
+ List,
+ TextLine,
+ )
from lazr.restful.declarations import (
collection_default_content,
@@ -26,11 +31,18 @@
export_read_operation,
operation_for_version,
operation_parameters,
+ operation_returns_collection_of,
operation_returns_entry,
)
+from lazr.restful.fields import ReferenceChoice
from canonical.launchpad import _
-from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH
+from lp.answers.enums import (
+ QUESTION_STATUS_DEFAULT_SEARCH,
+ QuestionSort,
+ QuestionStatus,
+ )
+from lp.services.fields import PublicPersonChoice
class IQuestionCollection(Interface):
@@ -42,20 +54,20 @@
language=None, sort=None):
"""Return the questions from the collection matching search criteria.
- :search_text: A string that is matched against the question
- title and description. If None, the search_text is not included as
- a filter criteria.
-
- :status: A sequence of QuestionStatus Items. If None or an empty
- sequence, the status is not included as a filter criteria.
-
- :language: An ILanguage or a sequence of ILanguage objects to match
- against the question's language. If None or an empty sequence,
- the language is not included as a filter criteria.
-
- :sort: An attribute of QuestionSort. If None, a default value is used.
- When there is a search_text value, the default is to sort by
- RELEVANCY, otherwise results are sorted NEWEST_FIRST.
+ :param search_text: A string that is matched against the question
+ title and description. If None, the search_text is not included as
+ a filter criteria.
+
+ :param status: A sequence of QuestionStatus Items. If None or an empty
+ sequence, the status is not included as a filter criteria.
+
+ :param language: An ILanguage or a sequence of ILanguage objects to
+ match against the question's language. If None or an empty
+ sequence, the language is not included as a filter criteria.
+
+ :param sort: An attribute of QuestionSort. If None, a default value is
+ used. When there is a search_text value, the default is to sort by
+ RELEVANCY, otherwise results are sorted NEWEST_FIRST.
"""
def getQuestionLanguages():
@@ -66,22 +78,51 @@
class ISearchableByQuestionOwner(IQuestionCollection):
"""Collection that support searching by question owner."""
+ @operation_parameters(
+ search_text=TextLine(
+ title=_('Search text'), required=False),
+ status=List(
+ title=_('Status'), required=False,
+ value_type=Choice(vocabulary=QuestionStatus)),
+ language=List(
+ title=_('Language'), required=False,
+ value_type=ReferenceChoice(vocabulary='Language')),
+ owner=PublicPersonChoice(
+ title=_('Owner'), required=False,
+ vocabulary='ValidPerson'),
+ needs_attention_from=PublicPersonChoice(
+ title=_('Needs attentions from'), required=False,
+ vocabulary='ValidPerson'),
+ sort=Choice(
+ title=_('Sort'), required=False,
+ vocabulary=QuestionSort))
+ @operation_returns_collection_of(Interface) # IQuestion.
+ @export_read_operation()
+ @operation_for_version('devel')
def searchQuestions(search_text=None,
- status=QUESTION_STATUS_DEFAULT_SEARCH,
+ status=list(QUESTION_STATUS_DEFAULT_SEARCH),
language=None, sort=None, owner=None,
needs_attention_from=None):
"""Return the questions from the collection matching search criteria.
- See `IQuestionCollection` for the description of the standard search
- parameters.
-
- :owner: The IPerson that created the question.
-
- :needs_attention_from: Selects questions that nee attention from an
- IPerson. These are the questions in the NEEDSINFO or ANSWERED state
- owned by the person. The questions not owned by the person but on
- which the person requested more information or gave an answer
- and that are back in the OPEN state are also included.
+ :param search_text: A string that is matched against the question
+ title and description. If None, the search_text is not included as
+ a filter criteria.
+ :param status: A sequence of QuestionStatus Items. If None or an empty
+ sequence, the status is not included as a filter criteria. The
+ default is to match all status except Expired and Invalid.
+ :param language: An ILanguage or a sequence of ILanguage objects to
+ match against the question's language. If None or an empty
+ sequence, the language is not included as a filter criteria.
+ :param owner: The IPerson that created the question.
+ :param needs_attention_from: Selects questions that need attention
+ from an IPerson. These are the questions in the NEEDSINFO or
+ ANSWERED state owned by the person. The questions not owned by the
+ person but on which the person requested more information or gave
+ an answer and that are back in the OPEN state are also included.
+ :param sort: An attribute of QuestionSort. If None, a default value is
+ used. When there is a search_text value, the default is to sort by
+ RELEVANCY, otherwise results are sorted NEWEST_FIRST.
"""
=== modified file 'lib/lp/answers/interfaces/questionmessage.py'
--- lib/lp/answers/interfaces/questionmessage.py 2011-04-27 16:42:38 +0000
+++ lib/lp/answers/interfaces/questionmessage.py 2011-05-14 17:43:39 +0000
@@ -11,13 +11,19 @@
'IQuestionMessage',
]
+from zope.interface import Interface
from zope.schema import (
Bool,
Choice,
- Field,
Int,
)
+from lazr.restful.declarations import (
+ export_as_webservice_entry,
+ exported,
+ )
+from lazr.restful.fields import Reference
+
from canonical.launchpad import _
from canonical.launchpad.interfaces.message import IMessage
from lp.answers.enums import (
@@ -31,30 +37,35 @@
It adds attributes to the IMessage interface.
"""
+ export_as_webservice_entry(as_of='devel')
+
# This is really an Object field with schema=IQuestion, but that
# would create a circular dependency between IQuestion
# and IQuestionMessage
- question = Field(
- title=_("The question related to this message."),
- description=_("An IQuestion object."), required=True, readonly=True)
-
- action = Choice(
+ question = exported(Reference(
+ title=_("The question related to this message."), schema=Interface,
+ description=_("An IQuestion object."), required=True, readonly=True),
+ as_of="devel")
+ action = exported(Choice(
title=_("Action operated on the question by this message."),
required=True, readonly=True, default=QuestionAction.COMMENT,
- vocabulary=QuestionAction)
-
- new_status = Choice(
+ vocabulary=QuestionAction),
+ as_of="devel")
+ new_status = exported(Choice(
title=_("Question status after message"),
description=_("The status of the question after the transition "
"related the action operated by this message."), required=True,
readonly=True, default=QuestionStatus.OPEN,
- vocabulary=QuestionStatus)
- index = Int(
+ vocabulary=QuestionStatus),
+ as_of="devel")
+ index = exported(Int(
title=_("Message index."),
description=_("The messages index in the question's list of "
"messages."),
- readonly=True)
- visible = Bool(
+ readonly=True),
+ as_of="devel")
+ visible = exported(Bool(
title=_("Message visibility."),
description=_("Whether or not the message is visible."),
- readonly=True)
+ readonly=True),
+ as_of="devel")
=== modified file 'lib/lp/answers/interfaces/questiontarget.py'
--- lib/lp/answers/interfaces/questiontarget.py 2011-05-12 04:18:32 +0000
+++ lib/lp/answers/interfaces/questiontarget.py 2011-05-14 17:43:39 +0000
@@ -63,14 +63,19 @@
If there is no such question number for this target, return None
"""
- def findSimilarQuestions(title):
- """Return questions similar to title.
+ @operation_parameters(
+ phrase=TextLine(title=_('A phrase'), required=True))
+ @operation_returns_collection_of(Interface)
+ @export_read_operation()
+ @operation_for_version('devel')
+ def findSimilarQuestions(phrase):
+ """Return questions similar to phrase.
- Return a list of question similar to the title provided. These
- questions should be found using a fuzzy search. The list should be
+ Return a list of question similar to the provided phrase. These
+ questions will be found using a fuzzy search. The list is
ordered from the most similar question to the least similar question.
- :title: A phrase
+ :param phrase: A phrase such as the summary of a question.
"""
@operation_parameters(
=== modified file 'lib/lp/answers/interfaces/webservice.py'
--- lib/lp/answers/interfaces/webservice.py 2011-05-10 11:14:05 +0000
+++ lib/lp/answers/interfaces/webservice.py 2011-05-14 17:43:39 +0000
@@ -16,12 +16,25 @@
from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
-from canonical.launchpad.components.apihelpers import patch_entry_return_type
+from canonical.launchpad.components.apihelpers import (
+ patch_collection_return_type,
+ patch_entry_return_type,
+ patch_reference_property,
+ )
from lp.answers.interfaces.question import IQuestion
-from lp.answers.interfaces.questioncollection import IQuestionSet
+from lp.answers.interfaces.questioncollection import (
+ IQuestionSet,
+ ISearchableByQuestionOwner,
+ )
+from lp.answers.interfaces.questionmessage import IQuestionMessage
from lp.answers.interfaces.questiontarget import IQuestionTarget
IQuestionSet.queryTaggedValue(
LAZR_WEBSERVICE_EXPORTED)['collection_entry_schema'] = IQuestion
patch_entry_return_type(IQuestionSet, 'get', IQuestion)
+patch_collection_return_type(
+ IQuestionTarget, 'findSimilarQuestions', IQuestion)
+patch_collection_return_type(
+ ISearchableByQuestionOwner, 'searchQuestions', IQuestion)
+patch_reference_property(IQuestionMessage, 'question', IQuestion)
=== modified file 'lib/lp/answers/model/question.py'
--- lib/lp/answers/model/question.py 2011-05-13 04:12:27 +0000
+++ lib/lp/answers/model/question.py 2011-05-14 17:43:39 +0000
@@ -1227,10 +1227,10 @@
return False
return True
- def findSimilarQuestions(self, title):
+ def findSimilarQuestions(self, phrase):
"""See `IQuestionTarget`."""
return SimilarQuestionsSearch(
- title, **self.getTargetTypes()).getResults()
+ phrase, **self.getTargetTypes()).getResults()
def getQuestionLanguages(self):
"""See `IQuestionTarget`."""
=== modified file 'lib/lp/answers/stories/webservice.txt'
--- lib/lp/answers/stories/webservice.txt 2011-05-06 18:26:58 +0000
+++ lib/lp/answers/stories/webservice.txt 2011-05-14 17:43:39 +0000
@@ -23,11 +23,12 @@
>>> _team_project = factory.makeProduct(name='team-project', owner=_team)
>>> _asker = factory.makePerson(name='asker')
>>> _question_1 = factory.makeQuestion(
- ... target=_project, title="Q 1", owner=_asker)
+ ... target=_project, title="Q 1 great", owner=_asker)
>>> _question_2 = factory.makeQuestion(
- ... target=_project, title="Q 2", owner=_asker)
+ ... target=_project, title="Q 2 greater", owner=_asker)
>>> _question_3 = factory.makeQuestion(
- ... target=_team_project, title="Q 3", owner=_asker)
+ ... target=_team_project, title="Q 3 greatest", owner=_asker)
+ >>> _message = _question_1.giveAnswer(_contact, 'This is the answer')
>>> logout()
>>> contact_webservice = webservice_for_person(
@@ -37,10 +38,10 @@
Answer contacts
---------------
-Users can add or remove themselves as an answer contact for a project. The
-user must have a preferred language. Scripts should call the
-canUserAlterAnswerContact method first to verify that the person can
-be added.
+Users can add or remove themselves as an answer contact for a project using
+addAnswerContact and removeAnswerContact. The user must have a preferred
+language. Scripts should call the canUserAlterAnswerContact method first to
+verify that the person can changed.
>>> project = contact_webservice.get(
... '/my-project', api_version='devel').jsonBody()
@@ -61,8 +62,8 @@
... person=contact['self_link'], api_version='devel').jsonBody()
True
-User can also make the teams they administer answer contacts if the team has a
-preferred language.
+Users can also make the teams they administer answer contacts using
+addAnswerContact and removeAnswerContact if the team has a preferred language.
>>> team = contact_webservice.get(
... '/~my-team', api_version='devel').jsonBody()
@@ -81,7 +82,7 @@
Anyone can get the collection of languages spoken by at least one
-answer contact.
+answer contact by calling getSupportedLanguages.
>>> languages = anon_webservice.named_get(
... project['self_link'], 'getSupportedLanguages',
@@ -92,7 +93,8 @@
>>> english = languages['entries'][0]
-Anyone can retrieve the collection of answer contacts for a language.
+Anyone can retrieve the collection of answer contacts for a language using
+getAnswerContactsForLanguage.
>>> contacts = anon_webservice.named_get(
... project['self_link'], 'getAnswerContactsForLanguage',
@@ -101,13 +103,105 @@
http://.../~contact
-Questions
----------
-
-Anyone can retrieve a question from a `IQuestionTarget`.
+Question collections
+--------------------
+
+Anyone can retrieve a collection of questions from an `IQuestionTarget` with
+searchQuestions. The question will that match the precise search criteria
+called with searchQuestions.
+
+ >>> questions = anon_webservice.named_get(
+ ... project['self_link'], 'searchQuestions',
+ ... search_text='q great',
+ ... status=['Open', 'Needs information', 'Answered'],
+ ... language=[english['self_link']],
+ ... sort='oldest first',
+ ... api_version='devel').jsonBody()
+ >>> for question in questions['entries']:
+ ... print question['title']
+ Q 1 great
+
+ >>> print questions['total_size']
+ 1
+
+Anyone can retrieve a collection of questions from an `IQuestionTarget` that
+are similar to a phrase using findSimilarQuestions. A phrase one or more the
+words that might appear in a question's title or description.
+findSimilarQuestions uses natural language techniques to match the question.
+
+ >>> questions = anon_webservice.named_get(
+ ... project['self_link'], 'findSimilarQuestions',
+ ... phrase='q great',
+ ... api_version='devel').jsonBody()
+ >>> for question in questions['entries']:
+ ... print question['title']
+ Q 1 great
+ Q 2 greater
+
+Anyone can retrieve a specific question from an `IQuestionTarget` calling
+getQuestion with the question Id.
>>> question_1 = anon_webservice.named_get(
... project['self_link'], 'getQuestion', question_id=_question_1.id,
... api_version='devel').jsonBody()
>>> print question_1['title']
- Q 1
+ Q 1 great
+
+
+A question
+----------
+
+A question has many exported attributes about the details of the question, its
+state, the people involved, and the dates of important events. There is also
+a link to retrieve the question's messages.
+
+ >>> from lazr.restful.testing.webservice import pprint_entry
+ >>> pprint_entry(question_1)
+ answer_link: None
+ answerer_link: None
+ assignee_link: None
+ date_created: u'20...+00:00'
+ date_due: None
+ date_last_query: u'20...+00:00'
+ date_last_response: u'20...+00:00'
+ date_solved: None
+ description: u'description'
+ id: ...
+ language_link: u'http://api.launchpad.dev/devel/+languages/en'
+ messages_collection_link:
+ u'http://api.launchpad.dev/devel/my-project/+question/.../messages'
+ owner_link: u'http://api.launchpad.dev/devel/~asker'
+ resource_type_link: u'http://api.launchpad.dev/devel/#question'
+ self_link: u'http://api.launchpad.dev/devel/my-project/+question/...'
+ status: u'Answered'
+ target_link: u'http://api.launchpad.dev/devel/my-project'
+ title: u'Q 1 great'
+ web_link: u'http://answers.launchpad.dev/my-project/+question/...'
+
+
+Question messages
+-----------------
+
+An `IQuestionMessage` provides the IMessage fields and additional fields
+that indicate how the message changed the question.
+
+ >>> messages = anon_webservice.get(
+ ... question_1['messages_collection_link'],
+ ... api_version='devel').jsonBody()
+ >>> pprint_entry(messages['entries'][0])
+ action: u'Answer'
+ bug_attachments_collection_link: u'...'
+ content: u'This is the answer'
+ date_created: u'20...+00:00'
+ index: 1
+ new_status: u'Answered'
+ owner_link: u'http://api.launchpad.dev/devel/~contact'
+ parent_link: None
+ question_link: u'http://api.launchpad.dev/devel/my-project/+question/...'
+ resource_type_link: u'http://api.launchpad.dev/devel/#question_message'
+ self_link:
+ u'http://api.launchpad.dev/devel/my-project/+question/.../+message/1'
+ subject: u'Re: Q 1 great'
+ visible: True
+ web_link:
+ u'http://answers.launchpad.dev/my-project/+question/.../+message/1'
Follow ups