~launchpad-pqm/launchpad/devel

7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
1
=========================
2
IQuestionTarget interface
3
=========================
4
5
Launchpad includes an answer tracker.  Questions are associated to objects
6
implementing IQuestionTarget.
7
8
    # An IQuestionTarget object is made available to this test via the
9
    # 'target' variable by the test framework.  It won't have any questions
10
    # associated with it at the start of the test.  This is done because the
11
    # exact same test applies to all types of question targets: products,
12
    # distributions, and distribution source packages.
13
    #
3691.398.18 by Francis J. Lacoste
Rename interfaces.
14
    # Some parts of the IQuestionTarget interface are only accessible
3691.235.2 by Francis J. Lacoste
Convert headers to moin style and comments in () to code comments.
15
    # to a registered user.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
16
    >>> login('no-priv@canonical.com')
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
17
18
    >>> from zope.component import getUtility
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
19
    >>> from zope.interface.verify import verifyObject
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
20
    >>> from lp.answers.interfaces.questiontarget import IQuestionTarget
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
21
3691.398.18 by Francis J. Lacoste
Rename interfaces.
22
    >>> verifyObject(IQuestionTarget, target)
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
23
    True
24
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
25
26
New questions
27
=============
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
28
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
29
Questions are always owned by a registered user.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
30
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
31
    >>> from lp.registry.interfaces.person import IPersonSet
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
32
    >>> sample_person = getUtility(IPersonSet).getByEmail(
33
    ...     'test@canonical.com')
34
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
35
The newQuestion() method is used to create a question that will be associated
36
with the target.  It takes as parameters the question's owner, title and
37
description.  It also takes an optional parameter 'datecreated' which defaults
38
to UTC_NOW.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
39
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
40
    # Initialize 'now' to a known value.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
41
    >>> from datetime import datetime, timedelta
42
    >>> from pytz import UTC
43
    >>> now = datetime.now(UTC)
44
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
45
    >>> question = target.newQuestion(sample_person, 'New question',
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
46
    ...     'Question description', datecreated=now)
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
47
    >>> print question.title
48
    New question
49
    >>> print question.description
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
50
    Question description
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
51
    >>> print question.owner.displayname
52
    Sample Person
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
53
    >>> question.datecreated == now
54
    True
55
    >>> question.datelastquery == now
56
    True
57
58
The created question starts in the 'Open' status and should have the owner
59
subscribed to the question.
60
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
61
    >>> print question.status.title
62
    Open
63
64
    >>> for subscription in question.subscriptions:
65
    ...     print subscription.person.displayname
66
    Sample Person
67
68
Questions can be written in any languages supported in Launchpad.  The
69
language of the request is available in the 'language' attribute.  By default,
70
requests are assumed to be written in English.
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
71
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
72
    >>> print question.language.code
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
73
    en
74
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
75
It is possible to create questions in another language than English, by
76
passing in the language that the question is written in.
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
77
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
78
    >>> from lp.services.worlddata.interfaces.language import ILanguageSet
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
79
    >>> french = getUtility(ILanguageSet)['fr']
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
80
    >>> question = target.newQuestion(
81
    ...     sample_person, "De l'aide S.V.P.",
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
82
    ...     "Pouvez-vous m'aider?", language=french,
83
    ...     datecreated=now + timedelta(seconds=30))
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
84
    >>> print question.language.code
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
85
    fr
86
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
87
Anonymous users cannot use newQuestion().
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
88
89
    >>> login(ANONYMOUS)
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
90
    >>> question = target.newQuestion(
91
    ...     sample_person, 'This will fail', 'Failed?')
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
92
    Traceback (most recent call last):
93
      ...
94
    Unauthorized...
95
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
96
97
Retrieving questions
98
====================
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
99
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
100
The getQuestion() method is used to retrieve a question by id for a
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
101
particular target.
102
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
103
    >>> target.getQuestion(question.id) == question
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
104
    True
105
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
106
If you pass in a non-existent id or a question for a different target, the
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
107
method returns None.
108
109
    >>> print target.getQuestion(2)
110
    None
