Merge lp:~barry/launchpad/rnr-techdebt into lp:launchpad/db-devel
- rnr-techdebt
- Merge into db-devel
| Status: | Merged |
|---|---|
| Approved by: | Curtis Hovey on 2010-02-09 |
| 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 on 2010-02-09 | |
|
Review via email:
|
|||
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