← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/answers-api-3 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/answers-api-3 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #784398 in Launchpad itself: "Export IQuestionsPerson to the API"
  https://bugs.launchpad.net/launchpad/+bug/784398

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/answers-api-3/+merge/61346

Export IQuestionsPerson to the API.

    Launchpad bug: https://bugs.launchpad.net/bugs/784398
    Pre-implementation: no one, well, flacoste about removing the adapter.

I want to call getDirectAnswerQuestionTargets and getTeamAnswerQuestionTargets
to get all the question targets I am an answer contact for. I want to call
me.searchQuestions() to search for questions across all question targets that
related to me.

The IQuestionsPerson is an adapter object, which is not supported by
lazr.restful. IPerson must extend IQuestionsPerson, and the adapter object
changed to a mixin.

--------------------------------------------------------------------

RULES

    * Make IPerson extend IQuestionsPerson.
    * Change QuestionsPerson into a mixin and add it to Person.
    * Export IQuestionsPerson methods.


QA

    * Run this script:
    from launchpadlib.launchpad import Launchpad
    lp = Launchpad.login_with(
        'test', 'https://api.qastaging.launchpad.net/', version='devel')
    indent = "    "
    sinzui = lp.people['sinzui']
    print '! Testing getDirectAnswerQuestionTargets'
    for target in sinzui.getDirectAnswerQuestionTargets():
        print indent, target.name
    print '! Testing getTeamAnswerQuestionTargets'
    for target in sinzui.getTeamAnswerQuestionTargets():
        print indent, target.name
    print '! Testing searchQuestions'
    questions = sinzui.searchQuestions(
        status=['Open', 'Needs information'], sort='oldest first')
    for question in questions:
        print indent,  question.title


LINT

    lib/lp/answers/configure.zcml
    lib/lp/answers/interfaces/questionsperson.py
    lib/lp/answers/interfaces/webservice.py
    lib/lp/answers/model/questionsperson.py
    lib/lp/answers/stories/webservice.txt
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/model/person.py


TEST

    ./bin/test -vv \
        -t answers.*stories/webservice-t answers.*doc/person.txt \
        -t answer-contact-report -t question-browse-and-search


IMPLEMENTATION

Updated IPerson to extend IQuestionsPerson. Changed QuestionsPerson from
an adapter to a mixin.
    lib/lp/answers/configure.zcml
    lib/lp/answers/interfaces/questionsperson.py
    lib/lp/answers/model/questionsperson.py
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/model/person.py

Exported the three IQuestionsPerson methods to the API.
    lib/lp/answers/interfaces/webservice.py
    lib/lp/answers/stories/webservice.txt
-- 
https://code.launchpad.net/~sinzui/launchpad/answers-api-3/+merge/61346
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/answers-api-3 into lp:launchpad.
=== modified file 'lib/lp/answers/configure.zcml'
--- lib/lp/answers/configure.zcml	2011-05-13 02:52:50 +0000
+++ lib/lp/answers/configure.zcml	2011-05-18 05:07:27 +0000
@@ -139,16 +139,6 @@
     <allow interface=".interfaces.questionmessage.IQuestionMessage"/>
   </class>
 
-  <class class=".model.questionsperson.QuestionsPerson">
-    <allow interface=".interfaces.questionsperson.IQuestionsPerson"/>
-  </class>
-
-  <adapter
-    for="lp.registry.interfaces.person.IPerson"
-    provides=".interfaces.questionsperson.IQuestionsPerson"
-    factory=".model.questionsperson.QuestionsPerson"
-    trusted="yes"
-    />
   <adapter
     for=".interfaces.question.IQuestion"
     provides=".interfaces.questiontarget.IQuestionTarget"

=== modified file 'lib/lp/answers/interfaces/questionsperson.py'
--- lib/lp/answers/interfaces/questionsperson.py	2011-05-06 04:00:41 +0000
+++ lib/lp/answers/interfaces/questionsperson.py	2011-05-18 05:07:27 +0000
@@ -8,13 +8,37 @@
     'IQuestionsPerson',
     ]
 
