launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #03659
[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)