← Back to team overview

launchpad-reviewers team mailing list archive

[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