111
    >>> print target.getQuestion(12345)
112
    None
113
114
115
Searching for questions
116
=======================
117
118
    # Create new questions for the following tests.  Odd questions will be
119
    # owned by Foo Bar and even questions will be owned by Sample Person.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
120
    >>> login('no-priv@canonical.com')
3691.188.1 by Francis J. Lacoste
Add owner criteria to searchTickets
121
    >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
122
    >>> questions = []
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
123
    >>> for num in range(5):
3691.188.1 by Francis J. Lacoste
Add owner criteria to searchTickets
124
    ...     if num % 2:
125
    ...         owner = foo_bar
126
    ...     else:
127
    ...         owner = sample_person
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
128
    ...     description = ('Support request description%d.\n'
129
    ...         'This request index is %d.') % (num, num)
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
130
    ...     questions.append(target.newQuestion(
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
131
    ...         owner, 'Question title%d' % num, description,
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
132
    ...         datecreated=now+timedelta(minutes=num+1)))
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
133
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
134
    # For more variety, we will set the status of the last to INVALID and the
135
    # fourth one to ANSWERED.
3691.197.104 by Francis J. Lacoste
- Protect setStatus by launchpad.Admin.
136
    >>> login('foo.bar@canonical.com')
3691.197.16 by Francis J. Lacoste
- Add setStatus() and addComment() methods.
137
    >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
138
    >>> message = questions[-1].reject(
139
    ...     foo_bar, 'Invalid question.', datecreated=now+timedelta(hours=1))
140
    >>> message = questions[3].giveAnswer(
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
141
    ...     sample_person, 'This is your answer.',
142
    ...     datecreated=now+timedelta(hours=1))
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
143
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
144
    # Also add a reply from the owner on the first of these.
3691.345.5 by Francis J. Lacoste
Add TicketSort.RECENT_OWNER_ACTIVITY.
145
    >>> login('test@canonical.com')
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
146
    >>> message = questions[0].giveInfo(
3691.345.5 by Francis J. Lacoste
Add TicketSort.RECENT_OWNER_ACTIVITY.
147
    ...     'I think I forgot something.', datecreated=now+timedelta(hours=4))
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
148
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
149
    # Create another one that will also have the word 'new' in its
150
    # description.
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
151
    >>> question = target.newQuestion(sample_person, 'Another question',
152
    ...     'Another new question that is actually very new.',
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
153
    ...     datecreated=now+timedelta(hours=1))
154
    >>> login(ANONYMOUS)
155
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
156
The searchQuestions() method is used to search for questions.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
157
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
158
159
Search text
160
-----------
3691.235.3 by Francis J. Lacoste
Add section headers for searchTickets() parameters.
161
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
162
The search_text parameter will select the questions that contain the
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
163
passed in text.  The standard text searching algorithm is used; see
164
../../../canonical/launchpad/doct/textsearching.txt.
3691.235.3 by Francis J. Lacoste
Add section headers for searchTickets() parameters.
165
3691.398.20 by Francis J. Lacoste
Rename all methods.
166
    >>> for t in target.searchQuestions(search_text='new'):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
167
    ...     print t.title
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
168
    New question
169
    Another question
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
170
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
171
The results are sorted by relevancy.  In the last questions, 'New' appeared in
172
the description which makes it less relevant than when the word appears in the
173
title.
174
175
176
Status
177
------
178
179
The searchQuestions() method can also filter questions by status.
180
12915.5.1 by Curtis Hovey
Rename questionenums.py to the new standard or enums.py
181
    >>> from lp.answers.enums import QuestionStatus
3691.398.20 by Francis J. Lacoste
Rename all methods.
182
    >>> for t in target.searchQuestions(status=QuestionStatus.OPEN):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
183
    ...     print t.title
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
184
    Another question
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
185
    Question title2
186
    Question title1
187
    Question title0
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
188
    De l'aide S.V.P.
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
189
    New question
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
190
191
In this previous example, because there is no sort text, the
192
default sort order is from newest to oldest.
193
3691.398.20 by Francis J. Lacoste
Rename all methods.
194
    >>> for t in target.searchQuestions(status=QuestionStatus.INVALID):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