-
-from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH
+from zope.interface import Interface
+from zope.schema import (
+    Bool,
+    Choice,
+    List,
+    TextLine,
+    )
+
+from lazr.restful.declarations import (
+    export_read_operation,
+    operation_for_version,
+    operation_parameters,
+    operation_returns_collection_of,
+    )
+from lazr.restful.fields import ReferenceChoice
+
+from canonical.launchpad import _
+from lp.answers.enums import (
+    QuestionParticipation,
+    QuestionSort,
+    QuestionStatus,
+    QUESTION_STATUS_DEFAULT_SEARCH,
+    )
 from lp.answers.interfaces.questioncollection import IQuestionCollection
 
 
 class IQuestionsPerson(IQuestionCollection):
 
+    @operation_returns_collection_of(Interface)  # IQuestionTarger.
+    @export_read_operation()
+    @operation_for_version('devel')
     def getDirectAnswerQuestionTargets():
         """Return a list of IQuestionTargets that a person is subscribed to.
 
@@ -22,6 +46,9 @@
         answer contact because he subscribed himself.
         """
 
+    @operation_returns_collection_of(Interface)  # IQuestionsTarget
+    @export_read_operation()
+    @operation_for_version('devel')
     def getTeamAnswerQuestionTargets():
         """Return a list of IQuestionTargets that are indirect subscriptions.
 
@@ -29,23 +56,53 @@
         registered as an answer contact because of his membership in a team.
         """
 
