~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
========================
Launchpad Answer Tracker
========================

Launchpad includes an Answer Tracker where users can post questions, usually
about problems they encounter with projects, and other people can answer them.
Questions are created and accessed using the IQuestionTarget interface.  This
interface is available on Products, Distributions and
DistributionSourcePackages.

    >>> login('test@canonical.com')

    >>> from canonical.launchpad.webapp.testing import verifyObject
    >>> from lp.answers.interfaces.questiontarget import IQuestionTarget
    >>> from lp.registry.interfaces.distribution import IDistributionSet
    >>> from lp.registry.interfaces.person import IPersonSet
    >>> from lp.registry.interfaces.product import IProductSet

    >>> firefox = getUtility(IProductSet)['firefox']
    >>> verifyObject(IQuestionTarget, firefox)
    True
    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
    >>> verifyObject(IQuestionTarget, ubuntu)
    True

    >>> evolution_in_ubuntu = ubuntu.getSourcePackage('evolution')
    >>> verifyObject(IQuestionTarget, evolution_in_ubuntu)
    True

Although distribution series do not implement the IQuestionTarget interface,
it is possible to adapt one to it.  The adapter is actually the distroseries's
distribution.

    >>> ubuntu_warty = ubuntu.getSeries('warty')
    >>> IQuestionTarget.providedBy(ubuntu_warty)
    False
    >>> questiontarget = IQuestionTarget(ubuntu_warty)
    >>> verifyObject(IQuestionTarget, questiontarget)
    True

Similarly, it is possible to adapt an ISourcePackageRelease into an
IQuestionTarget.

    >>> firefox_in_warty = ubuntu_warty.getSourcePackage('mozilla-firefox')
    >>> firefox_release_in_warty = firefox_in_warty.currentrelease

    >>> questiontarget = IQuestionTarget(firefox_release_in_warty)
    >>> verifyObject(IQuestionTarget, questiontarget)
    True

SourcePackages are can be adapted to QuestionTargets.

    >>> evolution_in_hoary = ubuntu.currentseries.getSourcePackage(
    ...     'evolution')
    >>> questiontarget = IQuestionTarget(evolution_in_hoary)
    >>> verifyObject(IQuestionTarget, questiontarget)
    True

You create a new question by calling the newQuestion() method of an
IQuestionTarget attribute.

    >>> sample_person = getUtility(IPersonSet).getByEmail(
    ...     'test@canonical.com')
    >>> firefox_question = firefox.newQuestion(
    ...     sample_person, "Firefox question", "Unable to use Firefox")

The complete IQuestionTarget interface is documented in questiontarget.txt.


Official usage
==============

A product or distribution may be officially supported by the community using
the Answer Tracker.  This status is set by the answers_usage attribute on
the IProduct and IDistribution.

    >>> print ubuntu.answers_usage.name
    LAUNCHPAD
    >>> print firefox.answers_usage.name
    LAUNCHPAD


IQuestion interface
===================

Questions are manipulated through the IQuestion interface.

    >>> from zope.security.proxy import removeSecurityProxy
    >>> from lp.answers.interfaces.question import IQuestion

    # The complete interface is not necessarily available to the
    # logged in user.
    >>> verifyObject(IQuestion, removeSecurityProxy(firefox_question))
    True

The person who submitted the question is available in the owner field.

    >>> firefox_question.owner
    <Person at ... name12 (Sample Person)>

When the question is created, the owner is added to the question's
subscribers.

    >>> from operator import attrgetter
    >>> def print_subscribers(question):
    ...     people = [subscription.person
    ...               for subscription in question.subscriptions]
    ...     for person in sorted(people, key=attrgetter('name')):
    ...         print person.displayname
    >>> print_subscribers(firefox_question)
    Sample Person

The question status is 'Open'.

    >>> print firefox_question.status.title
    Open

The question has a creation time.

    >>> from datetime import datetime, timedelta
    >>> from pytz import UTC
    >>> now = datetime.now(UTC)
    >>> now - firefox_question.datecreated < timedelta(seconds=5)
    True

The target onto which the question was created is also available.

    >>> print firefox_question.target.displayname
    Mozilla Firefox

It is also possible to adapt a question to its IQuestionTarget.

    >>> target = IQuestionTarget(firefox_question)
    >>> verifyObject(IQuestionTarget, target)
    True