195
    ...     print t.title
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
196
    Question title4
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
197
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
198
You can pass in a list of statuses, and you can also use the search_text and
199
status parameters at the same time.  This will search OPEN and INVALID
200
questions with the word 'index'.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
201
3691.398.20 by Francis J. Lacoste
Rename all methods.
202
    >>> for t in target.searchQuestions(search_text='request index',
3691.398.17 by Francis J. Lacoste
Rename dbschemas.
203
    ...         status=(QuestionStatus.OPEN, QuestionStatus.INVALID)):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
204
    ...     print t.title
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
205
    Question title4
206
    Question title2
207
    Question title1
208
    Question title0
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
209
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
210
211
Sorting
212
-------
213
214
You can control the sort order by passing one of the constants defined in
215
QuestionSort.  Previously, we saw the NEWEST_FIRST and RELEVANCY sort order.
216
217
You can sort also from oldest to newest using the OLDEST_FIRST constant.
218
12915.5.1 by Curtis Hovey
Rename questionenums.py to the new standard or enums.py
219
    >>> from lp.answers.enums import QuestionSort
3691.398.20 by Francis J. Lacoste
Rename all methods.
220
    >>> for t in target.searchQuestions(search_text='new',
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
221
    ...                                 sort=QuestionSort.OLDEST_FIRST):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
222
    ...     print t.title
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
223
    New question
224
    Another question
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
225
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
226
You can sort by status (the status order is OPEN, NEEDSINFO, ANSWERED, SOLVED,
227
EXPIRED, INVALID).  This also sorts from newest to oldest as a secondary key.
228
Here we use status=None to search for all statuses; by default INVALID and
229
EXPIRED questions are excluded.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
230
3691.398.20 by Francis J. Lacoste
Rename all methods.
231
    >>> for t in target.searchQuestions(search_text='request index',
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
232
    ...                                 status=None,
233
    ...                                 sort=QuestionSort.STATUS):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
234
    ...     print t.status.title, t.title
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
235
    Open Question title2
236
    Open Question title1
237
    Open Question title0
238
    Answered Question title3
239
    Invalid Question title4
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
240
241
If there is no search_text and the requested sort order is RELEVANCY,
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
242
the questions will be sorted NEWEST_FIRST.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
243
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
244
    # 'Question title4' is not shown in this case because it has INVALID as
245
    # its status.
3691.398.20 by Francis J. Lacoste
Rename all methods.
246
    >>> for t in target.searchQuestions(sort=QuestionSort.RELEVANCY):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
247
    ...     print t.title
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
248
    Another question
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
249
    Question title3
250
    Question title2
251
    Question title1
252
    Question title0
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
253
    De l'aide S.V.P.
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
254
    New question
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
255
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
256
The RECENT_OWNER_ACTIVITY sort order sorts first questions which recently
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
257
received a new message by their owner.  It effectively sorts descending on the
258
datelastquery attribute.
3691.345.5 by Francis J. Lacoste
Add TicketSort.RECENT_OWNER_ACTIVITY.
259
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
260
    # Question title0 sorts first because it has a message from its owner
261
    # after the others were created.
262
    >>> for t in target.searchQuestions(
263
    ...                             sort=QuestionSort.RECENT_OWNER_ACTIVITY):
3691.345.5 by Francis J. Lacoste
Add TicketSort.RECENT_OWNER_ACTIVITY.
264
    ...     print t.title
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
265
    Question title0
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
266
    Another question
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
267
    Question title3
268
    Question title2
269
    Question title1
3691.345.5 by Francis J. Lacoste
Add TicketSort.RECENT_OWNER_ACTIVITY.
270
    De l'aide S.V.P.
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
271
    New question
3691.345.5 by Francis J. Lacoste
Add TicketSort.RECENT_OWNER_ACTIVITY.
272
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
273
274
Owner
275
-----
276
277
You can find question owned by a particular user by using the owner parameter.
3691.188.1 by Francis J. Lacoste
Add owner criteria to searchTickets
278
3691.398.20 by Francis J. Lacoste
Rename all methods.
279
    >>> for t in target.searchQuestions(owner=foo_bar):