+    @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')),
+        participation=Choice(
+            title=_('Participation'), required=False,
+            vocabulary=QuestionParticipation),
+        needs_attention=Bool(
+            title=_('Needs attentions from'), default=False, required=False),
+        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,
+                        # Lp wants a sequence, but lazr.restful only supports
+                        # lists; cast the tuple as a list.
+                        status=list(QUESTION_STATUS_DEFAULT_SEARCH),
                         language=None, sort=None, participation=None,
                         needs_attention=None):
         """Search the person's questions.
 
-        See IQuestionCollection for the description of the standard search
-        parameters.
-
-        :participation: A list of QuestionParticipation that defines the set
-        of relationship to questions that will be searched. If None or an
-        empty sequence, all relationships are considered.
-
-        :needs_attention: If this flag is true, only questions needing
-        attention from the person will be included. Questions needing
-        attention are those owned by the person in the ANSWERED or NEEDSINFO
-        state, as well as, those 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.
+        :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 participation: A list of QuestionParticipation that defines the
+            set of relationship to questions that will be searched. If None or
+            an empty sequence, all relationships are considered.
+        :param needs_attention: If this flag is true, only questions that
+            need attention the person will be included. 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/webservice.py'
--- lib/lp/answers/interfaces/webservice.py	2011-05-14 04:00:13 +0000
+++ lib/lp/answers/interfaces/webservice.py	2011-05-18 05:07:27 +0000
@@ -27,6 +27,7 @@
     ISearchableByQuestionOwner,
     )
 from lp.answers.interfaces.questionmessage import  IQuestionMessage
+from lp.answers.interfaces.questionsperson import  IQuestionsPerson
 from lp.answers.interfaces.questiontarget import IQuestionTarget
 
 
@@ -38,3 +39,9 @@
 patch_collection_return_type(
     ISearchableByQuestionOwner, 'searchQuestions', IQuestion)
 patch_reference_property(IQuestionMessage, 'question', IQuestion)
+patch_collection_return_type(
+    IQuestionsPerson, 'getDirectAnswerQuestionTargets', IQuestionTarget)
+patch_collection_return_type(
+    IQuestionsPerson, 'getTeamAnswerQuestionTargets', IQuestionTarget)
+patch_collection_return_type(
+    IQuestionsPerson, 'searchQuestions', IQuestion)

=== modified file 'lib/lp/answers/model/questionsperson.py'
--- lib/lp/answers/model/questionsperson.py	2011-04-26 16:22:11 +0000
+++ lib/lp/answers/model/questionsperson.py	2011-05-18 05:07:27 +0000
@@ -3,29 +3,19 @@
 
 __metaclass__ = type
 __all__ = [
-    'QuestionsPerson',
+    'QuestionsPersonMixin',
     ]
 
 
-from zope.component import adapts
-from zope.interface import implements
-
 from canonical.database.sqlbase import sqlvalues
 from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH
-from lp.answers.interfaces.questionsperson import IQuestionsPerson
 from lp.answers.model.answercontact import AnswerContact
 from lp.answers.model.question import QuestionPersonSearch
-from lp.registry.interfaces.person import IPerson
 from lp.services.worlddata.model.language import Language
 
 
-class QuestionsPerson:
+class QuestionsPersonMixin:
     """See `IQuestionsPerson`."""
-    implements(IQuestionsPerson)
-    adapts(IPerson)
-
-    def __init__(self, person):
-        self.person = person
 
     def searchQuestions(self, search_text=None,
                         status=QUESTION_STATUS_DEFAULT_SEARCH,
@@ -33,7 +23,7 @@
                         needs_attention=None):
         """See `IQuestionsPerson`."""
         return QuestionPersonSearch(
-                person=self.person,
+                person=self,
                 search_text=search_text,
                 status=status, language=language, sort=sort,
                 participation=participation,
@@ -52,13 +42,13 @@
             UNION SELECT question
                   FROM QuestionMessage JOIN Message ON (message = Message.id)
                   WHERE owner = %(personID)s
-            )""" % sqlvalues(personID=self.person.id),
+            )""" % sqlvalues(personID=self.id),
             clauseTables=['Question'], distinct=True))
 
     def getDirectAnswerQuestionTargets(self):
         """See `IQuestionsPerson`."""
         answer_contacts = AnswerContact.select(
-            'person = %s' % sqlvalues(self.person))
+            'person = %s' % sqlvalues(self))
         return self._getQuestionTargetsFromAnswerContacts(answer_contacts)
 
     def getTeamAnswerQuestionTargets(self):
@@ -67,7 +57,7 @@
             '''AnswerContact.person = TeamParticipation.team
             AND TeamParticipation.person = %(personID)s
             AND AnswerContact.person != %(personID)s''' % sqlvalues(
-                personID=self.person.id),
+                personID=self.id),
             clauseTables=['TeamParticipation'], distinct=True)
         return self._getQuestionTargetsFromAnswerContacts(answer_contacts)
 

=== modified file 'lib/lp/answers/stories/webservice.txt'
--- lib/lp/answers/stories/webservice.txt	2011-05-16 20:48:19 +0000
+++ lib/lp/answers/stories/webservice.txt	2011-05-18 05:07:27 +0000
@@ -67,8 +67,10 @@
 
     >>> team = contact_webservice.get(
     ...     '/~my-team', api_version='devel').jsonBody()
+    >>> team_project = contact_webservice.get(
+    ...     '/team-project', api_version='devel').jsonBody()
     >>> contact_webservice.named_get(
-    ...     project['self_link'], 'canUserAlterAnswerContact',
+    ...     team_project['self_link'], 'canUserAlterAnswerContact',
     ...     person=team['self_link'], api_version='devel').jsonBody()
     True
 
@@ -76,32 +78,50 @@
     ...     team['self_link'], 'addLanguage',
     ...     language='/+languages/fr', api_version='devel').jsonBody()
     >>> contact_webservice.named_post(
-    ...     project['self_link'], 'addAnswerContact',
+    ...     team_project['self_link'], 'addAnswerContact',
     ...     person=team['self_link'], api_version='devel').jsonBody()
     True
 
-
 Anyone can get the collection of languages spoken by at least one
 answer contact by calling getSupportedLanguages.
 
     >>> languages = anon_webservice.named_get(
-    ...     project['self_link'], 'getSupportedLanguages',
+    ...     team_project['self_link'], 'getSupportedLanguages',
     ...     api_version='devel').jsonBody()
     >>> print_self_link_of_entries(languages)
     http://.../+languages/en
     http://.../+languages/fr
 
-    >>> english = languages['entries'][0]
-
 Anyone can retrieve the collection of answer contacts for a language using
 getAnswerContactsForLanguage.
 
+    >>> english = anon_webservice.get(
+    ...     '/+languages/en', api_version='devel').jsonBody()
+
     >>> contacts = anon_webservice.named_get(
     ...     project['self_link'], 'getAnswerContactsForLanguage',
     ...     language=english['self_link'], api_version='devel').jsonBody()
     >>> print_self_link_of_entries(contacts)
     http://.../~contact
 
+Anyone can retrieve the collection of `IQuestionTarget`s that a person
+is an answer contact for using getDirectAnswerQuestionTargets.
+
+    >>> targets = anon_webservice.named_get(
+    ...     contact['self_link'], 'getDirectAnswerQuestionTargets',
+    ...     api_version='devel').jsonBody()
+    >>> print_self_link_of_entries(targets)
+    http://api.launchpad.dev/devel/my-project
+
+Anyone can retrieve the collection of `IQuestionTarget`s that a person's
+teams is an answer contact for using getTeamAnswerQuestionTargets.
+
+    >>> targets = anon_webservice.named_get(
+    ...     contact['self_link'], 'getTeamAnswerQuestionTargets',
+    ...     api_version='devel').jsonBody()
+    >>> print_self_link_of_entries(targets)
+    http://api.launchpad.dev/devel/team-project
+
 
 Question collections
 --------------------
@@ -148,6 +168,23 @@
     Q 1 great
 
 
+Anyone can retrieve a collection of questions from an `IPerson` with
+searchQuestions. The question will that match the precise search criteria
+called with searchQuestions.
+
+    >>> questions = anon_webservice.named_get(
+    ...     contact['self_link'], 'searchQuestions',
+    ...     search_text='q great',
+    ...     status=['Open', 'Needs information', 'Answered'],
+    ...     language=[english['self_link']],
+    ...     needs_attention=False,
+    ...     sort='oldest first',
+    ...     api_version='devel').jsonBody()
+    >>> for question in questions['entries']:
+    ...     print question['title']
+    Q 1 great
+
+
 A question
 ----------
 

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2011-05-09 16:43:20 +0000
+++ lib/lp/registry/interfaces/person.py	2011-05-18 05:07:27 +0000
@@ -105,6 +105,7 @@
 from canonical.launchpad.interfaces.validation import validate_new_team_email
 from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
+from lp.answers.interfaces.questionsperson import IQuestionsPerson
 from lp.app.errors import NameLookupFailed
 from lp.app.interfaces.headings import IRootContext
 from lp.app.validators import LaunchpadValidationError
@@ -641,7 +642,7 @@
                     IHasMergeProposals, IHasLogo, IHasMugshot, IHasIcon,
                     IHasLocation, IHasRequestedReviews, IObjectWithLocation,
                     IPrivacy, IHasBugs, IHasRecipes, IHasTranslationImports,
-                    IPersonSettings):
+                    IPersonSettings, IQuestionsPerson):
     """Public attributes for a Person."""
 
     id = Int(title=_('ID'), required=True, readonly=True)

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2011-05-10 18:02:28 +0000
+++ lib/lp/registry/model/person.py	2011-05-18 05:07:27 +0000
@@ -172,6 +172,7 @@
 from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
 from canonical.launchpad.webapp.interfaces import ILaunchBag
 from canonical.lazr.utils import get_current_browser_request
+from lp.answers.model.questionsperson import QuestionsPersonMixin
 from lp.app.validators.email import valid_email
 from lp.app.validators.name import (
     sanitize_name,
@@ -431,7 +432,8 @@
 
 class Person(
     SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin,
-    HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin):
+    HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin,
+    QuestionsPersonMixin):
     """A Person."""
 
     implements(IPerson, IHasIcon, IHasLogo, IHasMugshot)