The question can be assigned to a new IQuestionTarget.

    >>> thunderbird = getUtility(IProductSet)['thunderbird']
    >>> firefox_question.target = thunderbird
    >>> print firefox_question.target.displayname
    Mozilla Thunderbird

When a question is reassigned, its product, distribution and
sourcepackagename attributes are reconciled with the IQuestionTarget.

    >>> firefox_question.target = ubuntu
    >>> print firefox_question.target.displayname
    Ubuntu
    >>> print firefox_question.distribution.name
    ubuntu
    >>> print firefox_question.sourcepackagename
    None
    >>> print firefox_question.product
    None

    >>> firefox_question.target = evolution_in_ubuntu
    >>> print firefox_question.target.displayname
    evolution in Ubuntu
    >>> print firefox_question.distribution.name
    ubuntu
    >>> print firefox_question.sourcepackagename.name
    evolution
    >>> print firefox_question.product
    None

    >>> firefox_question.target = firefox
    >>> print firefox_question.target.displayname
    Mozilla Firefox
    >>> print firefox_question.distribution
    None
    >>> print firefox_question.sourcepackagename
    None
    >>> print firefox_question.product.name
    firefox


Subscriptions and notifications
===============================

Whenever a question is created or changed, email notifications will be
sent.  To receive such notification, one can subscribe to the bug using
the subscribe() method.

    >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
    >>> subscription = firefox_question.subscribe(no_priv)

The subscribers include the owner and the newly subscribed person.

    >>> print_subscribers(firefox_question)
    Sample Person
    No Privileges Person

The getDirectSubscribers() method returns a sorted list of subscribers.
This method iterates like the NotificationRecipientSet returned by the
direct_recipients method.

    >>> for person in firefox_question.getDirectSubscribers():
    ...     print person.displayname
    No Privileges Person
    Sample Person

To remove a person from the subscriptions list, we use the unsubscribe()
method.

    >>> firefox_question.unsubscribe(no_priv, no_priv)
    >>> print_subscribers(firefox_question)
    Sample Person

The people on the subscription list are said to be directly subscribed to the
question.  They explicitly chose to get notifications about that particular
question.  This list of people is available through the direct_recipients
method.

    >>> subscribers = firefox_question.direct_recipients

That method returns an INotificationRecipientSet, containing the direct
subscribers along with the rationale for contacting them.

    >>> from canonical.launchpad.interfaces.launchpad import (
    ...     INotificationRecipientSet)
    >>> verifyObject(INotificationRecipientSet, subscribers)
    True
    >>> def print_reason(subscribers):
    ...     for person in subscribers:
    ...         text, header = subscribers.getReason(person)
    ...         print header, person.displayname, text
    >>> print_reason(subscribers)
    Asker Sample Person
    You received this question notification because you asked the question.

There is also a list of 'indirect' subscribers to the question.  These are
people that didn't explicitly subscribe to the question, but that will receive
notifications for other reasons.  Answer contacts for the question target are
part of the indirect subscribers list.

    # There are no answer contacts on the firefox product.
    >>> list(firefox_question.indirect_recipients)
    []

    >>> from lp.services.worlddata.interfaces.language import ILanguageSet
    >>> english = getUtility(ILanguageSet)['en']
    >>> no_priv.addLanguage(english)
    >>> firefox.addAnswerContact(no_priv, no_priv)
    True

    >>> from lp.services.propertycache import get_property_cache
    >>> del get_property_cache(firefox_question).indirect_recipients
    >>> indirect_subscribers = firefox_question.indirect_recipients
    >>> verifyObject(INotificationRecipientSet, indirect_subscribers)
    True
    >>> print_reason(indirect_subscribers)
    Answer Contact (Mozilla Firefox) No Privileges Person
    You received this question notification because you are an answer
    contact for Mozilla Firefox.