3691.188.1 by Francis J. Lacoste
Add owner criteria to searchTickets
280
    ...     print t.title
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
281
    Question title3
282
    Question title1
3691.188.1 by Francis J. Lacoste
Add owner criteria to searchTickets
283
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
284
285
Language
286
---------
3691.235.17 by Francis J. Lacoste
Merge RF, resolving 5 conflicts.
287
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
288
The language criteria can be used to select only questions written in a
3691.258.8 by Francis J. Lacoste
Add test for language criteria to searchTickets().
289
particular language.
290
291
    >>> english = getUtility(ILanguageSet)['en']
3691.398.20 by Francis J. Lacoste
Rename all methods.
292
    >>> for t in target.searchQuestions(language=french):
3691.258.8 by Francis J. Lacoste
Add test for language criteria to searchTickets().
293
    ...     print t.title
294
    De l'aide S.V.P.
295
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
296
    >>> for t in target.searchQuestions(language=(english, french)):
3691.258.8 by Francis J. Lacoste
Add test for language criteria to searchTickets().
297
    ...     print t.title
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
298
    Another question
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
299
    Question title3
300
    Question title2
301
    Question title1
302
    Question title0
3691.258.8 by Francis J. Lacoste
Add test for language criteria to searchTickets().
303
    De l'aide S.V.P.
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
304
    New question
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
305
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
306
307
Questions needing attention
308
---------------------------
309
310
You can search among the questions that need attention.  A question needs the
311
attention of a user if he owns it and if it is in the NEEDSINFO or ANSWERED
312
state.  Questions on which the user gave an answer or requested for more
313
information, and that are back in the OPEN state, are also included.
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
314
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
315
    # One of Sample Person's question gets to need attention from Foo Bar.
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
316
    >>> login('foo.bar@canonical.com')
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
317
    >>> message = questions[0].requestInfo(
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
318
    ...     foo_bar, 'Do you have a clue?',
319
    ...     datecreated=now+timedelta(hours=1))
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
320
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
321
    >>> login('test@canonical.com')
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
322
    >>> message = questions[0].giveInfo(
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
323
    ...     'I do, now please help me.', datecreated=now+timedelta(hours=2))
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
324
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
325
    # Another one of Foo Bar's questions needs attention.
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
326
    >>> message = questions[1].requestInfo(
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
327
    ...     sample_person, 'And you, do you have a clue?',
328
    ...     datecreated=now+timedelta(hours=1))
329
330
    >>> login(ANONYMOUS)
3691.398.20 by Francis J. Lacoste
Rename all methods.
331
    >>> for t in target.searchQuestions(needs_attention_from=foo_bar):
3691.235.4 by Francis J. Lacoste
Added needs_attention_from parameter to ITicketTarget.searchTickets().
332
    ...     print t.status.title, t.title, t.owner.displayname
3691.398.19 by Francis J. Lacoste
Rename most remaining classes (views, database, authorizations, notifications, scripts.)
333
    Answered Question title3 Foo Bar
334
    Needs information Question title1 Foo Bar
335
    Open Question title0 Sample Person
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
336
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
337
338
Unsupported language
339
--------------------
4004.4.14 by Curtis Hovey
Revised code and added a test per review. I'm still not convinced that handling show_all_languages outside of a widget is right.
340
341
The 'unsupported' criteria is used to select questions that are in a
342
language that is not spoken by any of the Support Contacts.
343
4004.4.22 by Curtis Hovey
Changed unsupported to a boolean value in QuestionTargetSearch.
344
    >>> for t in target.searchQuestions(unsupported=True):
4004.4.14 by Curtis Hovey
Revised code and added a test per review. I'm still not convinced that handling show_all_languages outside of a widget is right.
345
    ...     print t.title
346
    De l'aide S.V.P.
347
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
348
349
Finding similar questions
350
=========================
351
352
The method findSimilarQuestions() can be use to find questions similar to some
353
target text.  The questions don't have to contain all the words of the text.
354
355
    # This returns the same results as with the search 'new' because
