Merge lp:~jcsackett/launchpad/api-wants-questionset into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 12857
Proposed branch: lp:~jcsackett/launchpad/api-wants-questionset
Merge into: lp:launchpad
Diff against target: 161 lines (+46/-10)
5 files modified
lib/lp/answers/browser/question.py (+1/-1)
lib/lp/answers/interfaces/question.py (+2/-6)
lib/lp/answers/interfaces/questioncollection.py (+24/-3)
lib/lp/answers/interfaces/webservice.py (+9/-0)
lib/lp/answers/tests/test_question_webservice.py (+10/-0)
To merge this branch: bzr merge lp:~jcsackett/launchpad/api-wants-questionset
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+57723@code.launchpad.net

This proposal supersedes a proposal from 2011-04-08.

Commit message

[r=sinzui][bug=761877] Adds questionset toplevel to launchpad API.

Description of the change

Summary
=======
This exposes question set on the api, so https://api.launchpad.net/devel/questions/1 works, and the API doesn't require url hacking to get at a question.

This is a resubmit of a previous MP, specifically to address problems that were revealed when attempting to land this branch.

Preimp
======
Spoke with Curtis Hovey about exposing the top level elements of an application.

Implementation
==============
lib/lp/answers/browser/question.py
----------------------------------
Change the traverse method for the questionset. It previously returned a redirect to the canonical_url of the question directly, and was request unaware, so it would redirect outside of api. Since canonical_url is called on returned objects higher up the stack (and *is* request aware), we can just rely on that.

lib/lp/answers/interfaces/questioncollection.py
lib/lp/answers/interfaces/webservice.py
---------------------------------------
Exposed the needed elements of the QuestionSet interface on the API.
Some patching to deal with circular imports.

lib/lp/answers/interfaces/question.py
-------------------------------------------------
Exported IQuestion to all versions of the API, so the entry_schema is available for IQuestionSet (which cannot be restricted to a particular API version).

Tests
=====
bin/test -m lp.answers
bin/test -t test_wadl

QA
==
Since this changes traversal for questions, first make sure a the usual question links are working (e.g. /questions/$question_id and /$product/+question/$question_id).

Then, attempt to load a question via the api over https://api.launchpad.net/devel/questions/$question_id and https://api.launchpad.net/devel/$product/+question/$question_id. They should both show the same data.

Lint
====
make lint output:

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/answers/browser/question.py
  lib/lp/answers/interfaces/question.py
  lib/lp/answers/interfaces/questioncollection.py
  lib/lp/answers/interfaces/webservice.py

./lib/lp/answers/interfaces/questioncollection.py
      94: E302 expected 2 blank lines, found 0
./lib/lp/answers/interfaces/webservice.py
      18: 'IQuestionSet' imported but unused
      17: 'IQuestion' imported but unused

The two unused imports are needed for the webservice zcml.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote : Posted in a previous version of this proposal

This looks good.

I tried to find a way to silence the "imported but unused" lint, but couldn't find a good way to do it.

review: Approve (code)
Revision history for this message
Jonathan Lange (jml) wrote : Posted in a previous version of this proposal

On Mon, Apr 11, 2011 at 5:01 PM, Benji York <email address hidden> wrote:
> Review: Approve code
> This looks good.
>
> I tried to find a way to silence the "imported but unused" lint, but couldn't find a good way to do it.

One idiom in other pyflakes-using projects is:

"""
from foo import bar

# Shut up pyflakes. 'bar' imported for side-effect X
bar
"""

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Jonathan.

I think these changes are good, but I would be more certain if there were tests verifying that IQuestionSet is exported and the methods operate with the exported params. I expect an update to lib/lp/answers/tests/test_question_webservice.py that exercises the object properties and methods. Or maybe a test like /lib/lp/registry/stories/webservice/xx-people.txt that would document how I might write scripts that use this API.

Please add the following to lib/lp/answers/interfaces/webservice.py because I would delete those unused imports when lint told me they were unused:

# The two unused imports are needed for the webservice zcml
IQuestionSet
IQuestion

I image I would QA this with something like:

from launchpadlib.launchpad import Launchpad
lp = Launchpad.login_anonymously(
    'test', 'https://api.launchpad.net/', version='devel')
question = lp.questions['119712']
# expected: Request manual merge of my account
print question.title

review: Needs Fixing (code)
Revision history for this message
j.c.sackett (jcsackett) wrote :

> Please add the following to lib/lp/answers/interfaces/webservice.py because I
> would delete those unused imports when lint told me they were unused:
>
> # The two unused imports are needed for the webservice zcml
> IQuestionSet
> IQuestion

Those actually are used now. I failed to update the lint when I resubmitted.

jc@jabberwocky:~/Code/launchpad/lp-branches/api-wants-questionset$ make lint
= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/answers/browser/question.py
  lib/lp/answers/interfaces/question.py
  lib/lp/answers/interfaces/questioncollection.py
  lib/lp/answers/interfaces/webservice.py

./lib/lp/answers/interfaces/question.py
     496: E302 expected 2 blank lines, found 1

Revision history for this message
j.c.sackett (jcsackett) wrote :

I've pushed up changes that introduce a test of the API being hooked up to IQuestionSet.

