Merge lp:~sinzui/launchpad/anwers-api-0 into lp:launchpad
- anwers-api-0
- Merge into devel
Status: | Merged |
---|---|
Merged at revision: | 13009 |
Proposed branch: | lp:~sinzui/launchpad/anwers-api-0 |
Merge into: | lp:launchpad |
Diff against target: |
1160 lines (+362/-88) 28 files modified
lib/lp/answers/browser/question.py (+1/-1) lib/lp/answers/browser/questiontarget.py (+4/-4) lib/lp/answers/browser/tests/test_menus.py (+1/-1) lib/lp/answers/doc/expiration.txt (+1/-1) lib/lp/answers/doc/faq.txt (+1/-1) lib/lp/answers/doc/faqtarget.txt (+1/-1) lib/lp/answers/doc/notifications.txt (+2/-2) lib/lp/answers/doc/person.txt (+4/-3) lib/lp/answers/doc/question.txt (+3/-3) lib/lp/answers/doc/questiontarget-sourcepackage.txt (+7/-7) lib/lp/answers/doc/questiontarget.txt (+10/-10) lib/lp/answers/doc/workflow.txt (+1/-1) lib/lp/answers/errors.py (+20/-0) lib/lp/answers/interfaces/questiontarget.py (+73/-13) lib/lp/answers/interfaces/webservice.py (+2/-0) lib/lp/answers/model/question.py (+27/-8) lib/lp/answers/stories/distribution-package-answer-contact.txt (+1/-1) lib/lp/answers/stories/webservice.txt (+113/-0) lib/lp/answers/tests/test_faq.py (+1/-1) lib/lp/answers/tests/test_faqtarget.py (+1/-1) lib/lp/answers/tests/test_question_workflow.py (+4/-4) lib/lp/answers/tests/test_questionjob.py (+2/-2) lib/lp/answers/tests/test_questiontarget.py (+43/-2) lib/lp/coop/answersbugs/tests/test_doc.py (+2/-2) lib/lp/registry/configure.zcml (+4/-0) lib/lp/registry/interfaces/person.py (+16/-8) lib/lp/registry/model/sourcepackage.py (+5/-3) lib/lp/testing/factory.py (+12/-8) |
To merge this branch: | bzr merge lp:~sinzui/launchpad/anwers-api-0 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Benji York (community) | code | Approve | |
Review via email: mp+60238@code.launchpad.net |
Commit message
Description of the change
Export answer contact management over the API.
Launchpad bug: https:/
Pre-
This is an incremental branch, probably 1 of 3 to allow users and team
to work with Lp Answers over the API.
-------
RULES
* Export the needed methods on IQuestionTarget and IQuestion to
allow be answer contacts for a project.
QA
* Verify you can add yourself as an answer contact to Launchpad.
from launchpadlib.
lp = Launchpad.
'test', 'https:/
project = lp.projects[
user = lp.people['sinzui']
language = lp.languages['fr']
user.
print project.
# expect True
print project.
# expect True
for lang in project.
print lang.code
for contact in project.
print contact.name
# Expect sinzui
LINT
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
^ There are lint issues in some of these files and I can fix them after
the review.
TEST
./bin/test -vv -t answers.
IMPLEMENTATION
Updated the factory to allow me to use a more realistic user as the owner
of a question.
lib/
Added canUserAlterAns
StructuralSubsc
a vocabulary of the user and this administered teams. I changed
addAnswerContact and removeAnswerContact to require a subscribed_by
argument, then updated the only callsites in browser/
I expect mechanical fallout from this last change in doctests. I will
fix these after the review.
lib/
lib/
lib/
lib/
lib/
Exported the methods to document how to make myself and my teams answer
contacts. Note that the doctest setup more than is used. I have another
branch in progress that uses that setup to document how to search for
questions.
lib/
lib/
lib/
@operation_
Set. I changed the one call site that required a set to us a list.
lib/
lib/
Exported addLanguage and removeLanguage so that answer contacts can control
the Languages they support in Answers.
lib/
Curtis Hovey (sinzui) wrote : | # |
On Mon, 2011-05-09 at 14:47 +0000, Benji York wrote:
> In addAnswerContact you might want to use lazr.restful.
> make the ValueError return a 400 instead of 500, because (as far as I
> can tell), the error is on the client's part and not the server.
Thank you for calling my attention to this. I was under the mistaken
impression that Leonard had lands a change to Launchpad that ensures
ValueErrors raised on the API layer were always 400 errors. I do not see
code that does that So I created a new error that will raise a 400.
> The five attributes/methods newQuestion, createQuestionF
> canUserAlterAns
> used together several times in lib/lp/
> Perhaps
> they should be made into an interface so they can be treated as a
> unit.
I will decline this for now because it requires the rework of several
interfaces and models. This I will do this as time permits.
> Although I'm not a fan of it, the two existing parameters of
> makeQuestion in lib/lp/
> JavaDoc style so you may want to do the same for the new parameter you
> added.
Done.
--
__Curtis C. Hovey_________
http://
Benji York (benji) wrote : | # |
On Mon, May 9, 2011 at 12:48 PM, Curtis Hovey
<email address hidden> wrote:
> On Mon, 2011-05-09 at 14:47 +0000, Benji York wrote:
>> In addAnswerContact you might want to use lazr.restful.
>> make the ValueError return a 400 instead of 500, because (as far as I
>> can tell), the error is on the client's part and not the server.
>
> Thank you for calling my attention to this. I was under the mistaken
> impression that Leonard had lands a change to Launchpad that ensures
> ValueErrors raised on the API layer were always 400 errors. I do not see
> code that does that So I created a new error that will raise a 400.
If you want to stick with ValueError, all you have to do is wrap it in a
call to lazr.restful.
raise expose(
--
Benji York
Preview Diff
1 | === modified file 'lib/lp/answers/browser/question.py' |
2 | --- lib/lp/answers/browser/question.py 2011-05-06 13:18:12 +0000 |
3 | +++ lib/lp/answers/browser/question.py 2011-05-09 18:59:17 +0000 |
4 | @@ -437,7 +437,7 @@ |
5 | question_target = IQuestionTarget(self.view.question_target) |
6 | supported_languages = question_target.getSupportedLanguages() |
7 | else: |
8 | - supported_languages = set([english]) |
9 | + supported_languages = [english] |
10 | |
11 | terms = [] |
12 | for lang in languages: |
13 | |
14 | === modified file 'lib/lp/answers/browser/questiontarget.py' |
15 | --- lib/lp/answers/browser/questiontarget.py 2011-05-03 14:14:49 +0000 |
16 | +++ lib/lp/answers/browser/questiontarget.py 2011-05-09 18:59:17 +0000 |
17 | @@ -738,12 +738,12 @@ |
18 | replacements = {'context': self.context.displayname} |
19 | if want_to_be_answer_contact: |
20 | self._updatePreferredLanguages(self.user) |
21 | - if self.context.addAnswerContact(self.user): |
22 | + if self.context.addAnswerContact(self.user, self.user): |
23 | response.addNotification( |
24 | _('You have been added as an answer contact for ' |
25 | '$context.', mapping=replacements)) |
26 | else: |
27 | - if self.context.removeAnswerContact(self.user): |
28 | + if self.context.removeAnswerContact(self.user, self.user): |
29 | response.addNotification( |
30 | _('You have been removed as an answer contact for ' |
31 | '$context.', mapping=replacements)) |
32 | @@ -752,12 +752,12 @@ |
33 | replacements['teamname'] = team.displayname |
34 | if team in answer_contact_teams: |
35 | self._updatePreferredLanguages(team) |
36 | - if self.context.addAnswerContact(team): |
37 | + if self.context.addAnswerContact(team, self.user): |
38 | response.addNotification( |
39 | _('$teamname has been added as an answer contact ' |
40 | 'for $context.', mapping=replacements)) |
41 | else: |
42 | - if self.context.removeAnswerContact(team): |
43 | + if self.context.removeAnswerContact(team, self.user): |
44 | response.addNotification( |
45 | _('$teamname has been removed as an answer contact ' |
46 | 'for $context.', mapping=replacements)) |
47 | |
48 | === modified file 'lib/lp/answers/browser/tests/test_menus.py' |
49 | --- lib/lp/answers/browser/tests/test_menus.py 2010-08-20 20:31:18 +0000 |
50 | +++ lib/lp/answers/browser/tests/test_menus.py 2011-05-09 18:59:17 +0000 |
51 | @@ -46,7 +46,7 @@ |
52 | # A question with a linked FAQ has an 'edit' icon. |
53 | self.person.addLanguage(getUtility(ILanguageSet)['en']) |
54 | target = self.question.target |
55 | - target.addAnswerContact(self.person) |
56 | + target.addAnswerContact(self.person, self.person) |
57 | faq = self.factory.makeFAQ(target=target) |
58 | self.question.linkFAQ(self.person, faq, 'message') |
59 | link = menu.linkfaq() |
60 | |
61 | === modified file 'lib/lp/answers/doc/expiration.txt' |
62 | --- lib/lp/answers/doc/expiration.txt 2011-04-26 15:44:26 +0000 |
63 | +++ lib/lp/answers/doc/expiration.txt 2011-05-09 18:59:17 +0000 |
64 | @@ -82,7 +82,7 @@ |
65 | >>> old_open_question.subscribe(admin_team) |
66 | <QuestionSubscription...> |
67 | >>> salgado = getUtility(IPersonSet).getByName('salgado') |
68 | - >>> old_open_question.target.addAnswerContact(salgado) |
69 | + >>> old_open_question.target.addAnswerContact(salgado, salgado) |
70 | True |
71 | |
72 | # Link it to a FAQ item for the same reason. We are setting the |
73 | |
74 | === modified file 'lib/lp/answers/doc/faq.txt' |
75 | --- lib/lp/answers/doc/faq.txt 2010-12-06 22:59:15 +0000 |
76 | +++ lib/lp/answers/doc/faq.txt 2011-05-09 18:59:17 +0000 |
77 | @@ -199,7 +199,7 @@ |
78 | >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
79 | >>> no_priv = getUtility(ILaunchBag).user |
80 | >>> no_priv.addLanguage(getUtility(ILanguageSet)['en']) |
81 | - >>> firefox.addAnswerContact(no_priv) |
82 | + >>> firefox.addAnswerContact(no_priv, no_priv) |
83 | True |
84 | |
85 | >>> from canonical.launchpad.webapp.authorization import clear_cache |
86 | |
87 | === modified file 'lib/lp/answers/doc/faqtarget.txt' |
88 | --- lib/lp/answers/doc/faqtarget.txt 2010-10-18 22:24:59 +0000 |
89 | +++ lib/lp/answers/doc/faqtarget.txt 2011-05-09 18:59:17 +0000 |
90 | @@ -59,7 +59,7 @@ |
91 | |
92 | >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
93 | >>> no_priv.addLanguage(getUtility(ILanguageSet)['en']) |
94 | - >>> target.addAnswerContact(no_priv) |
95 | + >>> target.addAnswerContact(no_priv, no_priv) |
96 | True |
97 | |
98 | >>> clear_cache() # Clear authorization cache for check_permission. |
99 | |
100 | === modified file 'lib/lp/answers/doc/notifications.txt' |
101 | --- lib/lp/answers/doc/notifications.txt 2011-04-30 01:40:37 +0000 |
102 | +++ lib/lp/answers/doc/notifications.txt 2011-05-09 18:59:17 +0000 |
103 | @@ -68,7 +68,7 @@ |
104 | >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
105 | >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team') |
106 | >>> ubuntu_team.addLanguage(getUtility(ILanguageSet)['en']) |
107 | - >>> ubuntu.addAnswerContact(ubuntu_team) |
108 | + >>> ubuntu.addAnswerContact(ubuntu_team, ubuntu_team.teamowner) |
109 | True |
110 | |
111 | And assign this question to Foo Bar, so that he will also receive |
112 | @@ -699,7 +699,7 @@ |
113 | # supported in Ubuntu. |
114 | |
115 | >>> salgado = getUtility(IPersonSet).getByName('salgado') |
116 | - >>> ubuntu.addAnswerContact(salgado) |
117 | + >>> ubuntu.addAnswerContact(salgado, salgado) |
118 | True |
119 | |
120 | >>> sorted([lang.code for lang in ubuntu.getSupportedLanguages()]) |
121 | |
122 | === modified file 'lib/lp/answers/doc/person.txt' |
123 | --- lib/lp/answers/doc/person.txt 2011-04-26 16:39:33 +0000 |
124 | +++ lib/lp/answers/doc/person.txt 2011-05-09 18:59:17 +0000 |
125 | @@ -283,7 +283,7 @@ |
126 | |
127 | # Answer contacts must speak a language |
128 | >>> no_priv_raw.addLanguage(english) |
129 | - >>> firefox.addAnswerContact(no_priv_raw) |
130 | + >>> firefox.addAnswerContact(no_priv_raw, no_priv_raw) |
131 | True |
132 | |
133 | >>> for target in no_priv.getDirectAnswerQuestionTargets(): |
134 | @@ -306,7 +306,7 @@ |
135 | >>> from lp.registry.interfaces.distribution import IDistributionSet |
136 | >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu") |
137 | >>> landscape_team.addLanguage(english) |
138 | - >>> ubuntu.addAnswerContact(landscape_team) |
139 | + >>> ubuntu.addAnswerContact(landscape_team, landscape_team.teamowner) |
140 | True |
141 | |
142 | >>> print ', '.join( |
143 | @@ -334,7 +334,8 @@ |
144 | True |
145 | >>> evolution_package = ubuntu.getSourcePackage('evolution') |
146 | >>> translator_team.addLanguage(english) |
147 | - >>> evolution_package.addAnswerContact(translator_team) |
148 | + >>> evolution_package.addAnswerContact( |
149 | + ... translator_team, translator_team.teamowner) |
150 | True |
151 | >>> print ', '.join( |
152 | ... sorted(target.name |
153 | |
154 | === modified file 'lib/lp/answers/doc/question.txt' |
155 | --- lib/lp/answers/doc/question.txt 2011-04-25 14:37:52 +0000 |
156 | +++ lib/lp/answers/doc/question.txt 2011-05-09 18:59:17 +0000 |
157 | @@ -242,7 +242,7 @@ |
158 | >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
159 | >>> english = getUtility(ILanguageSet)['en'] |
160 | >>> no_priv.addLanguage(english) |
161 | - >>> firefox.addAnswerContact(no_priv) |
162 | + >>> firefox.addAnswerContact(no_priv, no_priv) |
163 | True |
164 | |
165 | >>> from lp.services.propertycache import get_property_cache |
166 | @@ -267,9 +267,9 @@ |
167 | [] |
168 | >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team') |
169 | >>> ubuntu_team.addLanguage(english) |
170 | - >>> ubuntu.addAnswerContact(ubuntu_team) |
171 | + >>> ubuntu.addAnswerContact(ubuntu_team, ubuntu_team.teamowner) |
172 | True |
173 | - >>> evolution_in_ubuntu.addAnswerContact(no_priv) |
174 | + >>> evolution_in_ubuntu.addAnswerContact(no_priv, no_priv) |
175 | True |
176 | >>> package_question = evolution_in_ubuntu.newQuestion( |
177 | ... sample_person, 'Upgrading to Evolution 1.4 breaks plug-ins', |
178 | |
179 | === modified file 'lib/lp/answers/doc/questiontarget-sourcepackage.txt' |
180 | --- lib/lp/answers/doc/questiontarget-sourcepackage.txt 2011-04-21 21:22:16 +0000 |
181 | +++ lib/lp/answers/doc/questiontarget-sourcepackage.txt 2011-05-09 18:59:17 +0000 |
182 | @@ -60,7 +60,7 @@ |
183 | >>> list(evolution.answer_contacts) |
184 | [] |
185 | |
186 | - >>> ubuntu.addAnswerContact(sample_person) |
187 | + >>> ubuntu.addAnswerContact(sample_person, sample_person) |
188 | True |
189 | |
190 | >>> [person.name for person in evolution.answer_contacts] |
191 | @@ -76,7 +76,7 @@ |
192 | A user can still subscribe as support contact for specific sourcepackage |
193 | even if he's already a support contact for the distribution. |
194 | |
195 | - >>> evolution.addAnswerContact(sample_person) |
196 | + >>> evolution.addAnswerContact(sample_person, sample_person) |
197 | True |
198 | |
199 | >>> [person.name for person in evolution.direct_answer_contacts] |
200 | @@ -91,7 +91,7 @@ |
201 | from the sourcepackage support contact list, not from the ubuntu |
202 | distribution list: |
203 | |
204 | - >>> evolution.removeAnswerContact(sample_person) |
205 | + >>> evolution.removeAnswerContact(sample_person, sample_person) |
206 | True |
207 | |
208 | >>> [person.name for person in evolution.direct_answer_contacts] |
209 | @@ -100,7 +100,7 @@ |
210 | >>> [person.name for person in evolution.answer_contacts] |
211 | [u'name16'] |
212 | |
213 | - >>> evolution.removeAnswerContact(sample_person) |
214 | + >>> evolution.removeAnswerContact(sample_person, sample_person) |
215 | False |
216 | |
217 | >>> [person.name for person in evolution.answer_contacts] |
218 | @@ -114,7 +114,7 @@ |
219 | >>> [person.name for person in evolution.answer_contacts] |
220 | [u'name16'] |
221 | |
222 | - >>> evolution.addAnswerContact(sample_person) |
223 | + >>> evolution.addAnswerContact(sample_person, sample_person) |
224 | True |
225 | |
226 | >>> [person.name for person in evolution.direct_answer_contacts] |
227 | @@ -123,13 +123,13 @@ |
228 | >>> [person.name for person in evolution.answer_contacts] |
229 | [u'name16'] |
230 | |
231 | - >>> evolution.removeAnswerContact(sample_person) |
232 | + >>> evolution.removeAnswerContact(sample_person, sample_person) |
233 | True |
234 | |
235 | >>> [person.name for person in evolution.answer_contacts] |
236 | [u'name16'] |
237 | |
238 | - >>> evolution.removeAnswerContact(sample_person) |
239 | + >>> evolution.removeAnswerContact(sample_person, sample_person) |
240 | False |
241 | |
242 | >>> [person.name for person in evolution.direct_answer_contacts] |
243 | |
244 | === modified file 'lib/lp/answers/doc/questiontarget.txt' |
245 | --- lib/lp/answers/doc/questiontarget.txt 2011-04-26 15:44:26 +0000 |
246 | +++ lib/lp/answers/doc/questiontarget.txt 2011-05-09 18:59:17 +0000 |
247 | @@ -385,7 +385,7 @@ |
248 | is only available to registered users. |
249 | |
250 | >>> name18 = getUtility(IPersonSet).getByName('name18') |
251 | - >>> target.addAnswerContact(name18) |
252 | + >>> target.addAnswerContact(name18, name18) |
253 | Traceback (most recent call last): |
254 | ... |
255 | Unauthorized... |
256 | @@ -394,7 +394,7 @@ |
257 | was already on the list. |
258 | |
259 | >>> login('no-priv@canonical.com') |
260 | - >>> target.addAnswerContact(name18) |
261 | + >>> target.addAnswerContact(name18, name18) |
262 | True |
263 | >>> people = [p.name for p in target.answer_contacts] |
264 | >>> len(people) |
265 | @@ -406,7 +406,7 @@ |
266 | 1 |
267 | >>> print people[0] |
268 | name18 |
269 | - >>> target.addAnswerContact(name18) |
270 | + >>> target.addAnswerContact(name18, name18) |
271 | False |
272 | |
273 | An answer contact must have at least one language among his preferred |
274 | @@ -415,28 +415,28 @@ |
275 | >>> sample_person = getUtility(IPersonSet).getByName('name12') |
276 | >>> len(sample_person.languages) |
277 | 0 |
278 | - >>> target.addAnswerContact(sample_person) |
279 | + >>> target.addAnswerContact(sample_person, sample_person) |
280 | Traceback (most recent call last): |
281 | ... |
282 | - AssertionError: An Answer Contact must speak a language... |
283 | + AddAnswerContactError: An answer contact must speak a language... |
284 | |
285 | Answer contacts can be removed by using the removeAnswerContact() method. |
286 | Like its counterpart, it returns True when the answer contact was removed and |
287 | False when the person wasn't on the answer contact list. |
288 | |
289 | - >>> target.removeAnswerContact(name18) |
290 | + >>> target.removeAnswerContact(name18, name18) |
291 | True |
292 | >>> list(target.answer_contacts) |
293 | [] |
294 | >>> list(target.direct_answer_contacts) |
295 | [] |
296 | - >>> target.removeAnswerContact(name18) |
297 | + >>> target.removeAnswerContact(name18, name18) |
298 | False |
299 | |
300 | Only registered users can remove an answer contact. |
301 | |
302 | >>> login(ANONYMOUS) |
303 | - >>> target.removeAnswerContact(name18) |
304 | + >>> target.removeAnswerContact(name18, name18) |
305 | Traceback (most recent call last): |
306 | ... |
307 | Unauthorized... |
308 | @@ -465,7 +465,7 @@ |
309 | ca |
310 | en |
311 | es |
312 | - >>> target.addAnswerContact(carlos) |
313 | + >>> target.addAnswerContact(carlos, carlos) |
314 | True |
315 | |
316 | While daf has en_GB as one of his preferred languages... |
317 | @@ -477,7 +477,7 @@ |
318 | en_GB |
319 | ja |
320 | cy |
321 | - >>> target.addAnswerContact(daf) |
322 | + >>> target.addAnswerContact(daf, daf) |
323 | True |
324 | |
325 | ...en_GB is not included in the target's supported languages, because all |
326 | |
327 | === modified file 'lib/lp/answers/doc/workflow.txt' |
328 | --- lib/lp/answers/doc/workflow.txt 2011-04-26 15:44:26 +0000 |
329 | +++ lib/lp/answers/doc/workflow.txt 2011-05-09 18:59:17 +0000 |
330 | @@ -53,7 +53,7 @@ |
331 | >>> ubuntu = getUtility(IDistributionSet)['ubuntu'] |
332 | >>> english = getUtility(ILanguageSet)['en'] |
333 | >>> sample_person.addLanguage(english) |
334 | - >>> ubuntu.addAnswerContact(sample_person) |
335 | + >>> ubuntu.addAnswerContact(sample_person, sample_person) |
336 | True |
337 | |
338 | # Sanity check: the admin isn't in the team owning the distribution. |
339 | |
340 | === added file 'lib/lp/answers/errors.py' |
341 | --- lib/lp/answers/errors.py 1970-01-01 00:00:00 +0000 |
342 | +++ lib/lp/answers/errors.py 2011-05-09 18:59:17 +0000 |
343 | @@ -0,0 +1,20 @@ |
344 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
345 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
346 | + |
347 | +__metaclass__ = type |
348 | +__all__ = [ |
349 | + 'AddAnswerContactError', |
350 | + ] |
351 | + |
352 | +import httplib |
353 | + |
354 | +from lazr.restful.declarations import webservice_error |
355 | + |
356 | + |
357 | +class AddAnswerContactError(ValueError): |
358 | + """The person cannot be an answer contact. |
359 | + |
360 | + An answer contacts must be a valid user or team that has a preferred |
361 | + language. |
362 | + """ |
363 | + webservice_error(httplib.BAD_REQUEST) |
364 | |
365 | === modified file 'lib/lp/answers/interfaces/questiontarget.py' |
366 | --- lib/lp/answers/interfaces/questiontarget.py 2011-04-26 16:22:11 +0000 |
367 | +++ lib/lp/answers/interfaces/questiontarget.py 2011-05-09 18:59:17 +0000 |
368 | @@ -16,11 +16,24 @@ |
369 | from zope.interface import Interface |
370 | from zope.schema import ( |
371 | Choice, |
372 | + Int, |
373 | List, |
374 | Set, |
375 | TextLine, |
376 | ) |
377 | |
378 | +from lazr.restful.declarations import ( |
379 | + call_with, |
380 | + export_as_webservice_entry, |
381 | + export_read_operation, |
382 | + export_write_operation, |
383 | + operation_for_version, |
384 | + operation_parameters, |
385 | + operation_returns_collection_of, |
386 | + REQUEST_USER, |
387 | + ) |
388 | +from lazr.restful.fields import Reference |
389 | + |
390 | from canonical.launchpad import _ |
391 | from lp.answers.interfaces.questioncollection import ( |
392 | ISearchableByQuestionOwner, |
393 | @@ -30,12 +43,16 @@ |
394 | QuestionStatus, |
395 | QUESTION_STATUS_DEFAULT_SEARCH, |
396 | ) |
397 | +from lp.registry.interfaces.person import IPerson |
398 | from lp.services.fields import PublicPersonChoice |
399 | +from lp.services.worlddata.interfaces.language import ILanguage |
400 | |
401 | |
402 | class IQuestionTarget(ISearchableByQuestionOwner): |
403 | """An object that can have a new question asked about it.""" |
404 | |
405 | + export_as_webservice_entry(as_of='devel') |
406 | + |
407 | def newQuestion(owner, title, description, language=None, |
408 | datecreated=None): |
409 | """Create a new question. |
410 | @@ -69,6 +86,10 @@ |
411 | :bug: An IBug. |
412 | """ |
413 | |
414 | + @operation_parameters( |
415 | + question_id=Int(title=_('Question Number'), required=True)) |
416 | + @export_read_operation() |
417 | + @operation_for_version('devel') |
418 | def getQuestion(question_id): |
419 | """Return the question by its id, if it is applicable to this target. |
420 | |
421 | @@ -87,25 +108,62 @@ |
422 | :title: A phrase |
423 | """ |
424 | |
425 | - def addAnswerContact(person): |
426 | + @operation_parameters( |
427 | + person=PublicPersonChoice( |
428 | + title=_('The user or an administered team'), required=True, |
429 | + vocabulary='ValidPersonOrTeam')) |
430 | + @call_with(subscribed_by=REQUEST_USER) |
431 | + @export_read_operation() |
432 | + @operation_for_version('devel') |
433 | + def canUserAlterAnswerContact(person, subscribed_by): |
434 | + """Can the user add or remove the answer contact. |
435 | + |
436 | + Users can add or remove themselves or one of the teams they |
437 | + administered. |
438 | + |
439 | + :param person: The `IPerson` that is or will be an answer contact. |
440 | + :param subscribed_by: The `IPerson` making the change. |
441 | + """ |
442 | + |
443 | + @operation_parameters( |
444 | + person=PublicPersonChoice( |
445 | + title=_('The user of an administered team'), required=True, |
446 | + vocabulary='ValidPersonOrTeam')) |
447 | + @call_with(subscribed_by=REQUEST_USER) |
448 | + @export_write_operation() |
449 | + @operation_for_version('devel') |
450 | + def addAnswerContact(person, subscribed_by): |
451 | """Add a new answer contact. |
452 | |
453 | - :person: An IPerson. |
454 | - |
455 | - Returns True if the person was added, False if the person already was |
456 | - an answer contact. A person must have at least one preferred |
457 | - language to be an answer contact. |
458 | + :param person: An `IPerson`. |
459 | + :param subscribed_by: The user making the change. |
460 | + :return: True if the person was added, False if the person already is |
461 | + an answer contact. |
462 | + :raises ValueError: When the person or team does no have a preferred |
463 | + language. |
464 | """ |
465 | |
466 | - def removeAnswerContact(person): |
467 | + @operation_parameters( |
468 | + person=PublicPersonChoice( |
469 | + title=_('The user of an administered team'), required=True, |
470 | + vocabulary='ValidPersonOrTeam')) |
471 | + @call_with(subscribed_by=REQUEST_USER) |
472 | + @export_write_operation() |
473 | + @operation_for_version('devel') |
474 | + def removeAnswerContact(person, subscribed_by): |
475 | """Remove an answer contact. |
476 | |
477 | - :person: An IPerson. |
478 | - |
479 | - Returns True if the person was removed, False if the person wasn't an |
480 | - answer contact. |
481 | + :param person: An `IPerson`. |
482 | + :param subscribed_by: The user making the change. |
483 | + :return: True if the person was removed, False if the person wasn't an |
484 | + answer contact. |
485 | """ |
486 | |
487 | + @operation_parameters( |
488 | + language=Reference(ILanguage)) |
489 | + @operation_returns_collection_of(IPerson) |
490 | + @export_read_operation() |
491 | + @operation_for_version('devel') |
492 | def getAnswerContactsForLanguage(language): |
493 | """Return the list of Persons that provide support for a language. |
494 | |
495 | @@ -124,9 +182,11 @@ |
496 | for the QuestionTarget. |
497 | """ |
498 | |
499 | + @operation_returns_collection_of(ILanguage) |
500 | + @export_read_operation() |
501 | + @operation_for_version('devel') |
502 | def getSupportedLanguages(): |
503 | - """Return the set of languages spoken by at least one of this object's |
504 | - answer contacts. |
505 | + """Return a list of languages spoken by at the answer contacts. |
506 | |
507 | An answer contact is considered to speak a given language if that |
508 | language is listed as one of his preferred languages. |
509 | |
510 | === modified file 'lib/lp/answers/interfaces/webservice.py' |
511 | --- lib/lp/answers/interfaces/webservice.py 2011-04-14 16:16:30 +0000 |
512 | +++ lib/lp/answers/interfaces/webservice.py 2011-05-09 18:59:17 +0000 |
513 | @@ -18,6 +18,8 @@ |
514 | |
515 | from lp.answers.interfaces.question import IQuestion |
516 | from lp.answers.interfaces.questioncollection import IQuestionSet |
517 | +from lp.answers.interfaces.questiontarget import IQuestionTarget |
518 | + |
519 | |
520 | IQuestionSet.queryTaggedValue( |
521 | LAZR_WEBSERVICE_EXPORTED)['collection_entry_schema'] = IQuestion |
522 | |
523 | === modified file 'lib/lp/answers/model/question.py' |
524 | --- lib/lp/answers/model/question.py 2011-04-28 18:40:45 +0000 |
525 | +++ lib/lp/answers/model/question.py 2011-05-09 18:59:17 +0000 |
526 | @@ -81,6 +81,9 @@ |
527 | QuestionStatus, |
528 | QUESTION_STATUS_DEFAULT_SEARCH, |
529 | ) |
530 | +from lp.answers.errors import ( |
531 | + AddAnswerContactError, |
532 | + ) |
533 | from lp.answers.interfaces.questiontarget import IQuestionTarget |
534 | from lp.answers.model.answercontact import AnswerContact |
535 | from lp.answers.model.questionmessage import QuestionMessage |
536 | @@ -1319,15 +1322,29 @@ |
537 | person.setLanguagesCache(languages) |
538 | return sorted(D.keys(), key=operator.attrgetter('displayname')) |
539 | |
540 | - def addAnswerContact(self, person): |
541 | - """See `IQuestionTarget`.""" |
542 | + def canUserAlterAnswerContact(self, person, subscribed_by): |
543 | + """See `IQuestionTarget`.""" |
544 | + if person is None or subscribed_by is None: |
545 | + return False |
546 | + admins = getUtility(ILaunchpadCelebrities).admin |
547 | + if (person == subscribed_by |
548 | + or person in subscribed_by.administrated_teams |
549 | + or subscribed_by.inTeam(admins)): |
550 | + return True |
551 | + return False |
552 | + |
553 | + def addAnswerContact(self, person, subscribed_by): |
554 | + """See `IQuestionTarget`.""" |
555 | + if not self.canUserAlterAnswerContact(person, subscribed_by): |
556 | + return False |
557 | answer_contact = AnswerContact.selectOneBy( |
558 | person=person, **self.getTargetTypes()) |
559 | if answer_contact is not None: |
560 | return False |
561 | # Person must speak a language to be an answer contact. |
562 | - assert len(person.languages) > 0, ( |
563 | - "An Answer Contact must speak a language.") |
564 | + if len(person.languages) == 0: |
565 | + raise AddAnswerContactError( |
566 | + "An answer contact must speak a language.") |
567 | params = dict(product=None, distribution=None, sourcepackagename=None) |
568 | params.update(self.getTargetTypes()) |
569 | answer_contact = AnswerContact(person=person, **params) |
570 | @@ -1370,8 +1387,8 @@ |
571 | else: |
572 | constraints.append(""" |
573 | Language.id = %s""" % sqlvalues(language)) |
574 | - return set(self._selectPersonFromAnswerContacts( |
575 | - constraints, ['PersonLanguage', 'Language'])) |
576 | + return list((self._selectPersonFromAnswerContacts( |
577 | + constraints, ['PersonLanguage', 'Language']))) |
578 | |
579 | def getAnswerContactRecipients(self, language): |
580 | """See `IQuestionTarget`.""" |
581 | @@ -1395,8 +1412,10 @@ |
582 | recipients.add(person, reason, header) |
583 | return recipients |
584 | |
585 | - def removeAnswerContact(self, person): |
586 | + def removeAnswerContact(self, person, subscribed_by): |
587 | """See `IQuestionTarget`.""" |
588 | + if not self.canUserAlterAnswerContact(person, subscribed_by): |
589 | + return False |
590 | if person not in self.answer_contacts: |
591 | return False |
592 | answer_contact = AnswerContact.selectOneBy( |
593 | @@ -1416,4 +1435,4 @@ |
594 | languages.add(getUtility(ILaunchpadCelebrities).english) |
595 | languages = set( |
596 | lang for lang in languages if not is_english_variant(lang)) |
597 | - return languages |
598 | + return list(languages) |
599 | |
600 | === modified file 'lib/lp/answers/stories/distribution-package-answer-contact.txt' |
601 | --- lib/lp/answers/stories/distribution-package-answer-contact.txt 2011-03-23 16:28:51 +0000 |
602 | +++ lib/lp/answers/stories/distribution-package-answer-contact.txt 2011-05-09 18:59:17 +0000 |
603 | @@ -16,7 +16,7 @@ |
604 | >>> # Answer contacts must speak a language |
605 | >>> user = getUtility(ILaunchBag).user |
606 | >>> user.addLanguage(getUtility(ILanguageSet)['en']) |
607 | - >>> ubuntu.addAnswerContact(user) |
608 | + >>> ubuntu.addAnswerContact(user, user) |
609 | True |
610 | >>> flush_database_updates() |
611 | >>> logout() |
612 | |
613 | === added file 'lib/lp/answers/stories/webservice.txt' |
614 | --- lib/lp/answers/stories/webservice.txt 1970-01-01 00:00:00 +0000 |
615 | +++ lib/lp/answers/stories/webservice.txt 2011-05-09 18:59:17 +0000 |
616 | @@ -0,0 +1,113 @@ |
617 | +Working with Launchpad Answers over the API |
618 | +=========================================== |
619 | + |
620 | +Users can work with question targets and questions over the api to |
621 | +search and update questions. This demonstration will use a project, it's |
622 | +contact, and asker, and three questions. |
623 | + |
624 | + >>> from zope.component import getUtility |
625 | + >>> from canonical.launchpad.testing.pages import webservice_for_person |
626 | + >>> from canonical.launchpad.webapp.interfaces import OAuthPermission |
627 | + >>> from lp.app.enums import ServiceUsage |
628 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
629 | + >>> from lp.testing.sampledata import ADMIN_EMAIL |
630 | + >>> lang_set = getUtility(ILanguageSet) |
631 | + |
632 | + >>> login(ADMIN_EMAIL) |
633 | + >>> _contact = factory.makePerson(name='contact') |
634 | + >>> _project = factory.makeProduct(name='my-project', owner=_contact) |
635 | + >>> _contact.addLanguage(lang_set['en']) |
636 | + >>> _project.answers_usage = ServiceUsage.LAUNCHPAD |
637 | + >>> success = _project.addAnswerContact(_contact, _contact) |
638 | + >>> _team = factory.makeTeam(owner=_contact, name='my-team') |
639 | + >>> _team_project = factory.makeProduct(name='team-project', owner=_team) |
640 | + >>> _asker = factory.makePerson(name='asker') |
641 | + >>> _question_1 = factory.makeQuestion( |
642 | + ... target=_project, title="Q 1", owner=_asker) |
643 | + >>> _question_2 = factory.makeQuestion( |
644 | + ... target=_project, title="Q 2", owner=_asker) |
645 | + >>> _question_3 = factory.makeQuestion( |
646 | + ... target=_team_project, title="Q 3", owner=_asker) |
647 | + >>> logout() |
648 | + |
649 | + >>> contact_webservice = webservice_for_person( |
650 | + ... _contact, permission=OAuthPermission.WRITE_PUBLIC) |
651 | + |
652 | + |
653 | +Answer contacts |
654 | +--------------- |
655 | + |
656 | +Users can add or remove themselves as an answer contact for a project. The |
657 | +user must have a preferred language. Scripts should call the |
658 | +canUserAlterAnswerContact method first to verify that the person can |
659 | +be added. |
660 | + |
661 | + >>> project = contact_webservice.get( |
662 | + ... '/my-project', api_version='devel').jsonBody() |
663 | + >>> contact = contact_webservice.get( |
664 | + ... '/~contact', api_version='devel').jsonBody() |
665 | + >>> contact_webservice.named_get( |
666 | + ... project['self_link'], 'canUserAlterAnswerContact', |
667 | + ... person=contact['self_link'], api_version='devel').jsonBody() |
668 | + True |
669 | + |
670 | + >>> contact_webservice.named_post( |
671 | + ... project['self_link'], 'removeAnswerContact', |
672 | + ... person=contact['self_link'], api_version='devel').jsonBody() |
673 | + True |
674 | + |
675 | + >>> contact_webservice.named_post( |
676 | + ... project['self_link'], 'addAnswerContact', |
677 | + ... person=contact['self_link'], api_version='devel').jsonBody() |
678 | + True |
679 | + |
680 | +User can also make the teams they administer answer contacts if the team has a |
681 | +preferred language. |
682 | + |
683 | + >>> team = contact_webservice.get( |
684 | + ... '/~my-team', api_version='devel').jsonBody() |
685 | + >>> contact_webservice.named_get( |
686 | + ... project['self_link'], 'canUserAlterAnswerContact', |
687 | + ... person=team['self_link'], api_version='devel').jsonBody() |
688 | + True |
689 | + |
690 | + >>> contact_webservice.named_post( |
691 | + ... team['self_link'], 'addLanguage', |
692 | + ... language='/+languages/fr', api_version='devel').jsonBody() |
693 | + >>> contact_webservice.named_post( |
694 | + ... project['self_link'], 'addAnswerContact', |
695 | + ... person=team['self_link'], api_version='devel').jsonBody() |
696 | + True |
697 | + |
698 | + |
699 | +Anyone can get the collection of languages spoken by at least one |
700 | +answer contact. |
701 | + |
702 | + >>> languages = anon_webservice.named_get( |
703 | + ... project['self_link'], 'getSupportedLanguages', |
704 | + ... api_version='devel').jsonBody() |
705 | + >>> print_self_link_of_entries(languages) |
706 | + http://.../+languages/en |
707 | + http://.../+languages/fr |
708 | + |
709 | + >>> english = languages['entries'][0] |
710 | + |
711 | +Anyone can retrieve the collection of answer contacts for a language. |
712 | + |
713 | + >>> contacts = anon_webservice.named_get( |
714 | + ... project['self_link'], 'getAnswerContactsForLanguage', |
715 | + ... language=english['self_link'], api_version='devel').jsonBody() |
716 | + >>> print_self_link_of_entries(contacts) |
717 | + http://.../~contact |
718 | + |
719 | + |
720 | +Questions |
721 | +--------- |
722 | + |
723 | +Anyone can retrieve a question from a `IQuestionTarget`. |
724 | + |
725 | + >>> question_1 = anon_webservice.named_get( |
726 | + ... project['self_link'], 'getQuestion', question_id=_question_1.id, |
727 | + ... api_version='devel').jsonBody() |
728 | + >>> print question_1['title'] |
729 | + Q 1 |
730 | |
731 | === modified file 'lib/lp/answers/tests/test_faq.py' |
732 | --- lib/lp/answers/tests/test_faq.py 2010-12-07 14:15:37 +0000 |
733 | +++ lib/lp/answers/tests/test_faq.py 2011-05-09 18:59:17 +0000 |
734 | @@ -33,7 +33,7 @@ |
735 | """Add the test person to the faq target's answer contacts.""" |
736 | language_set = getUtility(ILanguageSet) |
737 | answer_contact.addLanguage(language_set['en']) |
738 | - self.faq.target.addAnswerContact(answer_contact) |
739 | + self.faq.target.addAnswerContact(answer_contact, answer_contact) |
740 | |
741 | def assertCanEdit(self, user, faq): |
742 | """Assert that the user can edit an FAQ.""" |
743 | |
744 | === modified file 'lib/lp/answers/tests/test_faqtarget.py' |
745 | --- lib/lp/answers/tests/test_faqtarget.py 2010-12-07 14:15:37 +0000 |
746 | +++ lib/lp/answers/tests/test_faqtarget.py 2011-05-09 18:59:17 +0000 |
747 | @@ -27,7 +27,7 @@ |
748 | """Add the test person to the faq target's answer contacts.""" |
749 | language_set = getUtility(ILanguageSet) |
750 | answer_contact.addLanguage(language_set['en']) |
751 | - self.target.addAnswerContact(answer_contact) |
752 | + self.target.addAnswerContact(answer_contact, answer_contact) |
753 | |
754 | def assertCanAppend(self, user, target): |
755 | """Assert that the user can add an FAQ to an FAQ target.""" |
756 | |
757 | === modified file 'lib/lp/answers/tests/test_question_workflow.py' |
758 | --- lib/lp/answers/tests/test_question_workflow.py 2011-04-26 15:44:26 +0000 |
759 | +++ lib/lp/answers/tests/test_question_workflow.py 2011-05-09 18:59:17 +0000 |
760 | @@ -866,7 +866,7 @@ |
761 | # Reject user must be an answer contact, (or admin, or product owner). |
762 | # Answer contacts must speak a language |
763 | self.answerer.addLanguage(getUtility(ILanguageSet)['en']) |
764 | - self.ubuntu.addAnswerContact(self.answerer) |
765 | + self.ubuntu.addAnswerContact(self.answerer, self.answerer) |
766 | login_person(self.answerer) |
767 | self._testInvalidTransition( |
768 | valid_statuses, self.question.reject, |
769 | @@ -880,7 +880,7 @@ |
770 | login_person(self.answerer) |
771 | # Answer contacts must speak a language |
772 | self.answerer.addLanguage(getUtility(ILanguageSet)['en']) |
773 | - self.ubuntu.addAnswerContact(self.answerer) |
774 | + self.ubuntu.addAnswerContact(self.answerer, self.answerer) |
775 | valid_statuses = [status for status in QuestionStatus.items |
776 | if status.name != 'INVALID'] |
777 | |
778 | @@ -919,7 +919,7 @@ |
779 | |
780 | # Answer contacts must speak a language |
781 | self.answerer.addLanguage(getUtility(ILanguageSet)['en']) |
782 | - self.question.target.addAnswerContact(self.answerer) |
783 | + self.question.target.addAnswerContact(self.answerer, self.answerer) |
784 | # clear authorization cache for check_permission |
785 | clear_cache() |
786 | self.assertTrue( |
787 | @@ -938,7 +938,7 @@ |
788 | self.question.target = dsp |
789 | login_person(self.answerer) |
790 | self.answerer.addLanguage(getUtility(ILanguageSet)['en']) |
791 | - self.ubuntu.addAnswerContact(self.answerer) |
792 | + self.ubuntu.addAnswerContact(self.answerer, self.answerer) |
793 | self.assertTrue( |
794 | getattr(self.question, 'reject'), |
795 | "Answer contact cannot reject question.") |
796 | |
797 | === modified file 'lib/lp/answers/tests/test_questionjob.py' |
798 | --- lib/lp/answers/tests/test_questionjob.py 2011-05-02 18:23:05 +0000 |
799 | +++ lib/lp/answers/tests/test_questionjob.py 2011-05-09 18:59:17 +0000 |
800 | @@ -82,7 +82,7 @@ |
801 | with person_logged_in(contact): |
802 | lang_set = getUtility(ILanguageSet) |
803 | contact.addLanguage(lang_set['en']) |
804 | - question.target.addAnswerContact(contact) |
805 | + question.target.addAnswerContact(contact, contact) |
806 | return contact |
807 | |
808 | def test_create(self): |
809 | @@ -345,7 +345,7 @@ |
810 | with person_logged_in(user): |
811 | lang_set = getUtility(ILanguageSet) |
812 | user.addLanguage(lang_set['en']) |
813 | - question.target.addAnswerContact(user) |
814 | + question.target.addAnswerContact(user, user) |
815 | job = QuestionEmailJob.create( |
816 | question, user, QuestionRecipientSet.ASKER_SUBSCRIBER, |
817 | subject, body, headers) |
818 | |
819 | === modified file 'lib/lp/answers/tests/test_questiontarget.py' |
820 | --- lib/lp/answers/tests/test_questiontarget.py 2011-04-22 14:39:54 +0000 |
821 | +++ lib/lp/answers/tests/test_questiontarget.py 2011-05-09 18:59:17 +0000 |
822 | @@ -15,11 +15,52 @@ |
823 | from lp.registry.interfaces.distribution import IDistributionSet |
824 | from lp.services.worlddata.interfaces.language import ILanguageSet |
825 | from lp.testing import ( |
826 | + login_celebrity, |
827 | person_logged_in, |
828 | TestCaseWithFactory, |
829 | ) |
830 | |
831 | |
832 | +class QuestionTargetAnswerContactTestCase(TestCaseWithFactory): |
833 | + """Tests for changing an answer contact.""" |
834 | + |
835 | + layer = DatabaseFunctionalLayer |
836 | + |
837 | + def setUp(self): |
838 | + super(QuestionTargetAnswerContactTestCase, self).setUp() |
839 | + self.project = self.factory.makeProduct() |
840 | + self.user = self.factory.makePerson() |
841 | + |
842 | + def test_canUserAlterAnswerContact_self(self): |
843 | + login_person(self.user) |
844 | + self.assertTrue( |
845 | + self.project.canUserAlterAnswerContact(self.user, self.user)) |
846 | + |
847 | + def test_canUserAlterAnswerContact_other_user(self): |
848 | + login_person(self.user) |
849 | + other_user = self.factory.makePerson() |
850 | + self.assertFalse( |
851 | + self.project.canUserAlterAnswerContact(other_user, self.user)) |
852 | + |
853 | + def test_canUserAlterAnswerContact_administered_team(self): |
854 | + login_person(self.user) |
855 | + team = self.factory.makeTeam(owner=self.user) |
856 | + self.assertTrue( |
857 | + self.project.canUserAlterAnswerContact(team, self.user)) |
858 | + |
859 | + def test_canUserAlterAnswerContact_other_team(self): |
860 | + login_person(self.user) |
861 | + other_team = self.factory.makeTeam() |
862 | + self.assertFalse( |
863 | + self.project.canUserAlterAnswerContact(other_team, self.user)) |
864 | + |
865 | + def test_canUserAlterAnswerContact_admin(self): |
866 | + admin = login_celebrity('admin') |
867 | + other_user = self.factory.makePerson() |
868 | + self.assertTrue( |
869 | + self.project.canUserAlterAnswerContact(other_user, admin)) |
870 | + |
871 | + |
872 | class TestQuestionTarget_answer_contacts_with_languages(TestCaseWithFactory): |
873 | """Tests for the 'answer_contacts_with_languages' property of question |
874 | targets. |
875 | @@ -39,7 +80,7 @@ |
876 | # some non public methods to change its language cache. |
877 | answer_contact = removeSecurityProxy(self.answer_contact) |
878 | product = self.factory.makeProduct() |
879 | - product.addAnswerContact(answer_contact) |
880 | + product.addAnswerContact(answer_contact, answer_contact) |
881 | |
882 | # Must delete the cache because it's been filled in addAnswerContact. |
883 | answer_contact.deleteLanguagesCache() |
884 | @@ -62,7 +103,7 @@ |
885 | ubuntu = getUtility(IDistributionSet)['ubuntu'] |
886 | self.factory.makeSourcePackageName(name='test-pkg') |
887 | source_package = ubuntu.getSourcePackage('test-pkg') |
888 | - source_package.addAnswerContact(answer_contact) |
889 | + source_package.addAnswerContact(answer_contact, answer_contact) |
890 | |
891 | # Must delete the cache because it's been filled in addAnswerContact. |
892 | answer_contact.deleteLanguagesCache() |
893 | |
894 | === modified file 'lib/lp/coop/answersbugs/tests/test_doc.py' |
895 | --- lib/lp/coop/answersbugs/tests/test_doc.py 2010-10-21 04:19:36 +0000 |
896 | +++ lib/lp/coop/answersbugs/tests/test_doc.py 2011-05-09 18:59:17 +0000 |
897 | @@ -49,7 +49,7 @@ |
898 | ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team') |
899 | ubuntu_team.addLanguage(getUtility(ILanguageSet)['en']) |
900 | ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
901 | - ubuntu.addAnswerContact(ubuntu_team) |
902 | + ubuntu.addAnswerContact(ubuntu_team, ubuntu_team.teamowner) |
903 | ubuntu_question = ubuntu.newQuestion( |
904 | sample_person, "Can't install Ubuntu", |
905 | "I insert the install CD in the CD-ROM drive, but it won't boot.") |
906 | @@ -62,7 +62,7 @@ |
907 | [ubuntu_bugtask] = bug.bugtasks |
908 | login(ANONYMOUS) |
909 | # Remove the notifcations for the newly created question. |
910 | - notifications = pop_notifications() |
911 | + pop_notifications() |
912 | return ubuntu_bugtask.id |
913 | |
914 | |
915 | |
916 | === modified file 'lib/lp/registry/configure.zcml' |
917 | --- lib/lp/registry/configure.zcml 2011-05-03 04:39:43 +0000 |
918 | +++ lib/lp/registry/configure.zcml 2011-05-09 18:59:17 +0000 |
919 | @@ -529,6 +529,7 @@ |
920 | attributes=" |
921 | newQuestion |
922 | createQuestionFromBug |
923 | + canUserAlterAnswerContact |
924 | addAnswerContact |
925 | removeAnswerContact"/> |
926 | <require |
927 | @@ -1269,6 +1270,7 @@ |
928 | attributes=" |
929 | newQuestion |
930 | createQuestionFromBug |
931 | + canUserAlterAnswerContact |
932 | addAnswerContact |
933 | removeAnswerContact"/> |
934 | |
935 | @@ -1550,6 +1552,7 @@ |
936 | attributes=" |
937 | newQuestion |
938 | createQuestionFromBug |
939 | + canUserAlterAnswerContact |
940 | addAnswerContact |
941 | removeAnswerContact"/> |
942 | |
943 | @@ -1647,6 +1650,7 @@ |
944 | attributes=" |
945 | newQuestion |
946 | createQuestionFromBug |
947 | + canUserAlterAnswerContact |
948 | addAnswerContact |
949 | removeAnswerContact"/> |
950 | </class> |
951 | |
952 | === modified file 'lib/lp/registry/interfaces/person.py' |
953 | --- lib/lp/registry/interfaces/person.py 2011-05-03 13:38:30 +0000 |
954 | +++ lib/lp/registry/interfaces/person.py 2011-05-09 18:59:17 +0000 |
955 | @@ -15,7 +15,7 @@ |
956 | 'IObjectReassignment', |
957 | 'IPerson', |
958 | 'IPersonClaim', |
959 | - 'IPersonPublic', # Required for a monkey patch in interfaces/archive.py |
960 | + 'IPersonPublic', # Required for a monkey patch in interfaces/archive.py |
961 | 'IPersonSet', |
962 | 'IPersonSettings', |
963 | 'ISoftwareCenterAgentAPI', |
964 | @@ -731,7 +731,7 @@ |
965 | |
966 | sshkeys = exported( |
967 | CollectionField( |
968 | - title= _('List of SSH keys'), |
969 | + title=_('List of SSH keys'), |
970 | readonly=False, required=False, |
971 | value_type=Reference(schema=ISSHKey))) |
972 | |
973 | @@ -965,7 +965,7 @@ |
974 | hardware_submissions = exported(CollectionField( |
975 | title=_("Hardware submissions"), |
976 | readonly=True, required=False, |
977 | - value_type=Reference(schema=Interface))) # HWSubmission |
978 | + value_type=Reference(schema=Interface))) # HWSubmission |
979 | |
980 | # This is redefined from IPrivacy.private because the attribute is |
981 | # read-only. It is a summary of the team's visibility. |
982 | @@ -1030,7 +1030,7 @@ |
983 | """ |
984 | |
985 | @operation_parameters(name=TextLine(required=True)) |
986 | - @operation_returns_entry(Interface) # Really ISourcePackageRecipe. |
987 | + @operation_returns_entry(Interface) # Really ISourcePackageRecipe. |
988 | @export_read_operation() |
989 | @operation_for_version("beta") |
990 | def getRecipe(name): |
991 | @@ -1052,7 +1052,7 @@ |
992 | |
993 | @call_with(requester=REQUEST_USER) |
994 | @operation_parameters( |
995 | - archive=Reference(schema=Interface)) # Really IArchive |
996 | + archive=Reference(schema=Interface)) # Really IArchive |
997 | @export_write_operation() |
998 | @operation_for_version("beta") |
999 | def getArchiveSubscriptionURL(requester, archive): |
1000 | @@ -1302,6 +1302,10 @@ |
1001 | def getPathsToTeams(): |
1002 | """Return the paths to all teams related to this person.""" |
1003 | |
1004 | + @operation_parameters( |
1005 | + language=Reference(schema=ILanguage)) |
1006 | + @export_write_operation() |
1007 | + @operation_for_version("devel") |
1008 | def addLanguage(language): |
1009 | """Add a language to this person's preferences. |
1010 | |
1011 | @@ -1311,6 +1315,10 @@ |
1012 | already, nothing will happen. |
1013 | """ |
1014 | |
1015 | + @operation_parameters( |
1016 | + language=Reference(schema=ILanguage)) |
1017 | + @export_write_operation() |
1018 | + @operation_for_version("devel") |
1019 | def removeLanguage(language): |
1020 | """Remove a language from this person's preferences. |
1021 | |
1022 | @@ -1376,7 +1384,7 @@ |
1023 | |
1024 | @operation_parameters( |
1025 | name=TextLine(required=True, constraint=name_validator)) |
1026 | - @operation_returns_entry(Interface) # Really IArchive. |
1027 | + @operation_returns_entry(Interface) # Really IArchive. |
1028 | @export_read_operation() |
1029 | @operation_for_version("beta") |
1030 | def getPPAByName(name): |
1031 | @@ -1392,7 +1400,7 @@ |
1032 | name=TextLine(required=True, constraint=name_validator), |
1033 | displayname=TextLine(required=False), |
1034 | description=TextLine(required=False)) |
1035 | - @export_factory_operation(Interface, []) # Really IArchive. |
1036 | + @export_factory_operation(Interface, []) # Really IArchive. |
1037 | @operation_for_version("beta") |
1038 | def createPPA(name=None, displayname=None, description=None): |
1039 | """Create a PPA. |
1040 | @@ -1564,7 +1572,7 @@ |
1041 | """ |
1042 | |
1043 | @operation_parameters(status=copy_field(ITeamMembership['status'])) |
1044 | - @operation_returns_collection_of(Interface) # Really IPerson |
1045 | + @operation_returns_collection_of(Interface) # Really IPerson |
1046 | @export_read_operation() |
1047 | @operation_for_version("beta") |
1048 | def getMembersByStatus(status, orderby=None): |
1049 | |
1050 | === modified file 'lib/lp/registry/model/sourcepackage.py' |
1051 | --- lib/lp/registry/model/sourcepackage.py 2011-04-26 16:22:11 +0000 |
1052 | +++ lib/lp/registry/model/sourcepackage.py 2011-05-09 18:59:17 +0000 |
1053 | @@ -141,9 +141,11 @@ |
1054 | def getAnswerContactsForLanguage(self, language): |
1055 | """See `IQuestionTarget`.""" |
1056 | # Sourcepackages are supported by their distribtions too. |
1057 | - persons = self.distribution.getAnswerContactsForLanguage(language) |
1058 | - persons.update(QuestionTargetMixin.getAnswerContactsForLanguage( |
1059 | - self, language)) |
1060 | + persons = set( |
1061 | + self.distribution.getAnswerContactsForLanguage(language)) |
1062 | + persons.update( |
1063 | + set(QuestionTargetMixin.getAnswerContactsForLanguage( |
1064 | + self, language))) |
1065 | return sorted( |
1066 | [person for person in persons], key=attrgetter('displayname')) |
1067 | |
1068 | |
1069 | === modified file 'lib/lp/testing/factory.py' |
1070 | --- lib/lp/testing/factory.py 2011-05-06 15:05:51 +0000 |
1071 | +++ lib/lp/testing/factory.py 2011-05-09 18:59:17 +0000 |
1072 | @@ -1249,6 +1249,7 @@ |
1073 | if with_series_branches and naked_product is not None: |
1074 | series_branch_info = [] |
1075 | # Add some product series |
1076 | + |
1077 | def makeSeriesBranch(name, is_private=False): |
1078 | branch = self.makeBranch( |
1079 | name=name, |
1080 | @@ -1343,7 +1344,6 @@ |
1081 | related_package_branch_info, key=lambda branch_info: ( |
1082 | getattr(branch_info[1], 'name'))) |
1083 | |
1084 | - |
1085 | return ( |
1086 | reference_branch, |
1087 | related_series_branch_info, |
1088 | @@ -1997,21 +1997,25 @@ |
1089 | |
1090 | makeBlueprint = makeSpecification |
1091 | |
1092 | - def makeQuestion(self, target=None, title=None): |
1093 | + def makeQuestion(self, target=None, title=None, owner=None): |
1094 | """Create and return a new, arbitrary Question. |
1095 | |
1096 | :param target: The IQuestionTarget to make the question on. If one is |
1097 | not specified, an arbitrary product is created. |
1098 | :param title: The question title. If one is not provided, an |
1099 | arbitrary title is created. |
1100 | + :param owner: The owner of the question. If one is not provided, the |
1101 | + question target owner will be used. |
1102 | """ |
1103 | if target is None: |
1104 | target = self.makeProduct() |
1105 | if title is None: |
1106 | title = self.getUniqueString('title') |
1107 | + if owner is None: |
1108 | + owner = target.owner |
1109 | with person_logged_in(target.owner): |
1110 | question = target.newQuestion( |
1111 | - owner=target.owner, title=title, description='description') |
1112 | + owner=owner, title=title, description='description') |
1113 | return question |
1114 | |
1115 | def makeFAQ(self, target=None, title=None): |
1116 | @@ -2360,7 +2364,7 @@ |
1117 | changelog=changelogs.get('derived')) |
1118 | self.makeSourcePackagePublishingHistory( |
1119 | distroseries=derived_series, sourcepackagerelease=spr, |
1120 | - status = PackagePublishingStatus.PUBLISHED) |
1121 | + status=PackagePublishingStatus.PUBLISHED) |
1122 | |
1123 | if difference_type is not ( |
1124 | DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES): |
1125 | @@ -2371,7 +2375,7 @@ |
1126 | self.makeSourcePackagePublishingHistory( |
1127 | distroseries=derived_series.parent_series, |
1128 | sourcepackagerelease=spr, |
1129 | - status = PackagePublishingStatus.PUBLISHED) |
1130 | + status=PackagePublishingStatus.PUBLISHED) |
1131 | |
1132 | diff = getUtility(IDistroSeriesDifferenceSource).new( |
1133 | derived_series, source_package_name) |
1134 | @@ -2641,7 +2645,7 @@ |
1135 | bq = BuildQueue( |
1136 | job=recipe_build_job.job, lastscore=score, |
1137 | job_type=BuildFarmJobType.RECIPEBRANCHBUILD, |
1138 | - estimated_duration = timedelta(seconds=estimated_duration), |
1139 | + estimated_duration=timedelta(seconds=estimated_duration), |
1140 | virtualized=virtualized) |
1141 | store.add(bq) |
1142 | return bq |
1143 | @@ -2671,7 +2675,7 @@ |
1144 | |
1145 | records_inside_epoch = [] |
1146 | all_records = [] |
1147 | - for x in range(num_recent_records+num_records_outside_epoch): |
1148 | + for x in range(num_recent_records + num_records_outside_epoch): |
1149 | |
1150 | # We want some different source package names occasionally |
1151 | if not x % 3: |
1152 | @@ -2725,7 +2729,7 @@ |
1153 | if x < num_recent_records: |
1154 | naked_build.date_finished = ( |
1155 | now - timedelta( |
1156 | - days=epoch_days-1, |
1157 | + days=epoch_days - 1, |
1158 | hours=-x)) |
1159 | # And others is descending order |
1160 | else: |
This branch looks good. A few thoughts:
In addAnswerContact you might want to use lazr.restful. error.expose to
make the ValueError return a 400 instead of 500, because (as far as I
can tell), the error is on the client's part and not the server.
The five attributes/methods newQuestion, createQuestionF romBug, werContact, addAnswerContact, removeAnswerContact are registry/ configure. zcml. Perhaps
canUserAlterAns
used together several times in lib/lp/
they should be made into an interface so they can be treated as a unit.
Although I'm not a fan of it, the two existing parameters of testing/ factory. py were documented using the
makeQuestion in lib/lp/
JavaDoc style so you may want to do the same for the new parameter you
added.