356
    # all other words in the text are either common ('question', 'title') or
357
    # stop words ('with', 'a').
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
358
    >>> for t in target.findSimilarQuestions('new questions with a title'):
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
359
    ...     print t.title
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
360
    New question
361
    Another question
11177.1.5 by Robert Collins
Fix fallout in tests.
362
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
363
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
364
365
Answer contacts
366
===============
367
368
Targets can have answer contacts.  The list of answer contacts for a
3691.398.20 by Francis J. Lacoste
Rename all methods.
369
target is available through the answer_contacts attribute.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
370
3935.3.12 by Curtis Hovey
Restored ftests/questiontarget.txt after removing the new sampledata.
371
    >>> list(target.answer_contacts)
372
    []
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
373
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
374
There is also a direct_answer_contacts which includes only the answer contacts
375
registered explicitly on the question target.  In general, this will be the
376
same as the answer_contacts attribute, but some IQuestionTarget
377
implementations may inherit answer contacts from other contexts.  In these
378
cases, the direct_answer_contacts attribute would only contain the answer
379
contacts defined in the current IQuestionTarget context.
3691.243.4 by Francis J. Lacoste
Add registered_support_contacts attribute.
380
3935.3.12 by Curtis Hovey
Restored ftests/questiontarget.txt after removing the new sampledata.
381
    >>> list(target.direct_answer_contacts)
382
    []
3691.243.4 by Francis J. Lacoste
Add registered_support_contacts attribute.
383
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
384
You add an answer contact by using the addAnswerContact() method.  This
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
385
is only available to registered users.
386
387
    >>> name18 = getUtility(IPersonSet).getByName('name18')
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
388
    >>> target.addAnswerContact(name18, name18)
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
389
    Traceback (most recent call last):
390
      ...
391
    Unauthorized...
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
392
393
This method returns True when the contact was added the list and False when it
394
was already on the list.
395
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
396
    >>> login('no-priv@canonical.com')
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
397
    >>> target.addAnswerContact(name18, name18)
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
398
    True
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
399
    >>> people = [p.name for p in target.answer_contacts]
400
    >>> len(people)
401
    1
402
    >>> print people[0]
403
    name18
404
    >>> people = [p.name for p in target.direct_answer_contacts]
405
    >>> len(people)
406
    1
407
    >>> print people[0]
408
    name18
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
409
    >>> target.addAnswerContact(name18, name18)
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
410
    False
411
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
412
An answer contact must have at least one language among his preferred
413
languages.
4215.2.15 by Curtis Hovey
Added English to Lp. Rvised tests; removing old rules and added new rule. Translation tests probably fail. This branch needs some cleaning too.
414
415
    >>> sample_person = getUtility(IPersonSet).getByName('name12')
7354.1.3 by Guilherme Salgado
Second aproach works.
416
    >>> len(sample_person.languages)
4215.2.15 by Curtis Hovey
Added English to Lp. Rvised tests; removing old rules and added new rule. Translation tests probably fail. This branch needs some cleaning too.
417
    0
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
418
    >>> target.addAnswerContact(sample_person, sample_person)
4215.2.15 by Curtis Hovey
Added English to Lp. Rvised tests; removing old rules and added new rule. Translation tests probably fail. This branch needs some cleaning too.
419
    Traceback (most recent call last):
420
      ...
12959.4.23 by Curtis Hovey
Always pass the subscribed_by argument to addAnswerContact.
421
    AddAnswerContactError: An answer contact must speak a language...
4215.2.23 by Curtis Hovey
Removed trailing whitespace.
422
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
423
Answer contacts can be removed by using the removeAnswerContact() method.
424
Like its counterpart, it returns True when the answer contact was removed and
425
False when the person wasn't on the answer contact list.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
426
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
427
    >>> target.removeAnswerContact(name18, name18)
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
428
    True
3935.3.12 by Curtis Hovey
Restored ftests/questiontarget.txt after removing the new sampledata.
429
    >>> list(target.answer_contacts)