There is a special case for when the question is associated with a source
package.  The answer contacts for both the distribution and the source package
are part of the indirect subscribers list.

    # Let's register some answer contacts for the distribution and
    # the package.
    >>> list(ubuntu.answer_contacts)
    []
    >>> list(evolution_in_ubuntu.answer_contacts)
    []
    >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
    >>> ubuntu_team.addLanguage(english)
    >>> ubuntu.addAnswerContact(ubuntu_team, ubuntu_team.teamowner)
    True
    >>> evolution_in_ubuntu.addAnswerContact(no_priv, no_priv)
    True
    >>> package_question = evolution_in_ubuntu.newQuestion(
    ...     sample_person, 'Upgrading to Evolution 1.4 breaks plug-ins',
    ...     "The FnordsHighlighter plug-in doesn't work after upgrade.")

    >>> print_subscribers(package_question)
    Sample Person

    >>> del get_property_cache(firefox_question).indirect_recipients
    >>> indirect_subscribers = package_question.indirect_recipients
    >>> for person in indirect_subscribers:
    ...     print person.displayname
    No Privileges Person
    Ubuntu Team

    >>> text, header = indirect_subscribers.getReason(ubuntu_team)
    >>> print header, text
    Answer Contact (ubuntu) @ubuntu-team
    You received this question notification because you are a member of
    Ubuntu Team, which is an answer contact for Ubuntu.

The question's assignee is also part of the indirect subscription list:

    >>> login('admin@canonical.com')
    >>> package_question.assignee = getUtility(IPersonSet).getByName('name16')
    >>> del get_property_cache(package_question).indirect_recipients
    >>> indirect_subscribers = package_question.indirect_recipients
    >>> for person in indirect_subscribers:
    ...     print person.displayname
    Foo Bar
    No Privileges Person
    Ubuntu Team

    >>> text, header = indirect_subscribers.getReason(
    ...     package_question.assignee)
    >>> print header, text
    Assignee
    You received this question notification because you are the assignee for
    this question.

The getIndirectSubscribers() method iterates like the indirect_recipients
method, but it returns a sorted list instead of a NotificationRecipientSet.
It too contains the question assignee.

    >>> indirect_subscribers = package_question.getIndirectSubscribers()
    >>> for person in indirect_subscribers:
    ...     print person.displayname
    Foo Bar
    No Privileges Person
    Ubuntu Team

Notifications are sent to the list of direct and indirect subscribers.  The
notification recipients list can be obtained by using the getRecipients()
method.

    >>> login('no-priv@canonical.com')
    >>> subscribers = firefox_question.getRecipients()
    >>> verifyObject(INotificationRecipientSet, subscribers)
    True
    >>> for person in subscribers:
    ...     print person.displayname
    No Privileges Person
    Sample Person

More documentation on the question notifications can be found in
`answer-tracker-notifications.txt`.


Workflow
========

A question status should not be manipulated directly but through the
workflow methods.

The complete question workflow is documented in
`answer-tracker-workflow.txt`.


Bug linking
===========

Question implements the IBugLinkTarget interface which makes it possible
to link bug report to question.

    >>> from lp.bugs.interfaces.buglink import IBugLinkTarget
    >>> verifyObject(IBugLinkTarget, firefox_question)
    True

See ../../bugs/tests/buglinktarget.txt for the documentation and test of the
IBugLinkTarget interface.

When a bug is linked to a question, the question's owner is subscribed to the
bug.

    >>> from lp.bugs.interfaces.bug import IBugSet
    >>> bug7 = getUtility(IBugSet).get(7)
    >>> bug7.isSubscribed(firefox_question.owner)
    False
    >>> firefox_question.linkBug(bug7)
    <QuestionBug...>
    >>> bug7.isSubscribed(firefox_question.owner)
    True

When the link is removed, the owner is unsubscribed.

    >>> firefox_question.unlinkBug(bug7)
    <QuestionBug...>
    >>> bug7.isSubscribed(firefox_question.owner)
    False


Unsupported questions
=====================

While a Person may ask questions in his language of choice, that does not mean
that indirect subscribers (Answer Contacts) to an IQuestionTarget speak that
language.  IQuestionTarget can return a list Questions in languages that are
not supported.

    >>> unsupported_questions = firefox.searchQuestions(unsupported=True)
    >>> sorted(question.title for question in unsupported_questions)
    [u'Problemas de Impress\xe3o no Firefox']

    >>> unsupported_questions = evolution_in_ubuntu.searchQuestions(
    ...     unsupported=True)
    >>> sorted(question.title for question in unsupported_questions)
    []

    >>> warty_question_target = IQuestionTarget(ubuntu_warty)
    >>> unsupported_questions = warty_question_target.searchQuestions(
    ...     unsupported=True)
    >>> sorted(question.title for question in unsupported_questions)
    [u'Problema al recompilar kernel con soporte smp (doble-n\xfacleo)',
     u'\u0639\u0643\u0633 \u0627\u0644\u062a\u063a\u064a\u064a\u0631...]