http://paste.ubuntu.com/594190/

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you very much. I think this is good to land.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/browser/question.py'
2--- lib/lp/answers/browser/question.py 2011-02-02 15:43:31 +0000
3+++ lib/lp/answers/browser/question.py 2011-04-18 16:21:50 +0000
4@@ -237,7 +237,7 @@
5 question = None
6 if question is None:
7 raise NotFoundError(name)
8- return redirection(canonical_url(question), status=301)
9+ return redirection(canonical_url(question, self.request), status=301)
10
11
12 class QuestionBreadcrumb(Breadcrumb):
13
14=== modified file 'lib/lp/answers/interfaces/question.py'
15--- lib/lp/answers/interfaces/question.py 2011-03-27 21:40:32 +0000
16+++ lib/lp/answers/interfaces/question.py 2011-04-18 16:21:50 +0000
17@@ -25,10 +25,6 @@
18 operation_parameters,
19 REQUEST_USER,
20 )
21-from lazr.restful.fields import (
22- CollectionField,
23- Reference,
24- )
25 from zope.interface import (
26 Attribute,
27 Interface,
28@@ -67,7 +63,7 @@
29 class IQuestion(IHasOwner):
30 """A single question, often a support request."""
31
32- export_as_webservice_entry(as_of="devel")
33+ export_as_webservice_entry(as_of='beta')
34
35 id = exported(Int(
36 title=_('Question Number'), required=True, readonly=True,
37@@ -489,7 +485,7 @@
38 @operation_for_version('devel')
39 def setCommentVisibility(user, comment_number, visible):
40 """Set the visible attribute on a question message.
41-
42+
43 This is restricted to Launchpad admins and registry members, and will
44 return a HTTP Error 401: Unauthorized error for non-admin callers.
45 """
46
47=== modified file 'lib/lp/answers/interfaces/questioncollection.py'
48--- lib/lp/answers/interfaces/questioncollection.py 2010-08-20 20:31:18 +0000
49+++ lib/lp/answers/interfaces/questioncollection.py 2011-04-18 16:21:50 +0000
50@@ -11,14 +11,25 @@
51 'IQuestionCollection',
52 'IQuestionSet',
53 'ISearchableByQuestionOwner',
54- 'QUESTION_STATUS_DEFAULT_SEARCH'
55+ 'QUESTION_STATUS_DEFAULT_SEARCH',
56 ]
57
58 from zope.interface import (
59 Attribute,
60 Interface,
61 )
62-
63+from zope.schema import Int
64+
65+from lazr.restful.declarations import (
66+ collection_default_content,
67+ export_as_webservice_collection,
68+ export_operation_as,
69+ export_read_operation,
70+ operation_for_version,
71+ operation_parameters,
72+ )
73+
74+from canonical.launchpad import _
75 from lp.answers.interfaces.questionenums import QuestionStatus
76
77
78@@ -47,7 +58,7 @@
79 against the question's language. If None or an empty sequence,
80 the language is not included as a filter criteria.
81
82- :sort: An attribute of QuestionSort. If None, a default value is used.
83+ :sort: An attribute of QuestionSort. If None, a default value is used.
84 When there is a search_text value, the default is to sort by
85 RELEVANCY, otherwise results are sorted NEWEST_FIRST.
86 """
87@@ -82,8 +93,17 @@
88 class IQuestionSet(IQuestionCollection):
89 """A utility that contain all the questions published in Launchpad."""
90
91+ export_as_webservice_collection(Interface)
92+
93 title = Attribute('Title')
94
95+ @operation_parameters(
96+ question_id=Int(
97+ title=_('The id of the question to get'),
98+ required=True))
99+ @export_read_operation()
100+ @export_operation_as("getByID")
101+ @operation_for_version('devel')
102 def get(question_id, default=None):
103 """Return the question with the given id.
104
105@@ -98,6 +118,7 @@
106 comments in the last <days_before_expiration> days.
107 """
108
109+ @collection_default_content(limit=5)
110 def getMostActiveProjects(limit=5):
111 """Return the list of projects that asked the most questions in
112 the last 60 days.
113
114=== modified file 'lib/lp/answers/interfaces/webservice.py'
115--- lib/lp/answers/interfaces/webservice.py 2011-03-21 21:34:04 +0000
116+++ lib/lp/answers/interfaces/webservice.py 2011-04-18 16:21:50 +0000
117@@ -11,6 +11,15 @@
118
119 __all__ = [
120 'IQuestion',
121+ 'IQuestionSet',
122 ]
123
124+from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
125+
126 from lp.answers.interfaces.question import IQuestion
127+from lp.answers.interfaces.questioncollection import IQuestionSet
128+
129+IQuestionSet.queryTaggedValue(
130+ LAZR_WEBSERVICE_EXPORTED)['collection_entry_schema'] = IQuestion
131+IQuestionSet['get'].queryTaggedValue(
132+ LAZR_WEBSERVICE_EXPORTED)['return_type'] = IQuestion
133
134=== modified file 'lib/lp/answers/tests/test_question_webservice.py'
135--- lib/lp/answers/tests/test_question_webservice.py 2011-03-27 16:03:44 +0000
136+++ lib/lp/answers/tests/test_question_webservice.py 2011-04-18 16:21:50 +0000
137@@ -46,7 +46,16 @@
138 dd = dt.findNextSibling('dd')
139 return str(dd.contents.pop())
140
141+ def test_top_level_question_get(self):
142+ # The top level question set can be used via the api to get
143+ # a question by id via redirect without url hacking.
144+ response = self.webservice.get(
145+ '/questions/%s' % self.question.id, 'application/xhtml+xml')
146+ self.assertEqual(response.status, 301)
147+
148+
149 def test_GET_xhtml_representation(self):
150+ # A question's xhtml representation is available on the api.
151 response = self.webservice.get(
152 '/%s/+question/%d' % (self.question.target.name,
153 self.question.id),
154@@ -58,6 +67,7 @@
155 "<p>This is a question</p>")
156
157 def test_PATCH_xhtml_representation(self):
158+ # You can update the question through the api with PATCH.
159 new_title = "No, this is a question"
160
161 question_json = self.webservice.get(