430
    []
431
    >>> list(target.direct_answer_contacts)
432
    []
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
433
    >>> target.removeAnswerContact(name18, name18)
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
434
    False
435
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
436
Only registered users can remove an answer contact.
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
437
438
    >>> login(ANONYMOUS)
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
439
    >>> target.removeAnswerContact(name18, name18)
3691.110.79 by Francis J. Lacoste
Merge RF, resolving conflicts
440
    Traceback (most recent call last):
441
      ...
442
    Unauthorized...
3691.255.8 by Guilherme Salgado
Implementation of ITicketTarget.getSupportedLanguages(), which returns the languages spoken by that object's support contacts
443
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
444
445
Supported languages
446
===================
3691.255.14 by Francis J. Lacoste
Made language parameter optional.
447
3691.398.18 by Francis J. Lacoste
Rename interfaces.
448
The supported languages for a given IQuestionTarget are given by
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
449
getSupportedLanguages().  The supported languages of a question target include
450
all languages spoken by at least one of its answer contacts, with the
451
exception of all English variations since English is the assumed language for
452
support when there are no answer contacts.
453
454
    >>> codes = [lang.code for lang in target.getSupportedLanguages()]
455
    >>> len(codes)
456
    1
457
    >>> print codes[0]
458
    en
459
460
    # Let's add some answer contacts which speak different languages.
3691.255.8 by Guilherme Salgado
Implementation of ITicketTarget.getSupportedLanguages(), which returns the languages spoken by that object's support contacts
461
    >>> login('carlos@canonical.com')
462
    >>> carlos = getUtility(IPersonSet).getByName('carlos')
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
463
    >>> for language in carlos.languages:
464
    ...     print language.code
465
    ca
466
    en
467
    es
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
468
    >>> target.addAnswerContact(carlos, carlos)
4450.6.3 by Curtis Hovey
Reconciled the new English variant rule with the other question tests.
469
    True
3691.255.8 by Guilherme Salgado
Implementation of ITicketTarget.getSupportedLanguages(), which returns the languages spoken by that object's support contacts
470
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
471
While daf has en_GB as one of his preferred languages...
3935.3.12 by Curtis Hovey
Restored ftests/questiontarget.txt after removing the new sampledata.
472
4450.6.3 by Curtis Hovey
Reconciled the new English variant rule with the other question tests.
473
    >>> login('daf@canonical.com')
474
    >>> daf = getUtility(IPersonSet).getByName('daf')
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
475
    >>> for language in daf.languages:
476
    ...     print language.code
477
    en_GB
478
    ja
479
    cy
12959.4.21 by Curtis Hovey
Alway pass the subscribed_by argument to addAnswerContact.
480
    >>> target.addAnswerContact(daf, daf)
4450.6.3 by Curtis Hovey
Reconciled the new English variant rule with the other question tests.
481
    True
3691.255.8 by Guilherme Salgado
Implementation of ITicketTarget.getSupportedLanguages(), which returns the languages spoken by that object's support contacts
482
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
483
...en_GB is not included in the target's supported languages, because all
484
English variants are converted to English.
485
486
    >>> from operator import attrgetter
487
    >>> print ', '.join(
488
    ...     language.code
489
    ...     for language in sorted(target.getSupportedLanguages(),
490
    ...                            key=attrgetter('code')))
491
    ca, cy, en, es, ja
492
493
494
Answer contacts for languages
495
=============================
496
497
getAnswerContactsForLanguage() method returns a list of answer contacts who
498
support the specified language in their preferred languages.  Daf is in the
499
list because he speaks an English variant, which is treated as English.
4215.2.12 by Curtis Hovey
Removed Answer Contact want_english approach. Tests are broken
500
4215.2.1 by Curtis Hovey
Merged model changes from curtis/launchpad/81369.
501
    >>> spanish = getUtility(ILanguageSet)['es']
502
    >>> answer_contacts = target.getAnswerContactsForLanguage(spanish)
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
503
    >>> for person in answer_contacts:
504
    ...     print person.name
505
    carlos
