Merge lp:~barry/launchpad/rnr-techdebt into lp:launchpad/db-devel
- rnr-techdebt
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Curtis Hovey |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~barry/launchpad/rnr-techdebt |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
3054 lines (+986/-884) 7 files modified
lib/lp/answers/doc/person.txt (+155/-138) lib/lp/answers/doc/projectgroup.txt (+45/-37) lib/lp/answers/doc/question.txt (+162/-132) lib/lp/answers/doc/questionsets.txt (+148/-139) lib/lp/answers/doc/questiontarget.txt (+266/-245) lib/lp/answers/doc/workflow.txt (+209/-193) lib/lp/answers/interfaces/questionreopening.py (+1/-0) |
To merge this branch: | bzr merge lp:~barry/launchpad/rnr-techdebt |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+18696@code.launchpad.net |
Commit message
Description of the change
Curtis Hovey (sinzui) wrote : | # |
Curtis Hovey (sinzui) wrote : | # |
Hi Barry.
I really appreciate your attention to this issue. I am happy you asked me
to review this because I am familiar with the i18n Answers issues
in the test suite. Your diff must pass this test to be safe to commit.
iconv -t ascii ~/Desktop/
Launchpad code is not i18n enabled, Answers and my editor is! I see
the Arabic portions of the diff right to left. That is very exciting
to see, but made scanning impossible, and when I look at the doctest...well
wow, I cannot read that. I asked Björn read it and his editor refused to
display the file.
You can use .encode .decode to make the words ascii, or use asciismash()
from somewhere in our code. I used both of these when I had to print the
words out. I got tired of that so I just showed the representation.
You can also use ellipsis if you need to hide an awkward part.
You will see a lot of duplication in these tests. I was young and naive
when I wrote them. You can consider deleting any duplication you see when
you fix the encoding issues. There is a small chance that there is a model
or view test that is the definitive test for many of these stories (At least
most of the answers tests are stories)
I see Spanish and Arabic in the diff, I was sure there was some French in
the tests I wrote, but you may not have touched those tests.
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
...
> +Language
> +--------
...
> +But Carlos has one.
>
> >>> carlos_raw = personset.
> >>> carlos = IQuestionsPerso
> >>> for question in carlos.
> - ... language=[english, spanish]):
> - ... [question.title, question.
> - [u'Problema al recompilar kernel con soporte smp (doble-n\xfacleo)',
> - u'es']
> -
> -
> -=== needs_attention ===
> -
> -The method accept a parameter called needs_attention which only selects
> -the questions that needs attention from the person. This includes questions
> -owned by the person in the ANSWERED or NEEDSINFO state. It also includes
> -questions on which the person requested for more information or gave an
> -answer and that are back in the OPEN state.
> + ... language=(english, spanish)):
> + ... print question.title, question.
> + Problema al recompilar kernel con soporte smp (doble-núcleo) es
...
> === renamed file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
...
> +Searching questions
> +======
> +
> +The IQuestionSet interface defines a searchQuestions() method that is used to
> +search for questions defined in any question target.
> +
> +
> +Search text
> +-----------
> +
> +The search_text parameter will return questions matching the query using the
> +regular full text algorithm.
>
> >>> for question in question_
> - ... print repr(question.
Barry Warsaw (barry) wrote : | # |
Hi Curtis, thanks for the review.
On Feb 05, 2010, at 01:34 PM, Curtis Hovey wrote:
>I really appreciate your attention to this issue. I am happy you asked me
>to review this because I am familiar with the i18n Answers issues
>in the test suite. Your diff must pass this test to be safe to commit.
>
> iconv -t ascii ~/Desktop/
>
>Launchpad code is not i18n enabled, Answers and my editor is! I see
>the Arabic portions of the diff right to left. That is very exciting
>to see, but made scanning impossible, and when I look at the doctest...well
>wow, I cannot read that. I asked Björn read it and his editor refused to
>display the file.
Yes, I got too clever because I use a real editor <wink> where all those funny
characters look so beautiful.
>You can use .encode .decode to make the words ascii, or use asciismash()
>from somewhere in our code. I used both of these when I had to print the
>words out. I got tired of that so I just showed the representation.
>You can also use ellipsis if you need to hide an awkward part.
ascii_smash() comes from canonical.encoding and it seems to do a reasonable
job of making iconv happy, while still retaining enough of the original text
for Spanish. I'll have to take ascii_smash()'s word for the Arabic
conversion.
(Aside: I just had to attach a screen shot showing claws-mail's beautiful
handling of the Arabic diff, which right-to-lefts the diff lines. Not sure if
the attachment will come through on the merge-proposal or not. ;)
>You will see a lot of duplication in these tests. I was young and naive
>when I wrote them. You can consider deleting any duplication you see when
>you fix the encoding issues. There is a small chance that there is a model
>or view test that is the definitive test for many of these stories (At least
>most of the answers tests are stories)
I didn't see much duplication among the doctests I touch. I was mostly just
reading through them to get a sense for where I can hack in the R&R stuff. I
didn't touch all the .txt files though (didn't any French for instance). I'll
keep on the lookout for any additional duplication as I work on the R&R stuff.
Here's an incremental diff.
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -168,12 +168,14 @@
But Carlos has one.
+ # Because not everyone uses a real editor <wink>
+ >>> from canonical.encoding import ascii_smash
>>> carlos_raw = personset.
>>> carlos = IQuestionsPerso
>>> for question in carlos.
... language=(english, spanish)):
- ... print question.title, question.
- Problema al recompilar kernel con soporte smp (doble-núcleo) es
+ ... print ascii_smash(
+ Problema al recompilar kernel con soporte smp (doble-nucleo) es
Questions needing attention
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
Curtis Hovey (sinzui) wrote : | # |
Thanks for cleaning up the tests. Sorry for not responding quickly to your reply.
Preview Diff
1 | === modified file 'lib/lp/answers/doc/person.txt' |
2 | --- lib/lp/answers/doc/person.txt 2009-12-24 01:41:54 +0000 |
3 | +++ lib/lp/answers/doc/person.txt 2010-02-10 15:24:19 +0000 |
4 | @@ -1,22 +1,28 @@ |
5 | -= Person and the Answer Tracker = |
6 | - |
7 | -== searchQuestions() == |
8 | - |
9 | -IQuestionsPerson defines a searchQuestions() method which can be used to |
10 | -select all or a subset of the questions in which the person is |
11 | -involved. This includes questions which the person created, is assigned |
12 | -to, is subscribed to, commented on, or answered. Various subsets can |
13 | -be selected by using the following criteria status, search_text and |
14 | -participation type. |
15 | - |
16 | - >>> from canonical.launchpad.interfaces import IPersonSet |
17 | +============================= |
18 | +People and the answer tracker |
19 | +============================= |
20 | + |
21 | +Sometimes you want to find out what questions a person is involved with. |
22 | + |
23 | + |
24 | +Searching |
25 | +========= |
26 | + |
27 | +IQuestionsPerson defines a searchQuestions() method which is used to select |
28 | +all, or a subset of, the questions in which a person is involved. This |
29 | +includes questions which the person created, is assigned to, is subscribed to, |
30 | +commented on, or answered. Various subsets can be selected by using the |
31 | +various search criteria. |
32 | + |
33 | + >>> from lp.registry.interfaces.person import IPersonSet |
34 | >>> from lp.answers.interfaces.questionsperson import IQuestionsPerson |
35 | >>> personset = getUtility(IPersonSet) |
36 | >>> foo_bar_raw = personset.getByEmail('foo.bar@canonical.com') |
37 | >>> foo_bar = IQuestionsPerson(foo_bar_raw) |
38 | |
39 | |
40 | -=== search_text === |
41 | +Search text |
42 | +----------- |
43 | |
44 | The search_text parameter will limit the questions to those matching |
45 | the query using the regular full text algorithm. |
46 | @@ -28,13 +34,14 @@ |
47 | Newly installed plug-in doesn't seem to be used Answered |
48 | |
49 | |
50 | -=== sort === |
51 | - |
52 | -When using the search_text criteria, the default is to sort the results |
53 | -by relevancy. One can use the sort parameter to change that. It takes |
54 | -one of the constant defined in the QuestionSort enumeration. |
55 | - |
56 | - >>> from canonical.launchpad.interfaces import QuestionSort |
57 | +Sorting |
58 | +------- |
59 | + |
60 | +When using the search_text criteria, the default is to sort the results by |
61 | +relevancy. One can use the sort parameter to change that. It takes one of |
62 | +the constant defined in the QuestionSort enumeration. |
63 | + |
64 | + >>> from lp.answers.interfaces.questionenums import QuestionSort |
65 | >>> for question in foo_bar.searchQuestions( |
66 | ... search_text='firefox', sort=QuestionSort.OLDEST_FIRST): |
67 | ... print question.id, question.title, question.status.title |
68 | @@ -42,8 +49,7 @@ |
69 | 6 Newly installed plug-in doesn't seem to be used Answered |
70 | 9 mailto: problem in webpage Solved |
71 | |
72 | -When no text search is done, the default sort order is |
73 | -QuestionSort.NEWEST_FIRST. |
74 | +When no text search is done, the default sort order is newest first. |
75 | |
76 | >>> for question in foo_bar.searchQuestions(): |
77 | ... print question.id, question.title, question.status.title |
78 | @@ -56,13 +62,13 @@ |
79 | 4 Firefox loses focus and gets stuck Open |
80 | |
81 | |
82 | -=== status === |
83 | - |
84 | -The last searches showed that by default, not all statuses are searched |
85 | -for by default (they excluded expired and invalid questions). The status |
86 | -parameter can be used to control the list of statuses to select: |
87 | - |
88 | - >>> from canonical.launchpad.interfaces import QuestionStatus |
89 | +Status |
90 | +------ |
91 | + |
92 | +As shown above, expired and invalid questions are not returned. The status |
93 | +parameter can be used to control the list of statuses to select. |
94 | + |
95 | + >>> from lp.answers.interfaces.questionenums import QuestionStatus |
96 | >>> for question in foo_bar.searchQuestions(status=QuestionStatus.INVALID): |
97 | ... print question.title, question.status.title |
98 | Firefox is slow and consumes too much RAM Invalid |
99 | @@ -70,23 +76,23 @@ |
100 | The status parameter can also take a list of statuses. |
101 | |
102 | >>> for question in foo_bar.searchQuestions( |
103 | - ... status=[QuestionStatus.SOLVED, QuestionStatus.INVALID]): |
104 | + ... status=(QuestionStatus.SOLVED, QuestionStatus.INVALID)): |
105 | ... print question.title, question.status.title |
106 | mailto: problem in webpage Solved |
107 | Firefox is slow and consumes too much RAM Invalid |
108 | |
109 | |
110 | -=== participation === |
111 | +Participation |
112 | +------------- |
113 | |
114 | -By default, any types of relationship to a question is considered by |
115 | -searchQuestions. This can customized through the participation |
116 | -parameter. It takes one or a list of constants from the |
117 | -QuestionParticipation enumeration. |
118 | +By default, any relationship between a person and a question is considered by |
119 | +searchQuestions. This can customized through the participation parameter. It |
120 | +takes one or a list of constants from the QuestionParticipation enumeration. |
121 | |
122 | To select only questions on which the person commented, the |
123 | -QuestionParticipation.COMMENTER is used: |
124 | +QuestionParticipation.COMMENTER is used. |
125 | |
126 | - >>> from canonical.launchpad.interfaces import QuestionParticipation |
127 | + >>> from lp.answers.interfaces.questionenums import QuestionParticipation |
128 | >>> for question in foo_bar.searchQuestions( |
129 | ... participation=QuestionParticipation.COMMENTER, status=None): |
130 | ... print question.title |
131 | @@ -96,8 +102,8 @@ |
132 | Installation of Java Runtime Environment for Mozilla |
133 | Newly installed plug-in doesn't seem to be used |
134 | |
135 | -QuestionParticipation.SUBSCRIBER will only select the questions to which |
136 | -the person is subscribed to: |
137 | +QuestionParticipation.SUBSCRIBER will only select the questions to which the |
138 | +person is subscribed. |
139 | |
140 | >>> for question in foo_bar.searchQuestions( |
141 | ... participation=QuestionParticipation.SUBSCRIBER, status=None): |
142 | @@ -105,7 +111,7 @@ |
143 | Slow system |
144 | Firefox is slow and consumes too much RAM |
145 | |
146 | -QuestionParticipation.OWNER selects the questions that the person created: |
147 | +QuestionParticipation.OWNER selects the questions that the person created. |
148 | |
149 | >>> for question in foo_bar.searchQuestions( |
150 | ... participation=QuestionParticipation.OWNER, status=None): |
151 | @@ -114,8 +120,8 @@ |
152 | Firefox loses focus and gets stuck |
153 | Firefox is slow and consumes too much RAM |
154 | |
155 | -QuestionParticipation.ANSWERER selects the questions for which the person |
156 | -was marked as the answerer: |
157 | +QuestionParticipation.ANSWERER selects the questions for which the person gave |
158 | +an answer. |
159 | |
160 | >>> for question in foo_bar.searchQuestions( |
161 | ... participation=QuestionParticipation.ANSWERER, status=None): |
162 | @@ -124,19 +130,18 @@ |
163 | Firefox is slow and consumes too much RAM |
164 | |
165 | QuestionParticipation.ASSIGNEE selects that questions which are assigned to |
166 | -the person: |
167 | +the person. |
168 | |
169 | - >>> for question in foo_bar.searchQuestions( |
170 | - ... participation=QuestionParticipation.ASSIGNEE, status=None): |
171 | - ... print question.title |
172 | + >>> list(foo_bar.searchQuestions( |
173 | + ... participation=QuestionParticipation.ASSIGNEE, status=None)) |
174 | + [] |
175 | |
176 | If a list of these constants is used, all of these participation types |
177 | -will be selected: |
178 | +will be selected. |
179 | |
180 | >>> for question in foo_bar.searchQuestions( |
181 | - ... participation=[ |
182 | - ... QuestionParticipation.OWNER, |
183 | - ... QuestionParticipation.ANSWERER], |
184 | + ... participation=(QuestionParticipation.OWNER, |
185 | + ... QuestionParticipation.ANSWERER), |
186 | ... status=None): |
187 | ... print question.title |
188 | mailto: problem in webpage |
189 | @@ -145,40 +150,41 @@ |
190 | Firefox is slow and consumes too much RAM |
191 | |
192 | |
193 | -=== language === |
194 | - |
195 | -By default, questions in all languages are included in the results. |
196 | -It is possible to filter questions by the language they were written |
197 | -in . One or a list of ILanguage object should be passed in the |
198 | -language parameter to specify the language filter. |
199 | - |
200 | - >>> from canonical.launchpad.interfaces import ILanguageSet |
201 | +Language |
202 | +-------- |
203 | + |
204 | +By default, questions in all languages are included in the results. It is |
205 | +possible to filter questions by the language they were written in. One or a |
206 | +sequence of ILanguage object can be passed in to specify the language filter. |
207 | + |
208 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
209 | >>> spanish = getUtility(ILanguageSet)['es'] |
210 | >>> english = getUtility(ILanguageSet)['en'] |
211 | |
212 | Foo bar doesn't have any questions written in Spanish. |
213 | |
214 | - >>> for question in foo_bar.searchQuestions(language=spanish): |
215 | - ... print question.title |
216 | - |
217 | -But carlos has one. |
218 | - |
219 | + >>> list(foo_bar.searchQuestions(language=spanish)) |
220 | + [] |
221 | + |
222 | +But Carlos has one. |
223 | + |
224 | + # Because not everyone uses a real editor <wink> |
225 | + >>> from canonical.encoding import ascii_smash |
226 | >>> carlos_raw = personset.getByName('carlos') |
227 | >>> carlos = IQuestionsPerson(carlos_raw) |
228 | >>> for question in carlos.searchQuestions( |
229 | - ... language=[english, spanish]): |
230 | - ... [question.title, question.language.code] |
231 | - [u'Problema al recompilar kernel con soporte smp (doble-n\xfacleo)', |
232 | - u'es'] |
233 | - |
234 | - |
235 | -=== needs_attention === |
236 | - |
237 | -The method accept a parameter called needs_attention which only selects |
238 | -the questions that needs attention from the person. This includes questions |
239 | -owned by the person in the ANSWERED or NEEDSINFO state. It also includes |
240 | -questions on which the person requested for more information or gave an |
241 | -answer and that are back in the OPEN state. |
242 | + ... language=(english, spanish)): |
243 | + ... print ascii_smash(question.title), question.language.code |
244 | + Problema al recompilar kernel con soporte smp (doble-nucleo) es |
245 | + |
246 | + |
247 | +Questions needing attention |
248 | +--------------------------- |
249 | + |
250 | +You can select only the questions that needs attention from a person. This |
251 | +includes questions owned by the person in the ANSWERED or NEEDSINFO state. It |
252 | +also includes questions on which the person requested more information or gave |
253 | +an answer and are back in the OPEN state. |
254 | |
255 | >>> for question in foo_bar.searchQuestions(needs_attention=True): |
256 | ... print question.status.title, question.owner.displayname, ( |
257 | @@ -187,70 +193,80 @@ |
258 | Needs information Foo Bar Slow system |
259 | |
260 | |
261 | -=== Combination === |
262 | +Search combinations |
263 | +------------------- |
264 | |
265 | -The returned sets of questions is the intersection of the sets delimited |
266 | -by each criteria: |
267 | +The results are the intersection of the sets delimited by each criteria. |
268 | |
269 | >>> for question in foo_bar.searchQuestions( |
270 | - ... search_text='firefox OR Java', status=QuestionStatus.ANSWERED, |
271 | + ... search_text='firefox OR Java', |
272 | + ... status=QuestionStatus.ANSWERED, |
273 | ... participation=QuestionParticipation.COMMENTER): |
274 | ... print question.title, question.status.title |
275 | Installation of Java Runtime Environment for Mozilla Answered |
276 | Newly installed plug-in doesn't seem to be used Answered |
277 | |
278 | |
279 | -== getQuestionLanguages() == |
280 | +Question languages |
281 | +================== |
282 | |
283 | IQuestionsPerson also defines a getQuestionLanguages() attribute which |
284 | contains the set of languages used by all of the questions in which this |
285 | person is involved. |
286 | |
287 | - >>> sorted(language.code for language in foo_bar.getQuestionLanguages()) |
288 | - [u'en'] |
289 | - |
290 | -This includes questions which the person owns. But also, questions that |
291 | -the user subscribed to. |
292 | - |
293 | - >>> from canonical.launchpad.interfaces import IQuestionSet |
294 | + >>> print ', '.join( |
295 | + ... sorted(language.code |
296 | + ... for language in foo_bar.getQuestionLanguages())) |
297 | + en |
298 | + |
299 | +This includes questions which the person owns, and questions that the user is |
300 | +subscribed to... |
301 | + |
302 | + >>> from lp.answers.interfaces.questioncollection import IQuestionSet |
303 | >>> pt_BR_question = getUtility(IQuestionSet).get(13) |
304 | >>> login('foo.bar@canonical.com') |
305 | >>> pt_BR_question.subscribe(foo_bar_raw) |
306 | <QuestionSubscription...> |
307 | |
308 | - >>> sorted(language.code for language in foo_bar.getQuestionLanguages()) |
309 | - [u'en', u'pt_BR'] |
310 | + >>> print ', '.join( |
311 | + ... sorted(language.code |
312 | + ... for language in foo_bar.getQuestionLanguages())) |
313 | + en, pt_BR |
314 | |
315 | -And also questions for which he's the answerer. |
316 | +...and questions for which he's the answerer... |
317 | |
318 | >>> es_question = getUtility(IQuestionSet).get(12) |
319 | >>> es_question.reject(foo_bar_raw, 'Reject question.') |
320 | <QuestionMessage...> |
321 | |
322 | - >>> sorted(language.code for language in foo_bar.getQuestionLanguages()) |
323 | - [u'en', u'es', u'pt_BR'] |
324 | + >>> print ', '.join( |
325 | + ... sorted(language.code |
326 | + ... for language in foo_bar.getQuestionLanguages())) |
327 | + en, es, pt_BR |
328 | |
329 | -As well, as question which are assigned to the user. |
330 | +...as well as questions which are assigned to the user... |
331 | |
332 | >>> pt_BR_question.assignee = carlos_raw |
333 | - >>> from canonical.database.sqlbase import flush_database_updates |
334 | - >>> flush_database_updates() |
335 | - |
336 | - >>> sorted(language.code for language in carlos.getQuestionLanguages()) |
337 | - [u'es', u'pt_BR'] |
338 | - |
339 | -And questions on which the user commented: |
340 | + >>> print ', '.join( |
341 | + ... sorted(language.code |
342 | + ... for language in carlos.getQuestionLanguages())) |
343 | + es, pt_BR |
344 | + |
345 | +...and questions on which the user commented. |
346 | |
347 | >>> en_question = getUtility(IQuestionSet).get(1) |
348 | >>> login('carlos@canonical.com') |
349 | >>> en_question.addComment(carlos_raw, 'A simple comment.') |
350 | <QuestionMessage...> |
351 | |
352 | - >>> sorted(language.code for language in carlos.getQuestionLanguages()) |
353 | - [u'en', u'es', u'pt_BR'] |
354 | - |
355 | - |
356 | -== getDirectAnswerQuestionTargets() == |
357 | + >>> print ', '.join( |
358 | + ... sorted(language.code |
359 | + ... for language in carlos.getQuestionLanguages())) |
360 | + en, es, pt_BR |
361 | + |
362 | + |
363 | +Direct subscriptions |
364 | +==================== |
365 | |
366 | IQuestionsPerson defines getDirectAnswerQuestionTargets that can be used to |
367 | retrieve a list of IQuestionTargets that a person subscribed himself to as an |
368 | @@ -261,9 +277,10 @@ |
369 | >>> no_priv.getDirectAnswerQuestionTargets() |
370 | [] |
371 | |
372 | - >>> from canonical.launchpad.interfaces import IProductSet |
373 | + >>> from lp.registry.interfaces.product import IProductSet |
374 | >>> firefox = getUtility(IProductSet).getByName("firefox") |
375 | - >>> # Answer contacts must speak a language |
376 | + |
377 | + # Answer contacts must speak a language |
378 | >>> no_priv_raw.addLanguage(english) |
379 | >>> firefox.addAnswerContact(no_priv_raw) |
380 | True |
381 | @@ -272,7 +289,9 @@ |
382 | ... print target.name |
383 | firefox |
384 | |
385 | -== getTeamAnswerQuestionTargets() == |
386 | + |
387 | +Indirect subscriptions |
388 | +====================== |
389 | |
390 | IQuestionsPerson defines getTeamAnswerQuestionTargets that retrieves a list of |
391 | IQuestionTargets that the person is subscribed to indirectly as an answer |
392 | @@ -283,20 +302,21 @@ |
393 | >>> no_priv_raw.inTeam(landscape_team) |
394 | True |
395 | |
396 | - >>> from canonical.launchpad.interfaces import IDistributionSet |
397 | + >>> from lp.registry.interfaces.distribution import IDistributionSet |
398 | >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu") |
399 | >>> landscape_team.addLanguage(english) |
400 | >>> ubuntu.addAnswerContact(landscape_team) |
401 | True |
402 | |
403 | - >>> sorted(target.name |
404 | - ... for target in no_priv.getTeamAnswerQuestionTargets()) |
405 | - [u'ubuntu'] |
406 | + >>> print ', '.join( |
407 | + ... sorted(target.name |
408 | + ... for target in no_priv.getTeamAnswerQuestionTargets())) |
409 | + ubuntu |
410 | |
411 | -Indirect team membership is also taken in consideration. For example, |
412 | -the Landscape Team joins the Translator Team. So targets for which the |
413 | -Translator team is an answer contact will be included in No Privileges |
414 | -Person's supported IQuestionTargets: |
415 | +Indirect team membership is also taken in consideration. For example, when |
416 | +the Landscape Team joins the Translator Team, targets for which the Translator |
417 | +team is an answer contact will be included in No Privileges Person's supported |
418 | +IQuestionTargets. |
419 | |
420 | >>> translator_team = personset.getByName('ubuntu-translators') |
421 | >>> no_priv_raw.inTeam(translator_team) |
422 | @@ -315,36 +335,33 @@ |
423 | >>> translator_team.addLanguage(english) |
424 | >>> evolution_package.addAnswerContact(translator_team) |
425 | True |
426 | - >>> sorted(target.name |
427 | - ... for target in no_priv.getTeamAnswerQuestionTargets()) |
428 | - [u'evolution', u'ubuntu'] |
429 | - |
430 | - |
431 | -== Deactivated pillars and *AnswerQuestionTargets() == |
432 | - |
433 | -getDirectAnswerQuestionTargets() and getTeamAnswerQuestionTargets() use |
434 | -a _getQuestionTargetsFromAnswerContacts() to build a distinct list of |
435 | -valid IQuestionTargets. It ensures that no deactivated pillars are in |
436 | -the list. |
437 | + >>> print ', '.join( |
438 | + ... sorted(target.name |
439 | + ... for target in no_priv.getTeamAnswerQuestionTargets())) |
440 | + evolution, ubuntu |
441 | + |
442 | + |
443 | +Deactivated pillars |
444 | +=================== |
445 | + |
446 | +Only valid IQuestionTargets are returned, ensuring that no deactivated pillars |
447 | +are in the results. |
448 | |
449 | If the Firefox project is deactivated, it is removed from the list of |
450 | supported projects. |
451 | |
452 | - >>> from canonical.launchpad.ftests import syncUpdate |
453 | - |
454 | >>> login('foo.bar@canonical.com') |
455 | >>> firefox.active = False |
456 | - >>> syncUpdate(firefox) |
457 | >>> sorted(target.name |
458 | ... for target in no_priv.getDirectAnswerQuestionTargets()) |
459 | [] |
460 | |
461 | -When the Firefox project is reactivated, the answer contact relationship |
462 | -is visible. It is important to preserve the continuity of the project in |
463 | -cases were we only want is deactivated for a short period. |
464 | +When the Firefox project is reactivated, the answer contact relationship is |
465 | +visible. These relationships are persistent for cases where we only want is |
466 | +deactivated for a short period. |
467 | |
468 | >>> firefox.active = True |
469 | - >>> syncUpdate(firefox) |
470 | - >>> sorted(target.name |
471 | - ... for target in no_priv.getDirectAnswerQuestionTargets()) |
472 | - [u'firefox'] |
473 | + >>> print ', '.join( |
474 | + ... sorted(target.name |
475 | + ... for target in no_priv.getDirectAnswerQuestionTargets())) |
476 | + firefox |
477 | |
478 | === renamed file 'lib/lp/answers/doc/project.txt' => 'lib/lp/answers/doc/projectgroup.txt' |
479 | --- lib/lp/answers/doc/project.txt 2009-03-24 12:43:49 +0000 |
480 | +++ lib/lp/answers/doc/projectgroup.txt 2010-02-10 15:24:19 +0000 |
481 | @@ -1,12 +1,15 @@ |
482 | -= Project and the Answer Tracker = |
483 | +=============================== |
484 | +Projects and the answer tracker |
485 | +=============================== |
486 | |
487 | -Although question cannot be filed directly against projects, IProject in |
488 | -Launchpad also provides the IQuestionCollection and |
489 | +Although questions cannot be filed directly against project groups (nee |
490 | +'Project'), IProject provides the IQuestionCollection and |
491 | ISearchableByQuestionOwner interfaces. |
492 | |
493 | >>> from canonical.launchpad.webapp.testing import verifyObject |
494 | - >>> from canonical.launchpad.interfaces import ( |
495 | - ... IProjectSet, ISearchableByQuestionOwner, IQuestionCollection) |
496 | + >>> from lp.registry.interfaces.project import IProjectSet |
497 | + >>> from lp.answers.interfaces.questioncollection import ( |
498 | + ... ISearchableByQuestionOwner, IQuestionCollection) |
499 | |
500 | >>> mozilla_project = getUtility(IProjectSet).getByName('mozilla') |
501 | >>> verifyObject(IQuestionCollection, mozilla_project) |
502 | @@ -14,37 +17,41 @@ |
503 | >>> verifyObject(ISearchableByQuestionOwner, mozilla_project) |
504 | True |
505 | |
506 | -== searchQuestions() == |
507 | - |
508 | -This means that it is possible to search for all questions filed against |
509 | -products in a project using the project searchQuestions() method. |
510 | - |
511 | - # Add a question to thunderbird. |
512 | - >>> from canonical.launchpad.interfaces import ILaunchBag, IProductSet |
513 | + |
514 | +Questions filed against project in a project group |
515 | +================================================== |
516 | + |
517 | +You can search for all questions filed against projects in a project using the |
518 | +project group's searchQuestions() method. |
519 | + |
520 | + >>> from lp.registry.interfaces.person import IPersonSet |
521 | + >>> from lp.registry.interfaces.product import IProductSet |
522 | + |
523 | >>> login('test@canonical.com') |
524 | >>> thunderbird = getUtility(IProductSet).getByName('thunderbird') |
525 | - >>> sample_person = getUtility(ILaunchBag).user |
526 | + >>> sample_person = getUtility(IPersonSet).getByName('name12') |
527 | >>> question = thunderbird.newQuestion( |
528 | - ... sample_person, "SVG attachments aren't displayed", |
529 | + ... sample_person, |
530 | + ... "SVG attachments aren't displayed ", |
531 | ... "It would be a nice feature if SVG attachments could be displayed" |
532 | - ... "inlined.") |
533 | + ... " inlined.") |
534 | |
535 | >>> for question in mozilla_project.searchQuestions(search_text='svg'): |
536 | ... print question.title, question.target.displayname |
537 | SVG attachments aren't displayed Mozilla Thunderbird |
538 | Problem showing the SVG demo on W3C site Mozilla Firefox |
539 | |
540 | -In the case were a Project has no Products, then we can expect no |
541 | -possible questions. |
542 | +In the case where a project group has no projects, there are no results. |
543 | |
544 | >>> aaa_project = getUtility(IProjectSet).getByName('aaa') |
545 | - >>> [q for question in aaa_project.searchQuestions()] |
546 | + >>> list(aaa_project.searchQuestions()) |
547 | [] |
548 | |
549 | -Questions can be searched by all the standard searchQuestions() parameters |
550 | -(consult questiontarget.txt for the full details.) |
551 | +Questions can be searched by all the standard searchQuestions() parameters. |
552 | +See questiontarget.txt for the full details. |
553 | |
554 | - >>> from canonical.launchpad.interfaces import QuestionStatus, QuestionSort |
555 | + >>> from lp.answers.interfaces.questionenums import ( |
556 | + ... QuestionSort, QuestionStatus) |
557 | >>> for question in mozilla_project.searchQuestions( |
558 | ... owner=sample_person, status=QuestionStatus.OPEN, |
559 | ... sort=QuestionSort.OLDEST_FIRST): |
560 | @@ -52,20 +59,21 @@ |
561 | Problem showing the SVG demo on W3C site Mozilla Firefox |
562 | SVG attachments aren't displayed Mozilla Thunderbird |
563 | |
564 | -== getQuestionLanguages() == |
565 | - |
566 | -The getQuestionLanguages() returns the set of languages that is used by |
567 | -all the questions in the project products. |
568 | - |
569 | - >>> sorted(language.code |
570 | - ... for language in mozilla_project.getQuestionLanguages()) |
571 | - [u'en', u'pt_BR'] |
572 | - |
573 | -(The firefox product has one question created in Brazilian Portuguese.) |
574 | - |
575 | -In the case where a Project has no Products, language questions will |
576 | -still return and empty set. |
577 | - |
578 | - >>> [language.code for language in aaa_project.getQuestionLanguages()] |
579 | + |
580 | +Languages |
581 | +========= |
582 | + |
583 | +getQuestionLanguages() returns the set of languages that is used by all the |
584 | +questions in the project group's projects. |
585 | + |
586 | + # The Firefox project group has one question created in Brazilian |
587 | + # Portuguese. |
588 | + >>> print ', '.join( |
589 | + ... sorted(language.code |
590 | + ... for language in mozilla_project.getQuestionLanguages())) |
591 | + en, pt_BR |
592 | + |
593 | +In the case where a project group has no projects, there are no results. |
594 | + |
595 | + >>> list(aaa_project.getQuestionLanguages()) |
596 | [] |
597 | - |
598 | |
599 | === modified file 'lib/lp/answers/doc/question.txt' |
600 | --- lib/lp/answers/doc/question.txt 2009-03-24 12:43:49 +0000 |
601 | +++ lib/lp/answers/doc/question.txt 2010-02-10 15:24:19 +0000 |
602 | @@ -1,21 +1,24 @@ |
603 | -= Launchpad Answer Tracker = |
604 | +======================== |
605 | +Launchpad Answer Tracker |
606 | +======================== |
607 | |
608 | -Launchpad includes an Answer Tracker where users can post questions |
609 | -(usually about problems they encounter with projects) and other can |
610 | -answer them.) Questions are created and accessed using the |
611 | -IQuestionTarget interface. This interface is available on Products, |
612 | -Distributions and DistributionSourcePackages. |
613 | +Launchpad includes an Answer Tracker where users can post questions, usually |
614 | +about problems they encounter with projects, and other people can answer them. |
615 | +Questions are created and accessed using the IQuestionTarget interface. This |
616 | +interface is available on Products, Distributions and |
617 | +DistributionSourcePackages. |
618 | |
619 | >>> login('test@canonical.com') |
620 | |
621 | >>> from canonical.launchpad.webapp.testing import verifyObject |
622 | - >>> from canonical.launchpad.interfaces import ( |
623 | - ... IDistributionSet, IProductSet, IPersonSet, IQuestionTarget) |
624 | + >>> from lp.answers.interfaces.questiontarget import IQuestionTarget |
625 | + >>> from lp.registry.interfaces.product import IProductSet |
626 | |
627 | >>> firefox = getUtility(IProductSet)['firefox'] |
628 | >>> verifyObject(IQuestionTarget, firefox) |
629 | True |
630 | |
631 | + >>> from lp.registry.interfaces.distribution import IDistributionSet |
632 | >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
633 | >>> verifyObject(IQuestionTarget, ubuntu) |
634 | True |
635 | @@ -24,9 +27,9 @@ |
636 | >>> verifyObject(IQuestionTarget, evolution_in_ubuntu) |
637 | True |
638 | |
639 | -Although Distribution series do not implement the IQuestionTarget |
640 | -interface, it is possible to adapt one to it. (The adapter is actually |
641 | -the distroseries's distribution.) |
642 | +Although distribution series do not implement the IQuestionTarget interface, |
643 | +it is possible to adapt one to it. The adapter is actually the distroseries's |
644 | +distribution. |
645 | |
646 | >>> ubuntu_warty = ubuntu.getSeries('warty') |
647 | >>> IQuestionTarget.providedBy(ubuntu_warty) |
648 | @@ -56,29 +59,33 @@ |
649 | You create a new question by calling the newQuestion() method of an |
650 | IQuestionTarget attribute. |
651 | |
652 | + >>> from lp.registry.interfaces.person import IPersonSet |
653 | >>> sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com') |
654 | >>> firefox_question = firefox.newQuestion( |
655 | ... sample_person, "Firefox question", "Unable to use Firefox") |
656 | |
657 | -(The complete IQuestionTarget interface is documented in |
658 | -../interfaces/ftests/questiontarget.txt.) |
659 | - |
660 | -== Official usage == |
661 | - |
662 | -A product or distribution may be offically supported by the community |
663 | -using the Answer Tracker. This status is set by the official_answers |
664 | -attribute on the IProduct and IDistribution. |
665 | +The complete IQuestionTarget interface is documented in questiontarget.txt. |
666 | + |
667 | + |
668 | +Official usage |
669 | +============== |
670 | + |
671 | +A product or distribution may be officially supported by the community using |
672 | +the Answer Tracker. This status is set by the official_answers attribute on |
673 | +the IProduct and IDistribution. |
674 | |
675 | >>> ubuntu.official_answers |
676 | True |
677 | >>> firefox.official_answers |
678 | True |
679 | |
680 | -== IQuestion == |
681 | - |
682 | -Questions are manipulated through the IQuestion interface: |
683 | - |
684 | - >>> from canonical.launchpad.interfaces import IQuestion |
685 | + |
686 | +IQuestion interface |
687 | +=================== |
688 | + |
689 | +Questions are manipulated through the IQuestion interface. |
690 | + |
691 | + >>> from lp.answers.interfaces.question import IQuestion |
692 | >>> from zope.security.proxy import removeSecurityProxy |
693 | |
694 | # The complete interface is not necessarily available to the |
695 | @@ -88,21 +95,27 @@ |
696 | |
697 | The person who submitted the question is available in the owner field. |
698 | |
699 | - >>> firefox_question.owner == sample_person |
700 | - True |
701 | + >>> firefox_question.owner |
702 | + <Person at ... name12 (Sample Person)> |
703 | |
704 | When the question is created, the owner is added to the question's |
705 | -subscribers: |
706 | - |
707 | - >>> sample_person in [s.person for s in firefox_question.subscriptions] |
708 | - True |
709 | - |
710 | -The question status is 'Open': |
711 | - |
712 | - >>> firefox_question.status.title |
713 | - 'Open' |
714 | - |
715 | -And the creation time is recorded in the datecreated attribute: |
716 | +subscribers. |
717 | + |
718 | + >>> from operator import attrgetter |
719 | + >>> def print_subscribers(question): |
720 | + ... people = [subscription.person |
721 | + ... for subscription in question.subscriptions] |
722 | + ... for person in sorted(people, key=attrgetter('name')): |
723 | + ... print person.displayname |
724 | + >>> print_subscribers(firefox_question) |
725 | + Sample Person |
726 | + |
727 | +The question status is 'Open'. |
728 | + |
729 | + >>> print firefox_question.status.title |
730 | + Open |
731 | + |
732 | +The question has a creation time. |
733 | |
734 | >>> from datetime import datetime, timedelta |
735 | >>> from pytz import UTC |
736 | @@ -110,11 +123,10 @@ |
737 | >>> now - firefox_question.datecreated < timedelta(seconds=5) |
738 | True |
739 | |
740 | -The target onto which the question was created is available through the |
741 | -'target' attribute: |
742 | +The target onto which the question was created is also available. |
743 | |
744 | - >>> firefox_question.target == firefox |
745 | - True |
746 | + >>> print firefox_question.target.displayname |
747 | + Mozilla Firefox |
748 | |
749 | It is also possible to adapt a question to its IQuestionTarget. |
750 | |
751 | @@ -163,66 +175,69 @@ |
752 | firefox |
753 | |
754 | |
755 | -== Subscriptions and Notifications == |
756 | +Subscriptions and notifications |
757 | +=============================== |
758 | |
759 | Whenever a question is created or changed, email notifications will be |
760 | -sent. To receive such notification, one can subscribe to the bug using |
761 | +sent. To receive such notification, one can subscribe to the bug using |
762 | the subscribe() method. |
763 | |
764 | >>> no_priv = getUtility(IPersonSet).getByName('no-priv') |
765 | >>> subscription = firefox_question.subscribe(no_priv) |
766 | |
767 | -The list of subscriptions is available in the subscriptions attribute. |
768 | -In the current case, the subscribers will include the owner |
769 | -('Sample Person') and the newly subscribed person. |
770 | +The subscribers include the owner and the newly subscribed person. |
771 | |
772 | - >>> [s.person.displayname for s in firefox_question.subscriptions] |
773 | - [u'Sample Person', u'No Privileges Person'] |
774 | + >>> print_subscribers(firefox_question) |
775 | + Sample Person |
776 | + No Privileges Person |
777 | |
778 | The getDirectSubscribers() method returns a sorted list of subscribers. |
779 | This method iterates like the NotificationRecipientSet returned by the |
780 | getDirectRecipients() method. |
781 | |
782 | - >>> [person.displayname |
783 | - ... for person in firefox_question.getDirectSubscribers()] |
784 | - [u'No Privileges Person', u'Sample Person'] |
785 | + >>> for person in firefox_question.getDirectSubscribers(): |
786 | + ... print person.displayname |
787 | + No Privileges Person |
788 | + Sample Person |
789 | |
790 | To remove a person from the subscriptions list, we use the unsubscribe() |
791 | method. |
792 | |
793 | >>> firefox_question.unsubscribe(no_priv) |
794 | - >>> [s.person.displayname for s in firefox_question.subscriptions] |
795 | - [u'Sample Person'] |
796 | + >>> print_subscribers(firefox_question) |
797 | + Sample Person |
798 | |
799 | -The persons who are on the subscription list are said to be directly |
800 | -subscribed to the question. They explicitly choose to get notifications |
801 | -about that particular question. This list of persons is available through |
802 | -the getDirectRecipients() method. |
803 | +The people on the subscription list are said to be directly subscribed to the |
804 | +question. They explicitly chose to get notifications about that particular |
805 | +question. This list of people is available through the getDirectRecipients() |
806 | +method. |
807 | |
808 | >>> subscribers = firefox_question.getDirectRecipients() |
809 | |
810 | That method returns an INotificationRecipientSet, containing the direct |
811 | -subscribers along the rationale for contacting them: |
812 | +subscribers along with the rationale for contacting them. |
813 | |
814 | >>> from canonical.launchpad.interfaces import INotificationRecipientSet |
815 | >>> verifyObject(INotificationRecipientSet, subscribers) |
816 | True |
817 | - >>> [person.displayname for person in subscribers] |
818 | - [u'Sample Person'] |
819 | - >>> subscribers.getReason(sample_person) |
820 | - ('You received this question notification because you are a direct |
821 | - subscriber of the question.', 'Subscriber') |
822 | + >>> def print_reason(subscribers): |
823 | + ... for person in subscribers: |
824 | + ... text, header = subscribers.getReason(person) |
825 | + ... print header, person.displayname, text |
826 | + >>> print_reason(subscribers) |
827 | + Subscriber Sample Person You received this question notification |
828 | + because you are a direct subscriber of the question. |
829 | |
830 | -There is also a list of 'indirect' subscribers to the question. These |
831 | -are persons that didn't explicitly subscribed to the question, but that |
832 | -will receive notifications for other reason. Answer contacts for the |
833 | -question target are part of the indirect subscribers list. |
834 | +There is also a list of 'indirect' subscribers to the question. These are |
835 | +people that didn't explicitly subscribe to the question, but that will receive |
836 | +notifications for other reasons. Answer contacts for the question target are |
837 | +part of the indirect subscribers list. |
838 | |
839 | # There are no answer contacts on the firefox product. |
840 | - >>> [person.displayname |
841 | - ... for person in firefox_question.getIndirectRecipients()] |
842 | + >>> list(firefox_question.getIndirectRecipients()) |
843 | [] |
844 | - >>> from canonical.launchpad.interfaces import ILanguageSet |
845 | + |
846 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
847 | >>> english = getUtility(ILanguageSet)['en'] |
848 | >>> no_priv.addLanguage(english) |
849 | >>> firefox.addAnswerContact(no_priv) |
850 | @@ -231,16 +246,14 @@ |
851 | >>> indirect_subscribers = firefox_question.getIndirectRecipients() |
852 | >>> verifyObject(INotificationRecipientSet, indirect_subscribers) |
853 | True |
854 | - >>> [person.displayname for person in indirect_subscribers] |
855 | - [u'No Privileges Person'] |
856 | - >>> indirect_subscribers.getReason(no_priv) |
857 | - (u'You received this question notification because you are an answer |
858 | - contact for Mozilla Firefox.', |
859 | - u'Answer Contact (Mozilla Firefox)') |
860 | + >>> print_reason(indirect_subscribers) |
861 | + Answer Contact (Mozilla Firefox) No Privileges Person |
862 | + You received this question notification because you are an answer |
863 | + contact for Mozilla Firefox. |
864 | |
865 | -There is a special case for when the question's is associated to a |
866 | -source package. The answer contacts for both the distribution and the |
867 | -source package are part of the indirect subscribers list. |
868 | +There is a special case for when the question is associated with a source |
869 | +package. The answer contacts for both the distribution and the source package |
870 | +are part of the indirect subscribers list. |
871 | |
872 | # Let's register some answer contacts for the distribution and |
873 | # the package. |
874 | @@ -257,64 +270,80 @@ |
875 | >>> package_question = evolution_in_ubuntu.newQuestion( |
876 | ... sample_person, 'Upgrading to Evolution 1.4 breaks plug-ins', |
877 | ... "The FnordsHighlighter plug-in doesn't work after upgrade.") |
878 | - >>> [s.person.displayname for s in package_question.subscriptions] |
879 | - [u'Sample Person'] |
880 | + |
881 | + >>> print_subscribers(package_question) |
882 | + Sample Person |
883 | + |
884 | >>> indirect_subscribers = package_question.getIndirectRecipients() |
885 | - >>> [person.displayname for person in indirect_subscribers] |
886 | - [u'No Privileges Person', u'Ubuntu Team'] |
887 | - >>> indirect_subscribers.getReason(ubuntu_team) |
888 | - (u'You received this question notification because you are a member of |
889 | - Ubuntu Team, which is an answer contact for Ubuntu.', |
890 | - u'Answer Contact (ubuntu) @ubuntu-team') |
891 | - >>> indirect_subscribers.getReason(no_priv) |
892 | - (u'You received this question notification because you are an answer |
893 | - contact for evolution in ubuntu.', |
894 | - u'Answer Contact (evolution in ubuntu)') |
895 | + >>> for person in indirect_subscribers: |
896 | + ... print person.displayname |
897 | + No Privileges Person |
898 | + Ubuntu Team |
899 | + |
900 | + >>> text, header = indirect_subscribers.getReason(ubuntu_team) |
901 | + >>> print header, text |
902 | + Answer Contact (ubuntu) @ubuntu-team |
903 | + You received this question notification because you are a member of |
904 | + Ubuntu Team, which is an answer contact for Ubuntu. |
905 | |
906 | The question's assignee is also part of the indirect subscription list: |
907 | |
908 | - >>> login('foo.bar@canonical.com') |
909 | + >>> login('admin@canonical.com') |
910 | >>> package_question.assignee = getUtility(IPersonSet).getByName('name16') |
911 | >>> indirect_subscribers = package_question.getIndirectRecipients() |
912 | - >>> [person.displayname for person in indirect_subscribers] |
913 | - [u'Foo Bar', u'No Privileges Person', u'Ubuntu Team'] |
914 | - >>> indirect_subscribers.getReason(package_question.assignee) |
915 | - ('You received this question notification because you are the assignee |
916 | - for this question.', |
917 | - 'Assignee') |
918 | + >>> for person in indirect_subscribers: |
919 | + ... print person.displayname |
920 | + Foo Bar |
921 | + No Privileges Person |
922 | + Ubuntu Team |
923 | + |
924 | + >>> text, header = indirect_subscribers.getReason( |
925 | + ... package_question.assignee) |
926 | + >>> print header, text |
927 | + Assignee |
928 | + You received this question notification because you are the assignee for |
929 | + this question. |
930 | |
931 | The getIndirectSubscribers() method iterates like the getIndirectRecipients() |
932 | method, but it returns a sorted list instead of a NotificationRecipientSet. |
933 | It too contains the question assignee. |
934 | |
935 | >>> indirect_subscribers = package_question.getIndirectSubscribers() |
936 | - >>> [person.displayname for person in indirect_subscribers] |
937 | - [u'Foo Bar', u'No Privileges Person', u'Ubuntu Team'] |
938 | + >>> for person in indirect_subscribers: |
939 | + ... print person.displayname |
940 | + Foo Bar |
941 | + No Privileges Person |
942 | + Ubuntu Team |
943 | |
944 | -Notifications are sent to the list of direct and indirect subscribers. |
945 | -The notification recipients list can be obtained by using the |
946 | -getRecipients() method. |
947 | +Notifications are sent to the list of direct and indirect subscribers. The |
948 | +notification recipients list can be obtained by using the getRecipients() |
949 | +method. |
950 | |
951 | >>> login('no-priv@canonical.com') |
952 | >>> subscribers = firefox_question.getRecipients() |
953 | >>> verifyObject(INotificationRecipientSet, subscribers) |
954 | True |
955 | - >>> [person.displayname for person in subscribers] |
956 | - [u'No Privileges Person', u'Sample Person'] |
957 | - |
958 | -(More documentation on the question notifications can be found in |
959 | -'answer-tracker-notifications.txt'.) |
960 | - |
961 | - |
962 | -== Workflow == |
963 | + >>> for person in subscribers: |
964 | + ... print person.displayname |
965 | + No Privileges Person |
966 | + Sample Person |
967 | + |
968 | +More documentation on the question notifications can be found in |
969 | +`answer-tracker-notifications.txt`. |
970 | + |
971 | + |
972 | +Workflow |
973 | +======== |
974 | |
975 | A question status should not be manipulated directly but through the |
976 | workflow methods. |
977 | |
978 | The complete question workflow is documented in |
979 | -'answer-tracker-workflow.txt'. |
980 | - |
981 | -== Bug Linking == |
982 | +`answer-tracker-workflow.txt`. |
983 | + |
984 | + |
985 | +Bug linking |
986 | +=========== |
987 | |
988 | Question implements the IBugLinkTarget interface which makes it possible |
989 | to link bug report to question. |
990 | @@ -323,13 +352,13 @@ |
991 | >>> verifyObject(IBugLinkTarget, firefox_question) |
992 | True |
993 | |
994 | -(See ../interfaces/ftests/buglinktarget.txt for the documentation and |
995 | -test of the IBugLinkTarget interface.) |
996 | - |
997 | -When a bug is linked to a question, the question's owner is subscribed to |
998 | -the bug. |
999 | - |
1000 | - >>> from canonical.launchpad.interfaces import IBugSet |
1001 | +See ../../bugs/tests/buglinktarget.txt for the documentation and test of the |
1002 | +IBugLinkTarget interface. |
1003 | + |
1004 | +When a bug is linked to a question, the question's owner is subscribed to the |
1005 | +bug. |
1006 | + |
1007 | + >>> from lp.bugs.interfaces.bug import IBugSet |
1008 | >>> bug7 = getUtility(IBugSet).get(7) |
1009 | >>> bug7.isSubscribed(firefox_question.owner) |
1010 | False |
1011 | @@ -338,33 +367,34 @@ |
1012 | >>> bug7.isSubscribed(firefox_question.owner) |
1013 | True |
1014 | |
1015 | -When the link is removed, the owner is unsubscribed: |
1016 | +When the link is removed, the owner is unsubscribed. |
1017 | |
1018 | >>> firefox_question.unlinkBug(bug7) |
1019 | <QuestionBug...> |
1020 | >>> bug7.isSubscribed(firefox_question.owner) |
1021 | False |
1022 | |
1023 | -== Unsupported Questions == |
1024 | - |
1025 | -While a Person may ask questions in his language of choice, that does |
1026 | -not mean that indirect subscribers (Answer Contacts) to an |
1027 | -IQuestionTarget speak that language. IQuestionTarget can return a list |
1028 | -Questions in languages that are not supported |
1029 | + |
1030 | +Unsupported questions |
1031 | +===================== |
1032 | + |
1033 | +While a Person may ask questions in his language of choice, that does not mean |
1034 | +that indirect subscribers (Answer Contacts) to an IQuestionTarget speak that |
1035 | +language. IQuestionTarget can return a list Questions in languages that are |
1036 | +not supported. |
1037 | |
1038 | >>> unsupported_questions = firefox.searchQuestions(unsupported=True) |
1039 | - >>> sorted([question.title for question in unsupported_questions]) |
1040 | + >>> sorted(question.title for question in unsupported_questions) |
1041 | [u'Problemas de Impress\xe3o no Firefox'] |
1042 | |
1043 | >>> unsupported_questions = evolution_in_ubuntu.searchQuestions( |
1044 | ... unsupported=True) |
1045 | - >>> sorted([question.title for question in unsupported_questions]) |
1046 | + >>> sorted(question.title for question in unsupported_questions) |
1047 | [] |
1048 | |
1049 | >>> warty_question_target = IQuestionTarget(ubuntu_warty) |
1050 | >>> unsupported_questions = warty_question_target.searchQuestions( |
1051 | ... unsupported=True) |
1052 | - >>> sorted([question.title for question in unsupported_questions]) |
1053 | + >>> sorted(question.title for question in unsupported_questions) |
1054 | [u'Problema al recompilar kernel con soporte smp (doble-n\xfacleo)', |
1055 | u'\u0639\u0643\u0633 \u0627\u0644\u062a\u063a\u064a\u064a\u0631...] |
1056 | - |
1057 | |
1058 | === renamed file 'lib/lp/answers/doc/utility.txt' => 'lib/lp/answers/doc/questionsets.txt' |
1059 | --- lib/lp/answers/doc/utility.txt 2009-03-24 12:43:49 +0000 |
1060 | +++ lib/lp/answers/doc/questionsets.txt 2010-02-10 15:24:19 +0000 |
1061 | @@ -1,69 +1,75 @@ |
1062 | -= Answer Tracker Utility: IQuestionSet = |
1063 | +==================== |
1064 | +Question collections |
1065 | +==================== |
1066 | |
1067 | -There is an IQuestionSet utility that can be use to retrieve and search |
1068 | -for question whatever the target they were created in. |
1069 | +The IQuestionSet utility is used to retrieve and search for questions no |
1070 | +matter which question target they were created for. |
1071 | |
1072 | >>> from canonical.launchpad.webapp.testing import verifyObject |
1073 | - >>> from canonical.launchpad.interfaces import IQuestionSet |
1074 | + >>> from lp.answers.interfaces.questioncollection import IQuestionSet |
1075 | >>> question_set = getUtility(IQuestionSet) |
1076 | >>> verifyObject(IQuestionSet, question_set) |
1077 | True |
1078 | |
1079 | |
1080 | -== get() == |
1081 | +Retrieving questions |
1082 | +==================== |
1083 | |
1084 | -The get() method can be used to get a question with a specific id: |
1085 | +The get() method can be used to retrieve a question with a specific id. |
1086 | |
1087 | >>> question_one = question_set.get(1) |
1088 | - >>> question_one.title |
1089 | - u'Firefox cannot render Bank Site' |
1090 | + >>> print question_one.title |
1091 | + Firefox cannot render Bank Site |
1092 | |
1093 | -If no question exists, a default value is returned: |
1094 | +If no question exists, a default value is returned. |
1095 | |
1096 | >>> default = object() |
1097 | >>> question_nonexistant = question_set.get(123456, default=default) |
1098 | >>> question_nonexistant is default |
1099 | True |
1100 | |
1101 | -If no default value is given, None is returned: |
1102 | - |
1103 | - >>> question_set.get(123456) is None |
1104 | - True |
1105 | - |
1106 | - |
1107 | -== searchQuestions() == |
1108 | - |
1109 | -IQuestionSet also defines a searchQuestions() method that can be used to |
1110 | -search for questions defined in any products or distributions (in fact, |
1111 | -in any context that allows questions to be defined). Two search criteria |
1112 | -are defined search_text and status. |
1113 | - |
1114 | - |
1115 | -=== search_text === |
1116 | - |
1117 | -The search_text parameter will limit the questions to those matching |
1118 | -the query using the regular full text algorithm. |
1119 | - |
1120 | +If no default value is given, None is returned. |
1121 | + |
1122 | + >>> print question_set.get(123456) |
1123 | + None |
1124 | + |
1125 | + |
1126 | +Searching questions |
1127 | +=================== |
1128 | + |
1129 | +The IQuestionSet interface defines a searchQuestions() method that is used to |
1130 | +search for questions defined in any question target. |
1131 | + |
1132 | + |
1133 | +Search text |
1134 | +----------- |
1135 | + |
1136 | +The search_text parameter will return questions matching the query using the |
1137 | +regular full text algorithm. |
1138 | + |
1139 | + # Because not everyone uses a real editor <wink> |
1140 | + >>> from canonical.encoding import ascii_smash |
1141 | >>> for question in question_set.searchQuestions(search_text='firefox'): |
1142 | - ... print repr(question.title), question.target.displayname |
1143 | - u'Problemas de Impress\xe3o no Firefox' Mozilla Firefox |
1144 | - u'Firefox loses focus and gets stuck' Mozilla Firefox |
1145 | - u'Firefox cannot render Bank Site' Mozilla Firefox |
1146 | - u'mailto: problem in webpage' mozilla-firefox in ubuntu |
1147 | - u"Newly installed plug-in doesn't seem to be used" Mozilla Firefox |
1148 | - u'Problem showing the SVG demo on W3C site' Mozilla Firefox |
1149 | - u'\u0639\u0643\u0633 ...' Ubuntu |
1150 | - |
1151 | - |
1152 | -=== status === |
1153 | - |
1154 | -By default, expired and invalid questions are not searched for. The |
1155 | -status parameter can be used to select the questions in the status |
1156 | -you are interested in. |
1157 | - |
1158 | - >>> from canonical.launchpad.interfaces import QuestionStatus |
1159 | + ... print ascii_smash(question.title), question.target.displayname |
1160 | + Problemas de Impressao no Firefox Mozilla Firefox |
1161 | + Firefox loses focus and gets stuck Mozilla Firefox |
1162 | + Firefox cannot render Bank Site Mozilla Firefox |
1163 | + mailto: problem in webpage mozilla-firefox in ubuntu |
1164 | + Newly installed plug-in doesn't seem to be used Mozilla Firefox |
1165 | + Problem showing the SVG demo on W3C site Mozilla Firefox |
1166 | + AINKAFSEEN ALEFLAMTEHGHAINYEHYEHREHALEFTEH ... Ubuntu |
1167 | + |
1168 | + |
1169 | +Status |
1170 | +------ |
1171 | + |
1172 | +By default, expired and invalid questions are not searched for. The status |
1173 | +parameter can be used to select the questions in the status you are interested |
1174 | +in. |
1175 | + |
1176 | + >>> from lp.answers.interfaces.questionenums import QuestionStatus |
1177 | >>> for question in question_set.searchQuestions( |
1178 | - ... status=QuestionStatus.INVALID): |
1179 | + ... status=QuestionStatus.INVALID): |
1180 | ... print question.title, question.status.title, ( |
1181 | ... question.target.displayname) |
1182 | Firefox is slow and consumes too much RAM Invalid mozilla-firefox in ubuntu |
1183 | @@ -78,111 +84,118 @@ |
1184 | Firefox is slow and consumes too much RAM Invalid mozilla-firefox in ubuntu |
1185 | |
1186 | |
1187 | -=== language === |
1188 | +Language |
1189 | +-------- |
1190 | |
1191 | The language parameter can be used to select only questions written in a |
1192 | particular language. |
1193 | |
1194 | - >>> from canonical.launchpad.interfaces import ILanguageSet |
1195 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
1196 | >>> spanish = getUtility(ILanguageSet)['es'] |
1197 | >>> for t in question_set.searchQuestions(language=spanish): |
1198 | - ... print t.title.encode('us-ascii', 'backslashreplace') |
1199 | - Problema al recompilar kernel con soporte smp (doble-n\xfacleo) |
1200 | - |
1201 | -=== Combination === |
1202 | - |
1203 | -The returned sets of questions is the intersection of the sets delimited |
1204 | -by each criteria: |
1205 | + ... print ascii_smash(t.title) |
1206 | + Problema al recompilar kernel con soporte smp (doble-nucleo) |
1207 | + |
1208 | + |
1209 | +Combinations |
1210 | +------------ |
1211 | + |
1212 | +The returned set of questions is the intersection of the sets delimited by |
1213 | +each criteria. |
1214 | |
1215 | >>> for question in question_set.searchQuestions( |
1216 | ... search_text='firefox', |
1217 | - ... status=[QuestionStatus.OPEN, QuestionStatus.INVALID]): |
1218 | - ... print repr(question.title), question.status.title, ( |
1219 | + ... status=(QuestionStatus.OPEN, QuestionStatus.INVALID)): |
1220 | + ... print ascii_smash(question.title), question.status.title, ( |
1221 | ... question.target.displayname) |
1222 | - u'Problemas de Impress\xe3o no Firefox' Open Mozilla Firefox |
1223 | - u'Firefox is slow and consumes too much RAM' Invalid mozilla-firefox in ubuntu |
1224 | - u'Firefox loses focus and gets stuck' Open Mozilla Firefox |
1225 | - u'Firefox cannot render Bank Site' Open Mozilla Firefox |
1226 | - u'Problem showing the SVG demo on W3C site' Open Mozilla Firefox |
1227 | - u'\u0639\u0643\u0633 ...' Open Ubuntu |
1228 | - |
1229 | - |
1230 | -=== Sort Order === |
1231 | - |
1232 | -When using the search_text criteria, the default is to sort the results |
1233 | -by relevancy. One can use the sort parameter to change that. It takes |
1234 | -one of the constant defined in the QuestionSort enumeration. |
1235 | - |
1236 | - >>> from canonical.launchpad.interfaces import QuestionSort |
1237 | + Problemas de Impressao no Firefox Open Mozilla Firefox |
1238 | + Firefox is slow and consumes too much RAM Invalid mozilla-firefox in ubuntu |
1239 | + Firefox loses focus and gets stuck Open Mozilla Firefox |
1240 | + Firefox cannot render Bank Site Open Mozilla Firefox |
1241 | + Problem showing the SVG demo on W3C site Open Mozilla Firefox |
1242 | + AINKAFSEEN ALEFLAMTEHGHAINYEHYEHREHALEFTEH ... Ubuntu |
1243 | + |
1244 | + |
1245 | +Sort order |
1246 | +---------- |
1247 | + |
1248 | +When using the search_text criteria, the default is to sort the results by |
1249 | +relevancy. One can use the sort parameter to change the order. It takes one |
1250 | +of the constant defined in the QuestionSort enumeration. |
1251 | + |
1252 | + >>> from lp.answers.interfaces.questionenums import QuestionSort |
1253 | >>> for question in question_set.searchQuestions( |
1254 | ... search_text='firefox', sort=QuestionSort.OLDEST_FIRST): |
1255 | - ... print question.id, repr(question.title), ( |
1256 | + ... print question.id, ascii_smash(question.title), ( |
1257 | ... question.target.displayname) |
1258 | - 14 u'\u0639\u0643\u0633 ...' Ubuntu |
1259 | - 1 u'Firefox cannot render Bank Site' Mozilla Firefox |
1260 | - 2 u'Problem showing the SVG demo on W3C site' Mozilla Firefox |
1261 | - 4 u'Firefox loses focus and gets stuck' Mozilla Firefox |
1262 | - 6 u"Newly installed plug-in doesn't seem to be used" Mozilla Firefox |
1263 | - 9 u'mailto: problem in webpage' mozilla-firefox in ubuntu |
1264 | - 13 u'Problemas de Impress\xe3o no Firefox' Mozilla Firefox |
1265 | + 14 AINKAFSEEN ALEFLAMTEHGHAINYEHYEHREHALEFTEH ... Ubuntu |
1266 | + 1 Firefox cannot render Bank Site Mozilla Firefox |
1267 | + 2 Problem showing the SVG demo on W3C site Mozilla Firefox |
1268 | + 4 Firefox loses focus and gets stuck Mozilla Firefox |
1269 | + 6 Newly installed plug-in doesn't seem to be used Mozilla Firefox |
1270 | + 9 mailto: problem in webpage mozilla-firefox in ubuntu |
1271 | + 13 Problemas de Impressao no Firefox Mozilla Firefox |
1272 | |
1273 | -When no text search is done, the default sort order is |
1274 | -QuestionSort.NEWEST_FIRST. |
1275 | +When no text search is done, the default sort order is by newest first. |
1276 | |
1277 | >>> for question in question_set.searchQuestions( |
1278 | - ... status=QuestionStatus.OPEN)[:5]: |
1279 | - ... print question.id, repr(question.title), ( |
1280 | + ... status=QuestionStatus.OPEN)[:5]: |
1281 | + ... print question.id, ascii_smash(question.title), ( |
1282 | ... question.target.displayname) |
1283 | - 13 u'Problemas de Impress\xe3o no Firefox' Mozilla Firefox |
1284 | - 12 u'Problema al recompilar kernel con soporte smp (doble-n\xfacleo)' Ubuntu |
1285 | - 11 u'Continue playing after shutdown' Ubuntu |
1286 | - 5 u'Installation failed' Ubuntu |
1287 | - 4 u'Firefox loses focus and gets stuck' Mozilla Firefox |
1288 | - |
1289 | - |
1290 | -== getQuestionLanguages() == |
1291 | + 13 Problemas de Impressao no Firefox Mozilla Firefox |
1292 | + 12 Problema al recompilar kernel con soporte smp (doble-nucleo) Ubuntu |
1293 | + 11 Continue playing after shutdown Ubuntu |
1294 | + 5 Installation failed Ubuntu |
1295 | + 4 Firefox loses focus and gets stuck Mozilla Firefox |
1296 | + |
1297 | + |
1298 | +Question languages |
1299 | +================== |
1300 | |
1301 | The getQuestionLanguages() method returns the set of languages in which |
1302 | -questions are written in Launchpad. |
1303 | - |
1304 | - >>> sorted([language.code |
1305 | - ... for language in question_set.getQuestionLanguages()]) |
1306 | - [u'ar', u'en', u'es', u'pt_BR'] |
1307 | - |
1308 | - |
1309 | -== getActiveProjects() == |
1310 | - |
1311 | -This method can be used to retrieve the projects that are the most |
1312 | -actively using the Answer Tracker in the last 60 days. By active, we |
1313 | -mean that the project is registered as officially using Answers and |
1314 | -had some questions asked in the period. The projects are ordered |
1315 | -by the number of questions asked during the period. |
1316 | - |
1317 | -Sample data should not contain any questions more recent than |
1318 | -two months, so no projects are initially returned: |
1319 | - |
1320 | - >>> for project in question_set.getMostActiveProjects(): |
1321 | - ... print project.displayname |
1322 | - |
1323 | -Create recent questions on a number of projects. |
1324 | - |
1325 | - >>> from lp.answers.testing import ( |
1326 | - ... QuestionFactory) |
1327 | - >>> from canonical.launchpad.interfaces import ( |
1328 | - ... IDistributionSet, ILaunchBag, IProductSet) |
1329 | - >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
1330 | +questions are written in launchpad. |
1331 | + |
1332 | + >>> print ', '.join( |
1333 | + ... sorted(language.code |
1334 | + ... for language in question_set.getQuestionLanguages())) |
1335 | + ar, en, es, pt_BR |
1336 | + |
1337 | + |
1338 | +Active projects |
1339 | +=============== |
1340 | + |
1341 | +This method can be used to retrieve the projects that are the most actively |
1342 | +using the Answer Tracker in the last 60 days. By active, we mean that the |
1343 | +project is registered as officially using Answers and had some questions asked |
1344 | +in the period. The projects are ordered by the number of questions asked |
1345 | +during the period. |
1346 | + |
1347 | +Initially, no projects are returned. |
1348 | + |
1349 | + >>> list(question_set.getMostActiveProjects()) |
1350 | + [] |
1351 | + |
1352 | +Then some recent questions are created on a number of projects. |
1353 | + |
1354 | + >>> from lp.answers.testing import QuestionFactory |
1355 | + >>> from lp.registry.interfaces.distribution import IDistributionSet |
1356 | + >>> from lp.registry.interfaces.person import IPersonSet |
1357 | + >>> from lp.registry.interfaces.product import IProductSet |
1358 | + |
1359 | >>> firefox = getUtility(IProductSet).getByName('firefox') |
1360 | >>> landscape = getUtility(IProductSet).getByName('landscape') |
1361 | >>> launchpad = getUtility(IProductSet).getByName('launchpad') |
1362 | + >>> no_priv = getUtility(IPersonSet).getByName('no-priv') |
1363 | + >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu') |
1364 | |
1365 | >>> login('no-priv@canonical.com') |
1366 | - >>> no_priv = getUtility(ILaunchBag).user |
1367 | - >>> QuestionFactory.createManyByProject([ |
1368 | + >>> QuestionFactory.createManyByProject(( |
1369 | ... ('ubuntu', 3), |
1370 | ... ('firefox', 2), |
1371 | - ... ('landscape', 1)]) |
1372 | + ... ('landscape', 1), |
1373 | + ... )) |
1374 | |
1375 | -Create a question just before the time limit on Launchpad. |
1376 | +A question is created just before the time limit on Launchpad. |
1377 | |
1378 | >>> from datetime import datetime, timedelta |
1379 | >>> from pytz import UTC |
1380 | @@ -191,9 +204,9 @@ |
1381 | ... datecreated=datetime.now(UTC) - timedelta(days=61)) |
1382 | >>> login(ANONYMOUS) |
1383 | |
1384 | -The method returns only projects which officially use the Answer |
1385 | -Tracker. The order of the returned projects is based on the number of |
1386 | -questions asked during the period. |
1387 | +The method returns only projects which officially use the Answer Tracker. The |
1388 | +order of the returned projects is based on the number of questions asked |
1389 | +during the period. |
1390 | |
1391 | >>> ubuntu.official_answers |
1392 | True |
1393 | @@ -204,14 +217,13 @@ |
1394 | >>> launchpad.official_answers |
1395 | True |
1396 | |
1397 | + # Launchpad is not returned because the question was not asked in |
1398 | + # the last 60 days. |
1399 | >>> for project in question_set.getMostActiveProjects(): |
1400 | ... print project.displayname |
1401 | Ubuntu |
1402 | Mozilla Firefox |
1403 | |
1404 | -(Launchpad is not returned because the question was not asked in |
1405 | -the last 60 days.) |
1406 | - |
1407 | The method accepts an optional limit parameter limiting the number of |
1408 | project returned: |
1409 | |
1410 | @@ -220,10 +232,11 @@ |
1411 | Ubuntu |
1412 | |
1413 | |
1414 | -== getOpenQuestionCountByPackages() == |
1415 | +Counting the open questions |
1416 | +=========================== |
1417 | |
1418 | -getOpenQuestionCountByPackages() allow you to get the count of open |
1419 | -questions on a set of IDistributionSourcePackage packages. |
1420 | +getOpenQuestionCountByPackages() allow you to get the count of open questions |
1421 | +on a set of IDistributionSourcePackage packages. |
1422 | |
1423 | >>> question_set.getOpenQuestionCountByPackages([]) |
1424 | {} |
1425 | @@ -246,12 +259,10 @@ |
1426 | >>> closed_question.setStatus( |
1427 | ... closed_question.owner, QuestionStatus.SOLVED, 'no comment') |
1428 | <QuestionMessage at ...> |
1429 | - >>> from canonical.launchpad.ftests import syncUpdate |
1430 | - >>> syncUpdate(closed_question) |
1431 | |
1432 | >>> from operator import itemgetter |
1433 | - >>> packages = [ |
1434 | - ... ubuntu_evolution, ubuntu_pmount, debian_evolution, debian_pmount] |
1435 | + >>> packages = ( |
1436 | + ... ubuntu_evolution, ubuntu_pmount, debian_evolution, debian_pmount) |
1437 | >>> package_counts = question_set.getOpenQuestionCountByPackages(packages) |
1438 | >>> len(packages) |
1439 | 4 |
1440 | @@ -263,5 +274,3 @@ |
1441 | pmount (Ubuntu): 4 |
1442 | evolution (Debian): 3 |
1443 | pmount (Debian): 0 |
1444 | - |
1445 | - |
1446 | |
1447 | === modified file 'lib/lp/answers/doc/questiontarget.txt' |
1448 | --- lib/lp/answers/doc/questiontarget.txt 2009-03-24 12:43:49 +0000 |
1449 | +++ lib/lp/answers/doc/questiontarget.txt 2010-02-10 15:24:19 +0000 |
1450 | @@ -1,38 +1,43 @@ |
1451 | -= IQuestionTarget Interface = |
1452 | - |
1453 | -Launchpad includes an answer tracker. Questions are associated to |
1454 | -objects implementing IQuestionTarget. This file documents that interface |
1455 | -and can be used to validate implementation of this interface on a |
1456 | -particular object. (This object is made available through the 'target' |
1457 | -variable which is defined outside of this file, usually by a |
1458 | -LaunchpadFunctionalTestCase. This instance shouldn't have any questions |
1459 | -associated with it at the start of the test.) |
1460 | - |
1461 | +========================= |
1462 | +IQuestionTarget interface |
1463 | +========================= |
1464 | + |
1465 | +Launchpad includes an answer tracker. Questions are associated to objects |
1466 | +implementing IQuestionTarget. |
1467 | + |
1468 | + # An IQuestionTarget object is made available to this test via the |
1469 | + # 'target' variable by the test framework. It won't have any questions |
1470 | + # associated with it at the start of the test. This is done because the |
1471 | + # exact same test applies to all types of question targets: products, |
1472 | + # distributions, and distribution source packages. |
1473 | + # |
1474 | # Some parts of the IQuestionTarget interface are only accessible |
1475 | # to a registered user. |
1476 | >>> login('no-priv@canonical.com') |
1477 | |
1478 | >>> from zope.component import getUtility |
1479 | >>> from zope.interface.verify import verifyObject |
1480 | - >>> from canonical.launchpad.interfaces import IQuestionTarget |
1481 | + >>> from lp.answers.interfaces.questiontarget import IQuestionTarget |
1482 | |
1483 | >>> verifyObject(IQuestionTarget, target) |
1484 | True |
1485 | |
1486 | -== newQuestion() == |
1487 | + |
1488 | +New questions |
1489 | +============= |
1490 | |
1491 | Questions are always owned by a registered user. |
1492 | |
1493 | - >>> from canonical.launchpad.interfaces import IPersonSet |
1494 | + >>> from lp.registry.interfaces.person import IPersonSet |
1495 | >>> sample_person = getUtility(IPersonSet).getByEmail( |
1496 | ... 'test@canonical.com') |
1497 | |
1498 | -The newQuestion() method is used to create question that will be associated |
1499 | -with the target. It takes as parameters the question's owner, title and |
1500 | -description. It also takes an optional parameter 'datecreated' parameter |
1501 | -which defaults to UTC_NOW. |
1502 | +The newQuestion() method is used to create a question that will be associated |
1503 | +with the target. It takes as parameters the question's owner, title and |
1504 | +description. It also takes an optional parameter 'datecreated' which defaults |
1505 | +to UTC_NOW. |
1506 | |
1507 | - # Let's define now to a know value. |
1508 | + # Initialize 'now' to a known value. |
1509 | >>> from datetime import datetime, timedelta |
1510 | >>> from pytz import UTC |
1511 | >>> now = datetime.now(UTC) |
1512 | @@ -43,8 +48,8 @@ |
1513 | New question |
1514 | >>> print question.description |
1515 | Question description |
1516 | - >>> question.owner == sample_person |
1517 | - True |
1518 | + >>> print question.owner.displayname |
1519 | + Sample Person |
1520 | >>> question.datecreated == now |
1521 | True |
1522 | >>> question.datelastquery == now |
1523 | @@ -53,41 +58,44 @@ |
1524 | The created question starts in the 'Open' status and should have the owner |
1525 | subscribed to the question. |
1526 | |
1527 | - >>> question.status.title |
1528 | - 'Open' |
1529 | - |
1530 | - >>> sample_person in [s.person for s in question.subscriptions] |
1531 | - True |
1532 | - |
1533 | -Question can be written in any languages supported in Launchpad. The |
1534 | -language of the request is available in the 'language' attribute. By |
1535 | -default, requests are assumed to be written in English: |
1536 | + >>> print question.status.title |
1537 | + Open |
1538 | + |
1539 | + >>> for subscription in question.subscriptions: |
1540 | + ... print subscription.person.displayname |
1541 | + Sample Person |
1542 | + |
1543 | +Questions can be written in any languages supported in Launchpad. The |
1544 | +language of the request is available in the 'language' attribute. By default, |
1545 | +requests are assumed to be written in English. |
1546 | |
1547 | >>> print question.language.code |
1548 | en |
1549 | |
1550 | -It is possible to create question in another language than English. One |
1551 | -just need to pass the language in which the question is written in the |
1552 | -language parameter. |
1553 | +It is possible to create questions in another language than English, by |
1554 | +passing in the language that the question is written in. |
1555 | |
1556 | - >>> from canonical.launchpad.interfaces import ILanguageSet |
1557 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
1558 | >>> french = getUtility(ILanguageSet)['fr'] |
1559 | - >>> question = target.newQuestion(sample_person, "De l'aide S.V.P.", |
1560 | + >>> question = target.newQuestion( |
1561 | + ... sample_person, "De l'aide S.V.P.", |
1562 | ... "Pouvez-vous m'aider?", language=french, |
1563 | ... datecreated=now + timedelta(seconds=30)) |
1564 | >>> print question.language.code |
1565 | fr |
1566 | |
1567 | -Anonymous users cannot use newQuestion(): |
1568 | +Anonymous users cannot use newQuestion(). |
1569 | |
1570 | >>> login(ANONYMOUS) |
1571 | - >>> question = target.newQuestion(sample_person, 'This will fail', |
1572 | - ... 'Failed?') |
1573 | + >>> question = target.newQuestion( |
1574 | + ... sample_person, 'This will fail', 'Failed?') |
1575 | Traceback (most recent call last): |
1576 | ... |
1577 | Unauthorized... |
1578 | |
1579 | -== getQuestion() == |
1580 | + |
1581 | +Retrieving questions |
1582 | +==================== |
1583 | |
1584 | The getQuestion() method is used to retrieve a question by id for a |
1585 | particular target. |
1586 | @@ -96,19 +104,19 @@ |
1587 | True |
1588 | |
1589 | If you pass in a non-existent id or a question for a different target, the |
1590 | -method must return None. |
1591 | - |
1592 | - >>> target.getQuestion(2) is None |
1593 | - True |
1594 | - >>> target.getQuestion(12345) is None |
1595 | - True |
1596 | - |
1597 | -== Creating some additional questions == |
1598 | - |
1599 | -For the following methods, we will require some more questions. Create five |
1600 | -new questions. Odd questions will be owned by foo_bar and even questions will be |
1601 | -owned by sample_person. |
1602 | - |
1603 | +method returns None. |
1604 | + |
1605 | + >>> print target.getQuestion(2) |
1606 | + None |
1607 | + >>> print target.getQuestion(12345) |
1608 | + None |
1609 | + |
1610 | + |
1611 | +Searching for questions |
1612 | +======================= |
1613 | + |
1614 | + # Create new questions for the following tests. Odd questions will be |
1615 | + # owned by Foo Bar and even questions will be owned by Sample Person. |
1616 | >>> login('no-priv@canonical.com') |
1617 | >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com') |
1618 | >>> questions = [] |
1619 | @@ -123,9 +131,8 @@ |
1620 | ... owner, 'Question title%d' % num, description, |
1621 | ... datecreated=now+timedelta(minutes=num+1))) |
1622 | |
1623 | -For more variety, we will set the status of the last to INVALID and the |
1624 | -fourth one to ANSWERED. |
1625 | - |
1626 | + # For more variety, we will set the status of the last to INVALID and the |
1627 | + # fourth one to ANSWERED. |
1628 | >>> login('foo.bar@canonical.com') |
1629 | >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com') |
1630 | >>> message = questions[-1].reject( |
1631 | @@ -134,48 +141,44 @@ |
1632 | ... sample_person, 'This is your answer.', |
1633 | ... datecreated=now+timedelta(hours=1)) |
1634 | |
1635 | -Also add a reply from the owner on the first of these. |
1636 | - |
1637 | + # Also add a reply from the owner on the first of these. |
1638 | >>> login('test@canonical.com') |
1639 | >>> message = questions[0].giveInfo( |
1640 | ... 'I think I forgot something.', datecreated=now+timedelta(hours=4)) |
1641 | |
1642 | -And create another one that will also have the word 'new' in its |
1643 | -description. |
1644 | - |
1645 | + # Create another one that will also have the word 'new' in its |
1646 | + # description. |
1647 | >>> question = target.newQuestion(sample_person, 'Another question', |
1648 | ... 'Another new question that is actually very new.', |
1649 | ... datecreated=now+timedelta(hours=1)) |
1650 | >>> login(ANONYMOUS) |
1651 | |
1652 | - # Flush those changes to the database. |
1653 | - >>> from canonical.database.sqlbase import flush_database_updates |
1654 | - >>> flush_database_updates() |
1655 | - |
1656 | -== searchQuestions() == |
1657 | - |
1658 | The searchQuestions() method is used to search for questions. |
1659 | |
1660 | -=== search_text === |
1661 | + |
1662 | +Search text |
1663 | +----------- |
1664 | |
1665 | The search_text parameter will select the questions that contain the |
1666 | -passed in text. (The standard text searching algorithm is used, see |
1667 | -textsearching.txt.) |
1668 | +passed in text. The standard text searching algorithm is used; see |
1669 | +../../../canonical/launchpad/doct/textsearching.txt. |
1670 | |
1671 | >>> for t in target.searchQuestions(search_text='new'): |
1672 | ... print t.title |
1673 | New question |
1674 | Another question |
1675 | |
1676 | -The results here are sorted by relevancy. (In the last questions, 'New' |
1677 | -appeared in the description which makes it less relevant than when the |
1678 | -word appears in the title.) |
1679 | - |
1680 | -=== status === |
1681 | - |
1682 | -The searchQuestions() method can also filter questions by status: |
1683 | - |
1684 | - >>> from canonical.launchpad.interfaces import QuestionStatus |
1685 | +The results are sorted by relevancy. In the last questions, 'New' appeared in |
1686 | +the description which makes it less relevant than when the word appears in the |
1687 | +title. |
1688 | + |
1689 | + |
1690 | +Status |
1691 | +------ |
1692 | + |
1693 | +The searchQuestions() method can also filter questions by status. |
1694 | + |
1695 | + >>> from lp.answers.interfaces.questionenums import QuestionStatus |
1696 | >>> for t in target.searchQuestions(status=QuestionStatus.OPEN): |
1697 | ... print t.title |
1698 | Another question |
1699 | @@ -192,9 +195,9 @@ |
1700 | ... print t.title |
1701 | Question title4 |
1702 | |
1703 | -You can also pass in a list of status, and you can also use the |
1704 | -search_text and status parameters at the same time. This will search |
1705 | -OPEN and INVALID questions with the word 'index' |
1706 | +You can pass in a list of statuses, and you can also use the search_text and |
1707 | +status parameters at the same time. This will search OPEN and INVALID |
1708 | +questions with the word 'index'. |
1709 | |
1710 | >>> for t in target.searchQuestions(search_text='request index', |
1711 | ... status=(QuestionStatus.OPEN, QuestionStatus.INVALID)): |
1712 | @@ -204,29 +207,30 @@ |
1713 | Question title1 |
1714 | Question title0 |
1715 | |
1716 | -=== sort === |
1717 | - |
1718 | -You can control the sort order by passing one of the constants defined |
1719 | -in QuestionSort. (We already saw the NEWEST_FIRST and RELEVANCY sort |
1720 | -order). |
1721 | - |
1722 | -You can sort also from oldest to newest using the OLDEST_FIRST constant: |
1723 | - |
1724 | - >>> from canonical.launchpad.interfaces import QuestionSort |
1725 | - |
1726 | + |
1727 | +Sorting |
1728 | +------- |
1729 | + |
1730 | +You can control the sort order by passing one of the constants defined in |
1731 | +QuestionSort. Previously, we saw the NEWEST_FIRST and RELEVANCY sort order. |
1732 | + |
1733 | +You can sort also from oldest to newest using the OLDEST_FIRST constant. |
1734 | + |
1735 | + >>> from lp.answers.interfaces.questionenums import QuestionSort |
1736 | >>> for t in target.searchQuestions(search_text='new', |
1737 | - ... sort=QuestionSort.OLDEST_FIRST): |
1738 | + ... sort=QuestionSort.OLDEST_FIRST): |
1739 | ... print t.title |
1740 | New question |
1741 | Another question |
1742 | |
1743 | -You can sort by status, (the status order is OPEN, NEEDSINFO, ANSWERED, |
1744 | -SOLVED, EXPIRED, INVALID), this also sorts from newest to oldest as a |
1745 | -secondary key. |
1746 | +You can sort by status (the status order is OPEN, NEEDSINFO, ANSWERED, SOLVED, |
1747 | +EXPIRED, INVALID). This also sorts from newest to oldest as a secondary key. |
1748 | +Here we use status=None to search for all statuses; by default INVALID and |
1749 | +EXPIRED questions are excluded. |
1750 | |
1751 | >>> for t in target.searchQuestions(search_text='request index', |
1752 | - ... status=None, |
1753 | - ... sort=QuestionSort.STATUS): |
1754 | + ... status=None, |
1755 | + ... sort=QuestionSort.STATUS): |
1756 | ... print t.status.title, t.title |
1757 | Open Question title2 |
1758 | Open Question title1 |
1759 | @@ -234,12 +238,11 @@ |
1760 | Answered Question title3 |
1761 | Invalid Question title4 |
1762 | |
1763 | -(In the previous example, we used status=None to search for all |
1764 | -statuses, by default INVALID and EXPIRED questions are excluded.) |
1765 | - |
1766 | If there is no search_text and the requested sort order is RELEVANCY, |
1767 | the questions will be sorted NEWEST_FIRST. |
1768 | |
1769 | + # 'Question title4' is not shown in this case because it has INVALID as |
1770 | + # its status. |
1771 | >>> for t in target.searchQuestions(sort=QuestionSort.RELEVANCY): |
1772 | ... print t.title |
1773 | Another question |
1774 | @@ -250,14 +253,14 @@ |
1775 | De l'aide S.V.P. |
1776 | New question |
1777 | |
1778 | -('Question title4' is not shown in this case because it has INVALID as |
1779 | -its status.) |
1780 | - |
1781 | The RECENT_OWNER_ACTIVITY sort order sorts first questions which recently |
1782 | -received a new message by their owner. (It effectively sorts |
1783 | -descending on the datelastquery attribute.) |
1784 | +received a new message by their owner. It effectively sorts descending on the |
1785 | +datelastquery attribute. |
1786 | |
1787 | - >>> for t in target.searchQuestions(sort=QuestionSort.RECENT_OWNER_ACTIVITY): |
1788 | + # Question title0 sorts first because it has a message from its owner |
1789 | + # after the others were created. |
1790 | + >>> for t in target.searchQuestions( |
1791 | + ... sort=QuestionSort.RECENT_OWNER_ACTIVITY): |
1792 | ... print t.title |
1793 | Question title0 |
1794 | Another question |
1795 | @@ -267,20 +270,20 @@ |
1796 | De l'aide S.V.P. |
1797 | New question |
1798 | |
1799 | -(Question title0 sorts first because it had a message from its owner |
1800 | -after the others were created.) |
1801 | - |
1802 | -=== owner === |
1803 | - |
1804 | -You can also find question owner by a particular user by using the owner |
1805 | -parameter. |
1806 | + |
1807 | +Owner |
1808 | +----- |
1809 | + |
1810 | +You can find question owned by a particular user by using the owner parameter. |
1811 | |
1812 | >>> for t in target.searchQuestions(owner=foo_bar): |
1813 | ... print t.title |
1814 | Question title3 |
1815 | Question title1 |
1816 | |
1817 | -=== language === |
1818 | + |
1819 | +Language |
1820 | +--------- |
1821 | |
1822 | The language criteria can be used to select only questions written in a |
1823 | particular language. |
1824 | @@ -290,7 +293,7 @@ |
1825 | ... print t.title |
1826 | De l'aide S.V.P. |
1827 | |
1828 | - >>> for t in target.searchQuestions(language=[english, french]): |
1829 | + >>> for t in target.searchQuestions(language=(english, french)): |
1830 | ... print t.title |
1831 | Another question |
1832 | Question title3 |
1833 | @@ -300,39 +303,40 @@ |
1834 | De l'aide S.V.P. |
1835 | New question |
1836 | |
1837 | -=== needs_attention_from === |
1838 | - |
1839 | -You can also search among the questions that needs the attention of |
1840 | -somebody. A question needs the attention of a user if he owns it and that |
1841 | -it is in the NEEDSINFO or ANSWERED state. Questions on which the user gave |
1842 | -an answer or requested for more information and that are back in the |
1843 | -OPEN state are also included. |
1844 | + |
1845 | +Questions needing attention |
1846 | +--------------------------- |
1847 | + |
1848 | +You can search among the questions that need attention. A question needs the |
1849 | +attention of a user if he owns it and if it is in the NEEDSINFO or ANSWERED |
1850 | +state. Questions on which the user gave an answer or requested for more |
1851 | +information, and that are back in the OPEN state, are also included. |
1852 | |
1853 | # One of Sample Person's question gets to need attention from Foo Bar. |
1854 | >>> login('foo.bar@canonical.com') |
1855 | >>> message = questions[0].requestInfo( |
1856 | ... foo_bar, 'Do you have a clue?', |
1857 | ... datecreated=now+timedelta(hours=1)) |
1858 | + |
1859 | >>> login('test@canonical.com') |
1860 | >>> message = questions[0].giveInfo( |
1861 | - ... 'I do, now please help me.', datecreated=now+timedelta(hours=2)) |
1862 | + ... 'I do, now please help me.', datecreated=now+timedelta(hours=2)) |
1863 | |
1864 | - # Another one of Foo Bar's question needs attention. |
1865 | + # Another one of Foo Bar's questions needs attention. |
1866 | >>> message = questions[1].requestInfo( |
1867 | ... sample_person, 'And you, do you have a clue?', |
1868 | ... datecreated=now+timedelta(hours=1)) |
1869 | |
1870 | - # Flush those changes to the database. |
1871 | - >>> flush_database_updates() |
1872 | >>> login(ANONYMOUS) |
1873 | - |
1874 | >>> for t in target.searchQuestions(needs_attention_from=foo_bar): |
1875 | ... print t.status.title, t.title, t.owner.displayname |
1876 | Answered Question title3 Foo Bar |
1877 | Needs information Question title1 Foo Bar |
1878 | Open Question title0 Sample Person |
1879 | |
1880 | -=== unsupported === |
1881 | + |
1882 | +Unsupported language |
1883 | +-------------------- |
1884 | |
1885 | The 'unsupported' criteria is used to select questions that are in a |
1886 | language that is not spoken by any of the Support Contacts. |
1887 | @@ -341,40 +345,42 @@ |
1888 | ... print t.title |
1889 | De l'aide S.V.P. |
1890 | |
1891 | -== findSimilarQuestions() == |
1892 | - |
1893 | -The method findSimilarQuestions() can be use to find questions similar to a |
1894 | -sentence. The questions don't have to contain all the words of the sentence, |
1895 | -just some. |
1896 | - |
1897 | + |
1898 | +Finding similar questions |
1899 | +========================= |
1900 | + |
1901 | +The method findSimilarQuestions() can be use to find questions similar to some |
1902 | +target text. The questions don't have to contain all the words of the text. |
1903 | + |
1904 | + # This returns the same results as with the search 'new' because |
1905 | + # all other words in the text are either common ('question', 'title') or |
1906 | + # stop words ('with', 'a'). |
1907 | >>> for t in target.findSimilarQuestions('new questions with a title'): |
1908 | ... print t.title |
1909 | New question |
1910 | Another question |
1911 | |
1912 | -In this case, it returned the same results than with the search 'new' because |
1913 | -all other words in the sentence are either common ('question', 'title') or stop |
1914 | -words ('with', 'a'). |
1915 | - |
1916 | -== Answer contacts == |
1917 | - |
1918 | -Target can have answer contacts. The list of answer contacts for a |
1919 | + |
1920 | +Answer contacts |
1921 | +=============== |
1922 | + |
1923 | +Targets can have answer contacts. The list of answer contacts for a |
1924 | target is available through the answer_contacts attribute. |
1925 | |
1926 | >>> list(target.answer_contacts) |
1927 | [] |
1928 | |
1929 | -There is also a direct_answer_contacts which includes only the |
1930 | -answer contacts registered explicitly on the question target. (In |
1931 | -general, it will be equal to answer_contacts attribute, but some |
1932 | -IQuestionTarget implementations may inherit answer contacts |
1933 | -from other context. In these cases, that attribute would only contain |
1934 | -the answer contacts defined in the current IQuestionTarget context.) |
1935 | +There is also a direct_answer_contacts which includes only the answer contacts |
1936 | +registered explicitly on the question target. In general, this will be the |
1937 | +same as the answer_contacts attribute, but some IQuestionTarget |
1938 | +implementations may inherit answer contacts from other contexts. In these |
1939 | +cases, the direct_answer_contacts attribute would only contain the answer |
1940 | +contacts defined in the current IQuestionTarget context. |
1941 | |
1942 | >>> list(target.direct_answer_contacts) |
1943 | [] |
1944 | |
1945 | -You add an answer contact by using the addAnswerContact method. This |
1946 | +You add an answer contact by using the addAnswerContact() method. This |
1947 | is only available to registered users. |
1948 | |
1949 | >>> name18 = getUtility(IPersonSet).getByName('name18') |
1950 | @@ -382,22 +388,28 @@ |
1951 | Traceback (most recent call last): |
1952 | ... |
1953 | Unauthorized... |
1954 | + |
1955 | +This method returns True when the contact was added the list and False when it |
1956 | +was already on the list. |
1957 | + |
1958 | >>> login('no-priv@canonical.com') |
1959 | - |
1960 | -This method will return True when the contact was added the list and |
1961 | -False when it was already on the list: |
1962 | - |
1963 | >>> target.addAnswerContact(name18) |
1964 | True |
1965 | - >>> [p.name for p in target.answer_contacts] |
1966 | - [u'name18'] |
1967 | - >>> [p.name for p in target.direct_answer_contacts] |
1968 | - [u'name18'] |
1969 | + >>> people = [p.name for p in target.answer_contacts] |
1970 | + >>> len(people) |
1971 | + 1 |
1972 | + >>> print people[0] |
1973 | + name18 |
1974 | + >>> people = [p.name for p in target.direct_answer_contacts] |
1975 | + >>> len(people) |
1976 | + 1 |
1977 | + >>> print people[0] |
1978 | + name18 |
1979 | >>> target.addAnswerContact(name18) |
1980 | False |
1981 | |
1982 | -An answer contact must have at least one language among his |
1983 | -preferred languages. |
1984 | +An answer contact must have at least one language among his preferred |
1985 | +languages. |
1986 | |
1987 | >>> sample_person = getUtility(IPersonSet).getByName('name12') |
1988 | >>> len(sample_person.languages) |
1989 | @@ -407,10 +419,9 @@ |
1990 | ... |
1991 | AssertionError: An Answer Contact must speak a language... |
1992 | |
1993 | -Answer contacts can be removed by using the removeAnswerContact() |
1994 | -method. Like its counterpart, it returns True when the answer contact |
1995 | -was removed and False when the person wasn't on the answer contact |
1996 | -list. |
1997 | +Answer contacts can be removed by using the removeAnswerContact() method. |
1998 | +Like its counterpart, it returns True when the answer contact was removed and |
1999 | +False when the person wasn't on the answer contact list. |
2000 | |
2001 | >>> target.removeAnswerContact(name18) |
2002 | True |
2003 | @@ -421,7 +432,7 @@ |
2004 | >>> target.removeAnswerContact(name18) |
2005 | False |
2006 | |
2007 | -Only registered users can remove an answer contact: |
2008 | +Only registered users can remove an answer contact. |
2009 | |
2010 | >>> login(ANONYMOUS) |
2011 | >>> target.removeAnswerContact(name18) |
2012 | @@ -429,85 +440,102 @@ |
2013 | ... |
2014 | Unauthorized... |
2015 | |
2016 | -== Supported Languages == |
2017 | + |
2018 | +Supported languages |
2019 | +=================== |
2020 | |
2021 | The supported languages for a given IQuestionTarget are given by |
2022 | -getSupportedLanguages(). The supported languages of a question target |
2023 | -include all languages spoken by at least one of its answer contacts, |
2024 | -with the exception of all English variations. English is the assumed |
2025 | -language for support when there are no answer contacts. |
2026 | - |
2027 | - >>> [lang.code for lang in target.getSupportedLanguages()] |
2028 | - [u'en'] |
2029 | - |
2030 | -Let's add some answer contacts which speak different languages. |
2031 | - |
2032 | +getSupportedLanguages(). The supported languages of a question target include |
2033 | +all languages spoken by at least one of its answer contacts, with the |
2034 | +exception of all English variations since English is the assumed language for |
2035 | +support when there are no answer contacts. |
2036 | + |
2037 | + >>> codes = [lang.code for lang in target.getSupportedLanguages()] |
2038 | + >>> len(codes) |
2039 | + 1 |
2040 | + >>> print codes[0] |
2041 | + en |
2042 | + |
2043 | + # Let's add some answer contacts which speak different languages. |
2044 | >>> login('carlos@canonical.com') |
2045 | >>> carlos = getUtility(IPersonSet).getByName('carlos') |
2046 | - >>> [lang.code for lang in carlos.languages] |
2047 | - [u'ca', u'en', u'es'] |
2048 | + >>> for language in carlos.languages: |
2049 | + ... print language.code |
2050 | + ca |
2051 | + en |
2052 | + es |
2053 | >>> target.addAnswerContact(carlos) |
2054 | True |
2055 | |
2056 | -Note that daf has en_GB as one of his preferred languages... |
2057 | +While daf has en_GB as one of his preferred languages... |
2058 | |
2059 | >>> login('daf@canonical.com') |
2060 | >>> daf = getUtility(IPersonSet).getByName('daf') |
2061 | - >>> [lang.code for lang in daf.languages] |
2062 | - [u'en_GB', u'ja', u'cy'] |
2063 | + >>> for language in daf.languages: |
2064 | + ... print language.code |
2065 | + en_GB |
2066 | + ja |
2067 | + cy |
2068 | >>> target.addAnswerContact(daf) |
2069 | True |
2070 | |
2071 | -... but en_GB is not included in the target's supported languages, |
2072 | -because we convert all English variants to English. |
2073 | - |
2074 | - >>> import operator |
2075 | - >>> [lang.code for lang in sorted(target.getSupportedLanguages(), |
2076 | - ... key=operator.attrgetter('code'))] |
2077 | - [u'ca', u'cy', u'en', u'es', u'ja'] |
2078 | - |
2079 | - |
2080 | -== getAnswerContactsForLanguage() == |
2081 | - |
2082 | -Continuing from the previous section with Carlos and Daf, the |
2083 | -getAnswerContactsForLanguage() method returns a list of answer contacts |
2084 | -who support the specified language in their preferred languages. Daf |
2085 | -is in the list because he speaks an English variant, which is treated |
2086 | -as English. |
2087 | +...en_GB is not included in the target's supported languages, because all |
2088 | +English variants are converted to English. |
2089 | + |
2090 | + >>> from operator import attrgetter |
2091 | + >>> print ', '.join( |
2092 | + ... language.code |
2093 | + ... for language in sorted(target.getSupportedLanguages(), |
2094 | + ... key=attrgetter('code'))) |
2095 | + ca, cy, en, es, ja |
2096 | + |
2097 | + |
2098 | +Answer contacts for languages |
2099 | +============================= |
2100 | + |
2101 | +getAnswerContactsForLanguage() method returns a list of answer contacts who |
2102 | +support the specified language in their preferred languages. Daf is in the |
2103 | +list because he speaks an English variant, which is treated as English. |
2104 | |
2105 | >>> spanish = getUtility(ILanguageSet)['es'] |
2106 | >>> answer_contacts = target.getAnswerContactsForLanguage(spanish) |
2107 | - >>> sorted([person.name for person in answer_contacts]) |
2108 | - [u'carlos'] |
2109 | + >>> for person in answer_contacts: |
2110 | + ... print person.name |
2111 | + carlos |
2112 | |
2113 | >>> answer_contacts = target.getAnswerContactsForLanguage(english) |
2114 | - >>> sorted([person.name for person in answer_contacts]) |
2115 | - [u'carlos', u'daf'] |
2116 | - |
2117 | - |
2118 | -== getQuestionLanguages() == |
2119 | + >>> for person in answer_contacts: |
2120 | + ... print person.name |
2121 | + carlos |
2122 | + daf |
2123 | + |
2124 | + |
2125 | +A question's languages |
2126 | +====================== |
2127 | |
2128 | The getQuestionLanguages() method returns the set of languages used by all |
2129 | of the target's questions. |
2130 | |
2131 | - >>> sorted([language.code for language in target.getQuestionLanguages()]) |
2132 | - [u'en', u'fr'] |
2133 | - |
2134 | - |
2135 | -== createQuestionFromBug() == |
2136 | - |
2137 | -The target can create a question from a bug, and link that bug to the |
2138 | -new question. The question owner is the same as the bug owner. The |
2139 | -question title and description are taken from the bug. The messages on |
2140 | -the bug are copied to the question. |
2141 | + >>> print ', '.join( |
2142 | + ... sorted(language.code |
2143 | + ... for language in target.getQuestionLanguages())) |
2144 | + en, fr |
2145 | + |
2146 | + |
2147 | +Creating questions from bugs |
2148 | +============================ |
2149 | + |
2150 | +The target can create a question from a bug, and link that bug to the new |
2151 | +question. The question's owner is the same as the bug's owner. The question |
2152 | +title and description are taken from the bug. The comments on the bug are |
2153 | +copied to the question. |
2154 | |
2155 | >>> from datetime import datetime |
2156 | >>> from pytz import UTC |
2157 | - >>> from canonical.launchpad.interfaces import ( |
2158 | - ... CreateBugParams, IBugSet, IProductSet) |
2159 | + >>> from lp.bugs.interfaces.bug import CreateBugParams, IBugSet |
2160 | + >>> from lp.registry.interfaces.product import IProductSet |
2161 | |
2162 | >>> now = datetime.now(UTC) |
2163 | - |
2164 | >>> target = getUtility(IProductSet)['jokosher'] |
2165 | >>> bug_params = CreateBugParams( |
2166 | ... title="Print is broken", comment="blah blah blah", |
2167 | @@ -519,41 +547,34 @@ |
2168 | |
2169 | >>> target_question = target.createQuestionFromBug(target_bug) |
2170 | |
2171 | - >>> target_question.owner == target_bug.owner |
2172 | - True |
2173 | - >>> target_question.title == target_bug.title |
2174 | - True |
2175 | - >>> target_question.description == target_bug.description |
2176 | - True |
2177 | + >>> print target_question.owner.displayname |
2178 | + Sample Person |
2179 | + >>> print target_question.title |
2180 | + Print is broken |
2181 | + >>> print target_question.description |
2182 | + blah blah blah |
2183 | >>> question_message = target_question.messages[-1] |
2184 | - >>> question_message.text_contents == bug_message.text_contents |
2185 | - True |
2186 | - |
2187 | - >>> target_question.owner.displayname |
2188 | - u'Sample Person' |
2189 | - >>> target_question.title |
2190 | - u'Print is broken' |
2191 | - >>> target_question.description |
2192 | - u'blah blah blah' |
2193 | - >>> [bug_link.bug.title for bug_link in target_question.bug_links] |
2194 | - [u'Print is broken'] |
2195 | - >>> target_question.messages[-1].text_contents |
2196 | - u'This is really a question.' |
2197 | - |
2198 | -The question's datecreated attribute is the same as the bug's |
2199 | -datecreated. The question's datelastresponse attribute has a current |
2200 | -datetime stamp to indicate the question is active. The question janitor |
2201 | -would otherwise mistake the questions made from old bugs as old |
2202 | -questions and would expire them. |
2203 | + >>> print question_message.text_contents |
2204 | + This is really a question. |
2205 | + |
2206 | + >>> for bug_link in target_question.bug_links: |
2207 | + ... print bug_link.bug.title |
2208 | + Print is broken |
2209 | + >>> print target_question.messages[-1].text_contents |
2210 | + This is really a question. |
2211 | + |
2212 | +The question's creation date is the same as the bug's creation date. The |
2213 | +question's last response date has a current datetime stamp to indicate the |
2214 | +question is active. The question janitor would otherwise mistake the |
2215 | +questions made from old bugs as old questions and would expire them. |
2216 | |
2217 | >>> target_question.datecreated == target_bug.datecreated |
2218 | True |
2219 | >>> target_question.datelastresponse > now |
2220 | True |
2221 | |
2222 | -The question language is always English because all bugs in Launchpad |
2223 | -are written in English. |
2224 | - |
2225 | - >>> target_question.language.code |
2226 | - u'en' |
2227 | - |
2228 | +The question language is always English because all bugs in Launchpad are |
2229 | +written in English. |
2230 | + |
2231 | + >>> print target_question.language.code |
2232 | + en |
2233 | |
2234 | === modified file 'lib/lp/answers/doc/workflow.txt' |
2235 | --- lib/lp/answers/doc/workflow.txt 2009-07-13 05:48:57 +0000 |
2236 | +++ lib/lp/answers/doc/workflow.txt 2010-02-10 15:24:19 +0000 |
2237 | @@ -1,11 +1,13 @@ |
2238 | -= Answer Tracker Workflow = |
2239 | - |
2240 | -The state of a question is tracked through its status attribute. |
2241 | -Six statuses are used to model a question lifecycle. These are defined |
2242 | -in the QuestionStatus enumeration. |
2243 | - |
2244 | - >>> from canonical.launchpad.interfaces import QuestionStatus |
2245 | - >>> print "\n".join([status.name for status in QuestionStatus.items]) |
2246 | +======================= |
2247 | +Answer tracker workflow |
2248 | +======================= |
2249 | + |
2250 | +The state of a question is tracked through its status, which model a |
2251 | +question's lifecycle. These are defined in the QuestionStatus enumeration. |
2252 | + |
2253 | + >>> from lp.answers.interfaces.questionenums import QuestionStatus |
2254 | + >>> for status in QuestionStatus.items: |
2255 | + ... print status.name |
2256 | OPEN |
2257 | NEEDSINFO |
2258 | ANSWERED |
2259 | @@ -13,11 +15,12 @@ |
2260 | EXPIRED |
2261 | INVALID |
2262 | |
2263 | -Status change occurs in consequence of a user action. The possible |
2264 | +Status change occurs as a consequence of a user's action. The possible |
2265 | actions are defined in the QuestionAction enumeration. |
2266 | |
2267 | - >>> from canonical.launchpad.interfaces import QuestionAction |
2268 | - >>> print "\n".join([status.name for status in QuestionAction.items]) |
2269 | + >>> from lp.answers.interfaces.questionenums import QuestionAction |
2270 | + >>> for status in QuestionAction.items: |
2271 | + ... print status.name |
2272 | REQUESTINFO |
2273 | GIVEINFO |
2274 | COMMENT |
2275 | @@ -28,19 +31,18 @@ |
2276 | REOPEN |
2277 | SETSTATUS |
2278 | |
2279 | -There is a method available to execute each of these defined actions. |
2280 | +Each defined action can be executed. |
2281 | |
2282 | -Let's define the actors that we are going to use to demonstrate the |
2283 | -Answer Tracker workflow. The 'No Privileges Person' will be the |
2284 | -submitter of questions, 'Sample Person' will be an answer contact for |
2285 | -the Ubuntu distribution, and 'Marilize Coetze' will be another user |
2286 | -providing support. Stub is a launchpad administrator that isn't also in |
2287 | -the Ubuntu Team that owns the distribution. |
2288 | +No Privileges Person is the submitter of questions. Sample Person is an |
2289 | +answer contact for the Ubuntu distribution. Marilize Coetze is another user |
2290 | +providing support. Stub is a Launchpad administrator that isn't also in the |
2291 | +Ubuntu Team owning the distribution. |
2292 | |
2293 | >>> login('no-priv@canonical.com') |
2294 | |
2295 | - >>> from canonical.launchpad.interfaces import ( |
2296 | - ... IDistributionSet, ILanguageSet, IPersonSet) |
2297 | + >>> from lp.registry.interfaces.distribution import IDistributionSet |
2298 | + >>> from lp.registry.interfaces.person import IPersonSet |
2299 | + >>> from lp.services.worlddata.interfaces.language import ILanguageSet |
2300 | |
2301 | >>> personset = getUtility(IPersonSet) |
2302 | >>> sample_person = personset.getByEmail('test@canonical.com') |
2303 | @@ -63,28 +65,31 @@ |
2304 | >>> from datetime import datetime, timedelta |
2305 | >>> from pytz import UTC |
2306 | >>> now = datetime.now(UTC) |
2307 | - >>> new_question_args = { |
2308 | - ... 'owner': no_priv, |
2309 | - ... 'title': 'Unable to boot installer', |
2310 | - ... 'description': "I've tried installing Ubuntu on a Mac. " |
2311 | - ... "But the installer never boots.", |
2312 | - ... 'datecreated': now} |
2313 | + >>> new_question_args = dict( |
2314 | + ... owner=no_priv, |
2315 | + ... title='Unable to boot installer', |
2316 | + ... description="I've tried installing Ubuntu on a Mac. " |
2317 | + ... "But the installer never boots.", |
2318 | + ... datecreated=now, |
2319 | + ... ) |
2320 | >>> question = ubuntu.newQuestion(**new_question_args) |
2321 | >>> print question.status.title |
2322 | Open |
2323 | |
2324 | -From there, we have four representative scenarios. |
2325 | - |
2326 | -== 1) Another user helps the submitter with his question == |
2327 | - |
2328 | -The most common scenario is where another user comes to help the |
2329 | -submitter and answers his question. This may involve exchanging |
2330 | -information with the submitter to clarify the question. |
2331 | - |
2332 | -The requestInfo() method is used to ask the user for more information. |
2333 | -This method takes two mandatory parameters: the user making the question |
2334 | -and his question. It can also takes a 'datecreated' parameter specifying |
2335 | -the creation date of the question (which defaults to now). |
2336 | +The following scenarios are now possible. |
2337 | + |
2338 | + |
2339 | +1) Another user helps the submitter with his question |
2340 | +===================================================== |
2341 | + |
2342 | +The most common scenario is where another user comes to help the submitter and |
2343 | +answers his question. This may involve exchanging information with the |
2344 | +submitter to clarify the question. |
2345 | + |
2346 | +The requestInfo() method is used to ask the user for more information. This |
2347 | +method takes two mandatory parameters: the user asking the question and his |
2348 | +question. It can also takes a 'datecreated' parameter specifying the creation |
2349 | +date of the question (which defaults to 'now'). |
2350 | |
2351 | >>> question = ubuntu.newQuestion(**new_question_args) |
2352 | >>> now_plus_one_hour = now + timedelta(hours=1) |
2353 | @@ -92,11 +97,11 @@ |
2354 | ... sample_person, 'What is your Mac model?', |
2355 | ... datecreated=now_plus_one_hour) |
2356 | |
2357 | -It returns the IQuestionMessage that was added to the question messages |
2358 | -history: |
2359 | +We now have the IQuestionMessage that was added to the question messages |
2360 | +history. |
2361 | |
2362 | >>> from canonical.launchpad.webapp.testing import verifyObject |
2363 | - >>> from canonical.launchpad.interfaces import IQuestionMessage |
2364 | + >>> from lp.answers.interfaces.questionmessage import IQuestionMessage |
2365 | >>> verifyObject(IQuestionMessage, request_message) |
2366 | True |
2367 | >>> request_message == question.messages[-1] |
2368 | @@ -106,9 +111,8 @@ |
2369 | >>> print request_message.owner.displayname |
2370 | Sample Person |
2371 | |
2372 | -The question message contains the action that was executed in the action |
2373 | -attribute and the status of the question after the action was executed in |
2374 | -the new_status attribute: |
2375 | +The question message contains the action that was executed and the status of |
2376 | +the question after the action was executed. |
2377 | |
2378 | >>> print request_message.action.name |
2379 | REQUESTINFO |
2380 | @@ -118,13 +122,13 @@ |
2381 | >>> print request_message.text_contents |
2382 | What is your Mac model? |
2383 | |
2384 | -The subject of the message was generated automatically: |
2385 | +The subject of the message was generated automatically. |
2386 | |
2387 | >>> print request_message.subject |
2388 | Re: Unable to boot installer |
2389 | |
2390 | -The question is moved to the NEEDSINFO state and the datelastresponse |
2391 | -attribute is updated to the message timestamp. |
2392 | +The question is moved to the NEEDSINFO state and the last response date is |
2393 | +updated to the message's timestamp. |
2394 | |
2395 | >>> print question.status.name |
2396 | NEEDSINFO |
2397 | @@ -148,33 +152,35 @@ |
2398 | >>> print reply_message.owner.displayname |
2399 | No Privileges Person |
2400 | |
2401 | -The question is moved back to the OPEN state and the 'datelastquery' |
2402 | -attribute is updated to the message's creation date: |
2403 | +The question is moved back to the OPEN state and the last query date is |
2404 | +updated to the message's creation date. |
2405 | |
2406 | >>> print question.status.name |
2407 | OPEN |
2408 | >>> question.datelastquery == now_plus_two_hours |
2409 | True |
2410 | |
2411 | -The other user has now enough information to give an answer to the |
2412 | -question. The giveAnswer() method is used for that purpose. Like the |
2413 | -requestInfo() method, it takes two mandatory parameters: the user |
2414 | -providing the answer and the answer itself. |
2415 | +Now, the other user has enough information to give an answer to the question. |
2416 | +The giveAnswer() method is used for that purpose. Like the requestInfo() |
2417 | +method, it takes two mandatory parameters: the user providing the answer and |
2418 | +the answer itself. |
2419 | |
2420 | >>> login('test@canonical.com') |
2421 | >>> now_plus_three_hours = now + timedelta(hours=3) |
2422 | >>> answer_message = question.giveAnswer( |
2423 | - ... sample_person, "You need some configuration on the Mac side " |
2424 | + ... sample_person, |
2425 | + ... "You need some configuration on the Mac side " |
2426 | ... "to boot the installer on that model. Consult " |
2427 | ... "https://help.ubuntu.com/community/Installation/OldWorldMacs " |
2428 | - ... "for all the details.", datecreated=now_plus_three_hours) |
2429 | + ... "for all the details.", |
2430 | + ... datecreated=now_plus_three_hours) |
2431 | >>> print answer_message.action.name |
2432 | ANSWER |
2433 | >>> print answer_message.new_status.name |
2434 | ANSWERED |
2435 | |
2436 | -After that action, the question's status is changed to ANSWERED and the |
2437 | -datelastresponse is updated to contain the date of the message. |
2438 | +The question's status is changed to ANSWERED and the last response date is |
2439 | +updated to contain the date of the message. |
2440 | |
2441 | >>> print question.status.name |
2442 | ANSWERED |
2443 | @@ -182,9 +188,8 @@ |
2444 | True |
2445 | |
2446 | At that point, the question is considered answered, but we don't have |
2447 | -feedback from the user on whether it solved his problem or not. If it |
2448 | -doesn't the user can reopen the question. The reopen() method is used |
2449 | -for that purpose. |
2450 | +feedback from the user on whether it solved his problem or not. If it |
2451 | +doesn't, the user can reopen the question. |
2452 | |
2453 | >>> login('no-priv@canonical.com') |
2454 | >>> tomorrow = now + timedelta(days=1) |
2455 | @@ -200,38 +205,37 @@ |
2456 | >>> print reopen_message.owner.displayname |
2457 | No Privileges Person |
2458 | |
2459 | -This moves back the question to the OPEN state and the datelastquery |
2460 | -attribute is updated to the message creation date. |
2461 | +This moves back the question to the OPEN state and the last query date is |
2462 | +updated to the message's creation date. |
2463 | |
2464 | >>> print question.status.name |
2465 | OPEN |
2466 | >>> question.datelastquery == tomorrow |
2467 | True |
2468 | |
2469 | -The giveAnswer() will again be used to give an answer. |
2470 | +Once again, an answer is given. |
2471 | |
2472 | >>> login('test@canonical.com') |
2473 | >>> tomorrow_plus_one_hour = tomorrow + timedelta(hours=1) |
2474 | >>> answer2_message = question.giveAnswer( |
2475 | - ... marilize, "You probably do not have enough RAM to use the " |
2476 | + ... marilize, |
2477 | + ... "You probably do not have enough RAM to use the " |
2478 | ... "graphical installer. You can try the alternate CD with the " |
2479 | ... "text installer.") |
2480 | |
2481 | -This again moves the question to the ANSWERED state. |
2482 | +The question is moved back to the ANSWERED state. |
2483 | |
2484 | >>> print question.status.name |
2485 | ANSWERED |
2486 | |
2487 | -The question owner will hopefully come back to confirm that his |
2488 | -problem is solved. He can specify which answer message helped him |
2489 | -solved his problem. The confirmAnswer() method is used for that |
2490 | -purpose. |
2491 | +The question owner will hopefully come back to confirm that his problem is |
2492 | +solved. He can specify which answer message helped him solved his problem. |
2493 | |
2494 | >>> login('no-priv@canonical.com') |
2495 | >>> two_weeks_from_now = now + timedelta(days=14) |
2496 | >>> confirm_message = question.confirmAnswer( |
2497 | ... "I upgraded to 512M of RAM (found on eBay) and I've " |
2498 | - ... "succesfully managed to install Ubuntu. Thanks for all the help.", |
2499 | + ... "successfully managed to install Ubuntu. Thanks for all the help.", |
2500 | ... datecreated=two_weeks_from_now, answer=answer_message) |
2501 | >>> print confirm_message.action.name |
2502 | CONFIRM |
2503 | @@ -240,9 +244,9 @@ |
2504 | >>> print confirm_message.owner.displayname |
2505 | No Privileges Person |
2506 | |
2507 | -The question is moved to the SOLVED state, the message that solved |
2508 | -the question is saved in the answer attribute, the date_solved |
2509 | -and answerer attributes are also updated. |
2510 | +The question is moved to the SOLVED state, and the message that solved the |
2511 | +question is saved. The date the question was solved and answerer are also |
2512 | +updated. |
2513 | |
2514 | >>> print question.status.name |
2515 | SOLVED |
2516 | @@ -254,45 +258,43 @@ |
2517 | True |
2518 | |
2519 | |
2520 | -== 2) Self-answer == |
2521 | - |
2522 | -Another scenario is for the case when the user comes back to give the |
2523 | -solution to the question himself. The giveAnswer() method is also used |
2524 | -for that case. The question owner can choose a best answer message |
2525 | -later on. The workflow permits the question owner to choose an answer |
2526 | -before or after the question status is set to SOLVED. |
2527 | - |
2528 | -The question owner creates a question. |
2529 | +2) Self-answering |
2530 | +================= |
2531 | + |
2532 | +In this scenario the user comes back to give the solution to the question |
2533 | +himself. The question owner can choose a best answer message later on. The |
2534 | +workflow permits the question owner to choose an answer before or after the |
2535 | +question status is set to SOLVED. |
2536 | + |
2537 | +A new question is posed. |
2538 | |
2539 | >>> question = ubuntu.newQuestion(**new_question_args) |
2540 | |
2541 | -The question answer provides an answer that eludes to a decision |
2542 | -the question owner must make. |
2543 | +The answer provides some useful information to the questioner. |
2544 | |
2545 | >>> login('test@canonical.com') |
2546 | >>> tomorrow_plus_one_hour = tomorrow + timedelta(hours=1) |
2547 | >>> alt_answer_message = question.giveAnswer( |
2548 | - ... marilize, "Are you using a pre-G3 Mac? They are very difficult " |
2549 | + ... marilize, |
2550 | + ... "Are you using a pre-G3 Mac? They are very difficult " |
2551 | ... "to install to. You must mess with the hardware to trick " |
2552 | ... "the core chips to let it install. You may not want to do this.") |
2553 | |
2554 | -The question owner logs in, and explains that he has researched the |
2555 | -problem, and come to a solution. |
2556 | +The question has researched the problem, and has comes to a solution himself. |
2557 | |
2558 | >>> login('no-priv@canonical.com') |
2559 | >>> self_answer_message = question.giveAnswer( |
2560 | - ... no_priv, "I found some instructions on the Wiki on how to " |
2561 | + ... no_priv, |
2562 | + ... "I found some instructions on the Wiki on how to " |
2563 | ... "install BootX to boot the installation CD on OldWorld Mac: " |
2564 | ... "https://help.ubuntu.com/community/Installation/OldWorldMacs " |
2565 | ... "This is complicated and since it's a very old machine, not " |
2566 | ... "worth the trouble.", |
2567 | ... datecreated=now_plus_one_hour) |
2568 | |
2569 | -In that case, the question owner is considered to have given |
2570 | -information that the problem is solved and the question is moved to |
2571 | -the SOLVED state. The 'answerer' attribute will be the question owner, |
2572 | -the 'date_solved' date of the message, but the 'answer' attribute |
2573 | -will None. |
2574 | +The question owner is considered to have given information that the problem is |
2575 | +solved and the question is moved to the SOLVED state. The 'answerer' |
2576 | +will be the question owner. |
2577 | |
2578 | >>> print self_answer_message.action.name |
2579 | CONFIRM |
2580 | @@ -305,20 +307,20 @@ |
2581 | No Privileges Person |
2582 | >>> question.date_solved == now_plus_one_hour |
2583 | True |
2584 | - >>> question.answer is None |
2585 | - True |
2586 | + >>> print question.answer |
2587 | + None |
2588 | |
2589 | -The question owner can still specify which message helped him solved |
2590 | -his problem. The confirmAnswer() method is used when the question |
2591 | -owner chooses another user's answer as a best answer. The status |
2592 | -will remain SOLVED. The 'answerer' attribute will be the message |
2593 | -owner, and the 'answer' will be the message. The question's |
2594 | -'date_solved' attribute will be the date of the answer message. |
2595 | +The question owner can still specify which message helped him solved his |
2596 | +problem. The confirmAnswer() method is used when the question owner chooses |
2597 | +another user's answer as a best answer. The status will remain SOLVED. The |
2598 | +'answerer' will be the message owner, and the 'answer' will be the message. |
2599 | +The question's solution date will be the date of the answer message. |
2600 | |
2601 | >>> confirm_message = question.confirmAnswer( |
2602 | ... "Thanks Marilize for your help. I don't think I'll put Ubuntu " |
2603 | ... "Ubuntu on my Mac.", |
2604 | - ... datecreated=now_plus_one_hour, answer=alt_answer_message) |
2605 | + ... datecreated=now_plus_one_hour, |
2606 | + ... answer=alt_answer_message) |
2607 | >>> print confirm_message.action.name |
2608 | CONFIRM |
2609 | >>> print confirm_message.new_status.name |
2610 | @@ -336,17 +338,18 @@ |
2611 | True |
2612 | |
2613 | |
2614 | -== 3) The question expires == |
2615 | +3) The question expires |
2616 | +======================= |
2617 | |
2618 | -Another case is when nobody comes to answer the message, either because |
2619 | -the question is too complex or too vague. These questions can be expired |
2620 | -by using the expireQuestion() method. (See answer-tracker-expiration.txt |
2621 | -for the documentation of the cron script handling this task.) |
2622 | +It is also possible that nobody will answer the question, either because the |
2623 | +question is too complex or too vague. These questions are expired by using |
2624 | +the expireQuestion() method. |
2625 | |
2626 | >>> login('no-priv@canonical.com') |
2627 | >>> question = ubuntu.newQuestion(**new_question_args) |
2628 | >>> expire_message = question.expireQuestion( |
2629 | - ... sample_person, "There was no activity on this question for two " |
2630 | + ... sample_person, |
2631 | + ... "There was no activity on this question for two " |
2632 | ... "weeks and this question was expired. If you are still having " |
2633 | ... "this problem you should reopen the question and provide more " |
2634 | ... "information about your problem.", |
2635 | @@ -356,8 +359,8 @@ |
2636 | >>> print expire_message.new_status.name |
2637 | EXPIRED |
2638 | |
2639 | -The question is moved to the EXPIRED state and the 'datelastresponse' |
2640 | -attribute is updated to the message creation date. |
2641 | +The question is moved to the EXPIRED state and the last response date is |
2642 | +updated to the message creation date. |
2643 | |
2644 | >>> print question.status.name |
2645 | EXPIRED |
2646 | @@ -376,8 +379,8 @@ |
2647 | >>> print reopen_message.action.name |
2648 | REOPEN |
2649 | |
2650 | -The question status is changed back to OPEN and the 'datelastquery' |
2651 | -attribute is updated. |
2652 | +The question status is changed back to OPEN and the last query date is |
2653 | +updated. |
2654 | |
2655 | >>> print question.status.name |
2656 | OPEN |
2657 | @@ -385,22 +388,22 @@ |
2658 | True |
2659 | |
2660 | |
2661 | -== 4) The question is invalid == |
2662 | +4) The question is invalid |
2663 | +========================== |
2664 | |
2665 | -Another scenario to handle is the case where the user posts a message |
2666 | -that isn't really appropriate for the Answer Tracker like a SPAM |
2667 | +In this scenario the user posts an inappropriate message, such as a spam |
2668 | message or a request for Ubuntu CDs. |
2669 | |
2670 | >>> spam_question = ubuntu.newQuestion( |
2671 | ... no_priv, 'CDs', 'Please send 10 Ubuntu Dapper CDs.', |
2672 | ... datecreated=now) |
2673 | |
2674 | -The reject() method is used for such purpose. Only an answer contact, |
2675 | -a product or distribution owner, or an administrator can reject a question. |
2676 | +Such questions can be rejected by an answer contact, a product or distribution |
2677 | +owner, or a Launchpad administrator. |
2678 | |
2679 | -The canReject() method can be used to test if a user is allowed to |
2680 | -reject the question. It takes as parameter the user who would reject the |
2681 | -question: |
2682 | +The canReject() method can be used to test if a user is allowed to reject the |
2683 | +question. While neither No Privileges Person nor Marilize are able to reject |
2684 | +questions, Sample Person and the Ubuntu owner can. |
2685 | |
2686 | >>> spam_question.canReject(no_priv) |
2687 | False |
2688 | @@ -413,7 +416,8 @@ |
2689 | >>> spam_question.canReject(ubuntu.owner) |
2690 | True |
2691 | |
2692 | - # Administrator |
2693 | +As a Launchpad administrator, so can Stub. |
2694 | + |
2695 | >>> spam_question.canReject(stub) |
2696 | True |
2697 | |
2698 | @@ -424,8 +428,7 @@ |
2699 | ... |
2700 | Unauthorized: ... |
2701 | |
2702 | -The reject() method takes a comment explaining the reason behind the |
2703 | -rejection. |
2704 | +When rejecting a question, a comment explaining the reason is given. |
2705 | |
2706 | >>> login('test@canonical.com') |
2707 | >>> reject_message = spam_question.reject( |
2708 | @@ -436,8 +439,8 @@ |
2709 | >>> print reject_message.new_status.name |
2710 | INVALID |
2711 | |
2712 | -After rejection, the question is marked as invalid and the |
2713 | -'datelastresponse' attribute is updated. |
2714 | +After rejection, the question is marked as invalid and the last response date |
2715 | +is updated. |
2716 | |
2717 | >>> print spam_question.status.name |
2718 | INVALID |
2719 | @@ -445,7 +448,7 @@ |
2720 | True |
2721 | |
2722 | The rejection message is also considered as answering the message, so the |
2723 | -date_solved, answerer and answer attributes are also updated. |
2724 | +solution date, answerer, and answer are also updated. |
2725 | |
2726 | >>> spam_question.answer == reject_message |
2727 | True |
2728 | @@ -454,23 +457,27 @@ |
2729 | >>> spam_question.date_solved == now_plus_one_hour |
2730 | True |
2731 | |
2732 | -== Other scenarios == |
2733 | - |
2734 | -Many other scenarios are possible and some of those are probably more |
2735 | -common than the ones we exposed. For example, it is likely that a user |
2736 | -will answer directly a question (without asking for other |
2737 | -information first). Or that the question user won't come back to confirm |
2738 | -that an answer solved his problem. Another likely scenario is where |
2739 | -the question will expire in the NEEDSINFO state when the question owner |
2740 | -doesn't reply to the request for more information. All of these |
2741 | -scenarios are covered by this API. It is not necessary to cover all |
2742 | -these various possibilities here. |
2743 | -(The ../interfaces/ftests/test_question_workflow.py functional test |
2744 | -exercices all the various possible transitions.) |
2745 | - |
2746 | -== Changing the question status == |
2747 | - |
2748 | -It is not possible to change the status attribute directly: |
2749 | + |
2750 | +Other scenarios |
2751 | +=============== |
2752 | + |
2753 | +Many other scenarios are possible and some are likely more common than others. |
2754 | +For example, it is likely that a user will directly answer a question without |
2755 | +asking for other information first. Sometimes, the original questioner won't |
2756 | +come back to confirm that an answer solved his problem. |
2757 | + |
2758 | +Another likely scenario is where the question will expire in the NEEDSINFO |
2759 | +state because the question owner doesn't reply to the request for more |
2760 | +information. All of these scenarios are covered by this API, though it is not |
2761 | +necessary to cover all these various possibilities here. (The |
2762 | +../tests/test_question_workflow.py functional test exercises all the various |
2763 | +possible transitions.) |
2764 | + |
2765 | + |
2766 | +Changing the question status |
2767 | +============================ |
2768 | + |
2769 | +It is not possible to change the status attribute directly. |
2770 | |
2771 | >>> login('foo.bar@canonical.com') |
2772 | >>> question = ubuntu.newQuestion(**new_question_args) |
2773 | @@ -479,10 +486,9 @@ |
2774 | ... |
2775 | ForbiddenAttribute... |
2776 | |
2777 | -A user which has launchpad.Admin permission on the question, can set the |
2778 | -question status to an arbitrary value by using the setStatus() method. |
2779 | -That method takes as parameters the new status and a comment explaining |
2780 | -the status change. |
2781 | +A user having launchpad.Admin permission on the question can set the question |
2782 | +status to an arbitrary value, by giving the new status and a comment |
2783 | +explaining the status change. |
2784 | |
2785 | >>> old_datelastquery = question.datelastquery |
2786 | >>> login(stub.preferredemail.email) |
2787 | @@ -490,7 +496,7 @@ |
2788 | ... stub, QuestionStatus.INVALID, 'Changed status to INVALID', |
2789 | ... datecreated=now_plus_one_hour) |
2790 | |
2791 | -The method returns the IQuestionMessage recording the change: |
2792 | +The method returns the IQuestionMessage recording the change. |
2793 | |
2794 | >>> print status_change_message.action.name |
2795 | SETSTATUS |
2796 | @@ -499,7 +505,7 @@ |
2797 | >>> print question.status.name |
2798 | INVALID |
2799 | |
2800 | -The status change updates the datelastresponse attribute: |
2801 | +The status change updates the last response date. |
2802 | |
2803 | >>> question.datelastresponse == now_plus_one_hour |
2804 | True |
2805 | @@ -507,7 +513,7 @@ |
2806 | True |
2807 | |
2808 | If an answer was present on the question, the status change also clears |
2809 | -the answer and date_solved attributes. |
2810 | +the answer and solution date. |
2811 | |
2812 | >>> msg = question.setStatus(stub, QuestionStatus.OPEN, 'Status change.') |
2813 | >>> answer_message = question.giveAnswer(sample_person, 'Install BootX.') |
2814 | @@ -524,13 +530,13 @@ |
2815 | ... stub, QuestionStatus.OPEN, 'Reopen the question', |
2816 | ... datecreated=now_plus_one_hour) |
2817 | |
2818 | - >>> question.date_solved is None |
2819 | - True |
2820 | - >>> question.answer is None |
2821 | - True |
2822 | + >>> print question.date_solved |
2823 | + None |
2824 | + >>> print question.answer |
2825 | + None |
2826 | |
2827 | -But when the status is changed by a user who doesn't have the |
2828 | -launchpad.Admin permission, an Unauthorized error is thrown: |
2829 | +When the status is changed by a user who doesn't have the launchpad.Admin |
2830 | +permission, an Unauthorized exception is thrown. |
2831 | |
2832 | >>> login('test@canonical.com') |
2833 | >>> question.setStatus(sample_person, QuestionStatus.EXPIRED, 'Expire.') |
2834 | @@ -538,10 +544,11 @@ |
2835 | ... |
2836 | Unauthorized... |
2837 | |
2838 | -== Adding Comments Without Changing the Status == |
2839 | - |
2840 | -There is an addComment() method that can be use to add a message to the |
2841 | -question without changing its status. |
2842 | + |
2843 | +Adding Comments Without Changing the Status |
2844 | +=========================================== |
2845 | + |
2846 | +Comments can be added to questions without changing the question's status. |
2847 | |
2848 | >>> login('no-priv@canonical.com') |
2849 | >>> old_status = question.status |
2850 | @@ -556,8 +563,7 @@ |
2851 | >>> comment.new_status == old_status |
2852 | True |
2853 | |
2854 | -This method does not update the datelastresponse and datelastquery |
2855 | -attributes. |
2856 | +This method does not update the last response date or last query date. |
2857 | |
2858 | >>> question.datelastresponse == old_datelastresponse |
2859 | True |
2860 | @@ -565,19 +571,20 @@ |
2861 | True |
2862 | |
2863 | |
2864 | -== Setting the question assignee == |
2865 | +Setting the question assignee |
2866 | +============================= |
2867 | |
2868 | Users with launchpad.Moderator privileges, which are answer contacts, |
2869 | question target owners, and admins, can assign someone to answer a question. |
2870 | |
2871 | -Sample Person is an answer contact for ubuntu. He can set the assignee. |
2872 | +Sample Person is an answer contact for ubuntu, so he can set the assignee. |
2873 | |
2874 | >>> login('test@canonical.com') |
2875 | >>> question.assignee = stub |
2876 | >>> print question.assignee.displayname |
2877 | Stuart Bishop |
2878 | |
2879 | -Users without launchpad.Moderator privileges cannot set the assignee |
2880 | +Users without launchpad.Moderator privileges cannot set the assignee. |
2881 | |
2882 | >>> login('no-priv@canonical.com') |
2883 | >>> question.assignee = sample_person |
2884 | @@ -586,16 +593,18 @@ |
2885 | Unauthorized: (<Question ...>, 'assignee', 'launchpad.Moderate') |
2886 | |
2887 | |
2888 | -== Events == |
2889 | +Events |
2890 | +====== |
2891 | |
2892 | Each of the workflow methods will trigger a ObjectCreatedEvent for |
2893 | the message they create and a ObjectModifiedEvent for the question. |
2894 | |
2895 | - # Register an event listener that will print event it receives. |
2896 | + # Register an event listener that will print events it receives. |
2897 | >>> from lazr.lifecycle.interfaces import ( |
2898 | ... IObjectCreatedEvent, IObjectModifiedEvent) |
2899 | - >>> from canonical.launchpad.interfaces import IQuestion |
2900 | - >>> from canonical.launchpad.ftests.event import TestEventListener |
2901 | + >>> from lp.answers.interfaces.question import IQuestion |
2902 | + >>> from canonical.lazr.testing.event import TestEventListener |
2903 | + |
2904 | >>> def print_event(object, event): |
2905 | ... print "Received %s on %s" % ( |
2906 | ... event.__class__.__name__.split('.')[-1], |
2907 | @@ -605,14 +614,15 @@ |
2908 | >>> question_event_listener = TestEventListener( |
2909 | ... IQuestion, IObjectModifiedEvent, print_event) |
2910 | |
2911 | -Changing the status triggers the event: |
2912 | +Changing the status triggers the event. |
2913 | |
2914 | >>> login(stub.preferredemail.email) |
2915 | - >>> msg = question.setStatus(stub, QuestionStatus.EXPIRED, 'Status change.') |
2916 | + >>> msg = question.setStatus( |
2917 | + ... stub, QuestionStatus.EXPIRED, 'Status change.') |
2918 | Received ObjectCreatedEvent on QuestionMessage |
2919 | Received ObjectModifiedEvent on Question |
2920 | |
2921 | -Example of a workflow method that triggers the events: |
2922 | +Rejecting the question triggers the events. |
2923 | |
2924 | >>> msg = question.reject(stub, 'Close this question.') |
2925 | Received ObjectCreatedEvent on QuestionMessage |
2926 | @@ -630,25 +640,28 @@ |
2927 | >>> questionmessage_event_listener.unregister() |
2928 | >>> question_event_listener.unregister() |
2929 | |
2930 | -== Reopenings == |
2931 | + |
2932 | +Reopening the question |
2933 | +====================== |
2934 | |
2935 | Whenever a question considered answered (in the SOLVED or INVALID state) |
2936 | is reopened, a QuestionReopening is created. |
2937 | |
2938 | - # Let's register an event listener to notify us whenever a |
2939 | - # QuestionReopening is created. |
2940 | - >>> from canonical.launchpad.interfaces import IQuestionReopening |
2941 | + # Register an event listener to notify us whenever a QuestionReopening is |
2942 | + # created. |
2943 | + >>> from lp.answers.interfaces.questionreopening import IQuestionReopening |
2944 | >>> reopening_event_listener = TestEventListener( |
2945 | ... IQuestionReopening, IObjectCreatedEvent, print_event) |
2946 | |
2947 | The most common use case is when a user confirms a solution, and then |
2948 | -comes back to say that it doesn't work in fact. |
2949 | +comes back to say that it doesn't, in fact, work. |
2950 | |
2951 | >>> login('no-priv@canonical.com') |
2952 | >>> question = ubuntu.newQuestion(**new_question_args) |
2953 | >>> answer_message = question.giveAnswer( |
2954 | - ... sample_person, "You need some setup on the Mac side. " |
2955 | - ... "Follow the instructions at " |
2956 | + ... sample_person, |
2957 | + ... "You need some setup on the Mac side. " |
2958 | + ... "Follow the instructions at " |
2959 | ... "https://help.ubuntu.com/community/Installation/OldWorldMacs", |
2960 | ... datecreated=now_plus_one_hour) |
2961 | >>> confirm_message = question.confirmAnswer( |
2962 | @@ -662,23 +675,23 @@ |
2963 | |
2964 | The reopening record is available through the reopenings attribute. |
2965 | |
2966 | - >>> list(question.reopenings) |
2967 | - [<QuestionReopening...>] |
2968 | - >>> reopening = question.reopenings[0] |
2969 | + >>> reopenings = list(question.reopenings) |
2970 | + >>> len(reopenings) |
2971 | + 1 |
2972 | + >>> reopening = reopenings[0] |
2973 | >>> verifyObject(IQuestionReopening, reopening) |
2974 | True |
2975 | |
2976 | -The reopening contain the date of the reopening in the datecreated |
2977 | -attribute and the person who made the reopening in the reopener |
2978 | -attribute. |
2979 | +The reopening contain the date of the reopening, and the person who cause the |
2980 | +reopening to happen. |
2981 | |
2982 | >>> reopening.datecreated == now_plus_three_hours |
2983 | True |
2984 | >>> print reopening.reopener.displayname |
2985 | No Privileges Person |
2986 | |
2987 | -It contains the question prior answerer, datecreated, as well as the |
2988 | -prior status in the priorstate attribute: |
2989 | +It also contains the question's prior answerer, the date created, and the |
2990 | +prior status of the question. |
2991 | |
2992 | >>> print reopening.answerer.displayname |
2993 | Sample Person |
2994 | @@ -687,8 +700,8 @@ |
2995 | >>> print reopening.priorstate.name |
2996 | SOLVED |
2997 | |
2998 | -Another example of a reopening, would be when the question status is set |
2999 | -back to OPEN after having been rejected. |
3000 | +A reopening also occurs when the question status is set back to OPEN after |
3001 | +having been rejected. |
3002 | |
3003 | >>> login('test@canonical.com') |
3004 | >>> question = ubuntu.newQuestion(**new_question_args) |
3005 | @@ -698,7 +711,8 @@ |
3006 | |
3007 | >>> login(stub.preferredemail.email) |
3008 | >>> status_change_message = question.setStatus( |
3009 | - ... stub, QuestionStatus.OPEN, 'Disregard previous rejection. ' |
3010 | + ... stub, QuestionStatus.OPEN, |
3011 | + ... 'Disregard previous rejection. ' |
3012 | ... 'Sample Person was having a bad day.', |
3013 | ... datecreated=now_plus_two_hours) |
3014 | Received ObjectCreatedEvent on QuestionReopening |
3015 | @@ -718,7 +732,9 @@ |
3016 | # Cleanup |
3017 | >>> reopening_event_listener.unregister() |
3018 | |
3019 | -== Using an IMessage as Explanation == |
3020 | + |
3021 | +Using an IMessage as an explanation |
3022 | +=================================== |
3023 | |
3024 | In all the workflow methods, it is possible to pass an IMessage instead of |
3025 | a string. |
3026 | @@ -729,7 +745,7 @@ |
3027 | >>> question = ubuntu.newQuestion(**new_question_args) |
3028 | >>> reject_message = messageset.fromText( |
3029 | ... 'Reject', 'Because I feel like it.', sample_person) |
3030 | - >>> question_message = question.reject(sample_person,reject_message) |
3031 | + >>> question_message = question.reject(sample_person, reject_message) |
3032 | >>> print question_message.subject |
3033 | Reject |
3034 | >>> print question_message.text_contents |
3035 | @@ -737,7 +753,7 @@ |
3036 | >>> question_message.rfc822msgid == reject_message.rfc822msgid |
3037 | True |
3038 | |
3039 | -The IMessage owner must be the same than the person passed to the workflow |
3040 | +The IMessage owner must be the same as the person passed to the workflow |
3041 | method. |
3042 | |
3043 | >>> login(stub.preferredemail.email) |
3044 | |
3045 | === modified file 'lib/lp/answers/interfaces/questionreopening.py' |
3046 | --- lib/lp/answers/interfaces/questionreopening.py 2009-06-24 23:10:46 +0000 |
3047 | +++ lib/lp/answers/interfaces/questionreopening.py 2010-02-10 15:24:19 +0000 |
3048 | @@ -20,6 +20,7 @@ |
3049 | from lp.answers.interfaces.question import IQuestion |
3050 | from lp.answers.interfaces.questionenums import QuestionStatus |
3051 | |
3052 | + |
3053 | class IQuestionReopening(Interface): |
3054 | """A record of the re-opening of a question. |
3055 |
Maybe the diff will be sane if we merge into db-devel