4215.2.12 by Curtis Hovey
Removed Answer Contact want_english approach. Tests are broken
506
4215.2.1 by Curtis Hovey
Merged model changes from curtis/launchpad/81369.
507
    >>> answer_contacts = target.getAnswerContactsForLanguage(english)
10650.1.1 by Aaron Bentley
Fix test failure due to ordering issue.
508
    >>> for person in sorted(answer_contacts, key=lambda person: person.name):
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
509
    ...     print person.name
510
    carlos
511
    daf
512
513
514
A question's languages
515
======================
3691.258.15 by Francis J. Lacoste
Add ITicketTarget.ticket_languages.
516
3691.398.20 by Francis J. Lacoste
Rename all methods.
517
The getQuestionLanguages() method returns the set of languages used by all
3691.398.21 by Francis J. Lacoste
Rename all attributes and variables.
518
of the target's questions.
3691.258.15 by Francis J. Lacoste
Add ITicketTarget.ticket_languages.
519
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
520
    >>> print ', '.join(
521
    ...     sorted(language.code
522
    ...            for language in target.getQuestionLanguages()))
523
    en, fr
524
525
526
Creating questions from bugs
527
============================
528
529
The target can create a question from a bug, and link that bug to the new
530
question.  The question's owner is the same as the bug's owner.  The question
531
title and description are taken from the bug.  The comments on the bug are
532
copied to the question.
4755.1.3 by Curtis Hovey
Revised and expanded interface/doctests. UI is next.
533
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
534
    >>> from datetime import datetime
10650.1.2 by Aaron Bentley
Fix lint error.
535
    >>> from lp.bugs.interfaces.bug import CreateBugParams
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
536
    >>> from lp.registry.interfaces.product import IProductSet
11716.1.12 by Curtis Hovey
Sorted imports in doctests.
537
    >>> from pytz import UTC
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
538
539
    >>> now = datetime.now(UTC)
540
    >>> target = getUtility(IProductSet)['jokosher']
4755.1.3 by Curtis Hovey
Revised and expanded interface/doctests. UI is next.
541
    >>> bug_params = CreateBugParams(
542
    ...     title="Print is broken", comment="blah blah blah",
543
    ...     owner=sample_person)
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
544
    >>> target_bug = target.createBug(bug_params)
545
    >>> bug_message = target_bug.newMessage(
4755.1.13 by Curtis Hovey
Updated models. The event still needs defining. The view needes tough-love.
546
    ...     owner=sample_person, subject="Opps, my mistake",
547
    ...     content="This is really a question.")
548
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
549
    >>> target_question = target.createQuestionFromBug(target_bug)
4755.1.13 by Curtis Hovey
Updated models. The event still needs defining. The view needes tough-love.
550
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
551
    >>> print target_question.owner.displayname
552
    Sample Person
553
    >>> print target_question.title
554
    Print is broken
555
    >>> print target_question.description
556
    blah blah blah
4755.1.13 by Curtis Hovey
Updated models. The event still needs defining. The view needes tough-love.
557
    >>> question_message = target_question.messages[-1]
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
558
    >>> print question_message.text_contents
559
    This is really a question.
560
561
    >>> for bug_link in target_question.bug_links:
562
    ...     print bug_link.bug.title
563
    Print is broken
564
    >>> print target_question.messages[-1].text_contents
565
    This is really a question.
566
567
The question's creation date is the same as the bug's creation date.  The
568
question's last response date has a current datetime stamp to indicate the
569
question is active.  The question janitor would otherwise mistake the
570
questions made from old bugs as old questions and would expire them.
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
571
572
    >>> target_question.datecreated == target_bug.datecreated
573
    True
4755.1.49 by Curtis Hovey
Changes per review.
574
    >>> target_question.datelastresponse > now
4755.1.48 by Curtis Hovey
Changes per review. Anonther massive restructuring.
575
    True
576
7675.518.2 by Barry Warsaw
Reformat questiontarget.txt to current doctest style.
577
The question language is always English because all bugs in Launchpad are
578
written in English.
579
580
    >>> print target_question.language.code
581
    en