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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
|
Answer Tracker Pages
====================
Several views are used to handle the various operations on a question.
>>> from zope.component import getMultiAdapter
>>> from lp.services.webapp.servers import LaunchpadTestRequest
>>> from lp.registry.interfaces.distribution import IDistributionSet
>>> from lp.registry.interfaces.product import IProductSet
>>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
>>> question_three = ubuntu.getQuestion(3)
>>> firefox = getUtility(IProductSet).getByName('firefox')
>>> firefox_question = firefox.getQuestion(2)
# The firefox_question doesn't have any subscribers, let's subscribe
# the owner.
>>> login('test@canonical.com')
>>> firefox_question.subscribe(firefox_question.owner)
<QuestionSubscription...>
QuestionSubscriptionView
------------------------
This view is used to subscribe and unsubscribe from a question.
Subscription is done when the user click on the 'Subscribe' button.
>>> view = create_initialized_view(question_three, name='+subscribe')
>>> print view.label
Subscribe to question
>>> print view.page_title
Subscription
>>> form = {'subscribe': 'Subscribe'}
>>> view = create_initialized_view(
... question_three, name='+subscribe', form=form)
>>> question_three.isSubscribed(getUtility(ILaunchBag).user)
True
A notification message is displayed and the view redirect to the
question view page.
>>> for notice in view.request.notifications:
... print notice.message
You have subscribed to this question.
>>> view.request.response.getHeader('Location')
'.../+question/3'
Unsubscription works in a similar manner.
>>> view = create_initialized_view(question_three, name='+subscribe')
>>> print view.label
Unsubscribe from question
>>> form = {'subscribe': 'Unsubscribe'}
>>> view = create_initialized_view(
... question_three, name='+subscribe', form=form)
>>> question_three.isSubscribed(getUtility(ILaunchBag).user)
False
>>> for notice in view.request.notifications:
... print notice.message
You have unsubscribed from this question.
>>> view.request.response.getHeader('Location')
'.../+question/3'
QuestionWorkflowView
--------------------
QuestionWorkflowView is the view used to handle the comments submitted
by users on the question. The actions available on it always depends on
the current state of the question and the identify of the user viewing
the form.
# Setup a harness to easily test the view.
>>> from lp.answers.browser.question import QuestionWorkflowView
>>> from lp.testing.deprecated import LaunchpadFormHarness
>>> workflow_harness = LaunchpadFormHarness(
... firefox_question, QuestionWorkflowView)
# Let's define a helper method that will return the names of the
# available actions.
>>> def getAvailableActionNames(view):
... names = [action.__name__.split('.')[-1]
... for action in view.actions
... if action.available()]
... return sorted(names)
Unlogged-in users cannot post any comments on the question:
>>> login(ANONYMOUS)
>>> workflow_harness.submit('', {})
>>> getAvailableActionNames(workflow_harness.view)
[]
When question is in the OPEN state, the owner can comment, answer his
own question or provide more information.
>>> login('test@canonical.com')
>>> workflow_harness.submit('', {})
>>> getAvailableActionNames(workflow_harness.view)
['comment', 'giveinfo', 'selfanswer']
But when another user sees the question, he can comment, provide an
answer or request more information.
>>> login('no-priv@canonical.com')
>>> workflow_harness.submit('', {})
>>> getAvailableActionNames(workflow_harness.view)
['answer', 'comment', 'requestinfo']
When the other user requests for more information, a confirmation is
displayed, the question status is changed to NEEDSINFO and the user is
redirected back to the question page.
>>> workflow_harness.submit(
... 'requestinfo', {
... 'field.message': 'Can you provide an example of an URL'
... 'displaying the problem?'})
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your information request.
>>> print firefox_question.status.name
NEEDSINFO
>>> workflow_harness.redirectionTarget()
'.../+question/2'
The available actions for that other user are still comment, give an
answer or request more information:
>>> getAvailableActionNames(workflow_harness.view)
['answer', 'comment', 'requestinfo']
And the question owner still has the same possibilities as at first:
>>> login('test@canonical.com')
>>> workflow_harness.submit('', {})
>>> getAvailableActionNames(workflow_harness.view)
['comment', 'giveinfo', 'selfanswer']
If he replies with the requested information, the question is moved back
to the OPEN state.
>>> form = {
... 'field.message': "The following SVG doesn't display properly:"
... "\nhttp://www.w3.org/2001/08/rdfweb/rdfweb-chaals-and-dan.svg"
... }
>>> workflow_harness.submit('giveinfo', form)
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for adding more information to your question.
>>> print firefox_question.status.name
OPEN
>>> workflow_harness.redirectionTarget()
'.../+question/2'
The other user can come back and gives an answer:
>>> login('no-priv@canonical.com')
>>> workflow_harness.submit(
... 'answer', {
... 'field.message': "New version of the firefox package are "
... "available with SVG support enabled. Using apt-get or "
... "adept you should be able to upgrade."})
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your answer.
>>> print firefox_question.status.name
ANSWERED
>>> workflow_harness.redirectionTarget()
'.../+question/2'
Once the question is answered, the set of possible actions for the
question owner changes. He can now either comment, confirm the answer,
answer the problem himself, or reopen the request because that answer
isn't working.
>>> login('test@canonical.com')
>>> workflow_harness.submit('', {})
>>> getAvailableActionNames(workflow_harness.view)
['comment', 'confirm', 'reopen', 'selfanswer']
Let's say he confirms the previous answer, in this case, the question
will move to the 'SOLVED' state. Note that the UI doesn't enable the
user to enter a confirmation message at that stage.
>>> answer_message_number = firefox_question.messages.count() - 1
>>> workflow_harness.submit(
... 'confirm', {'answer_id': answer_message_number,
... 'field.message': ''})
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your feedback.
>>> print firefox_question.status.name
SOLVED
>>> workflow_harness.redirectionTarget()
'.../+question/2'
Since no confirmation message was given, a default one was used.
>>> print firefox_question.messages[-1].text_contents
Thanks No Privileges Person, that solved my question.
Once in the SOLVED state, when the answerer is a person other than the
question owner, the owner can now only either add a comment or reopen
the question:
>>> getAvailableActionNames(workflow_harness.view)
['comment', 'reopen']
Adding a comment doesn't change the status:
>>> workflow_harness.submit(
... 'comment', {
... 'field.message': "The example now displays "
... "correctly. Thanks."})
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Thanks for your comment.
>>> workflow_harness.redirectionTarget()
'.../+question/2'
>>> print firefox_question.status.name
SOLVED
And the other user can only comment on the question:
>>> login('no-priv@canonical.com')
>>> workflow_harness.submit('', {})
>>> getAvailableActionNames(workflow_harness.view)
['comment']
If the question owner reopens the question, its status is changed back
to 'OPEN'.
>>> login('test@canonical.com')
>>> workflow_harness.submit(
... 'reopen', {
... 'field.message': "Actually, there are still SVG "
... "that do not display correctly. For example, the following "
... "http://people.w3.org/maxf/ChessGML/immortal.svg doesn't "
... "display correctly."})
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Your question was reopened.
>>> print firefox_question.status.name
OPEN
>>> workflow_harness.redirectionTarget()
'.../+question/2'
When the question owner answers his own question, it is moved straight
to the SOLVED state. The question owner is attributed as the answerer,
but no answer message is assigned to the answer.
>>> workflow_harness.submit(
... 'selfanswer', {
... 'field.message': "OK, this example requires some "
... "SVG features that will only be available in Firefox 2.0."})
>>> for notification in workflow_harness.request.response.notifications:
... print notification.message
Your question is solved. If a particular message helped you solve the
problem, use the <em>'This solved my problem'</em> button.
>>> print firefox_question.status.name
SOLVED
>>> print firefox_question.answerer.displayname
Sample Person
>>> firefox_question.answer is None
True
>>> workflow_harness.redirectionTarget()
'.../+question/2'
When the answerer is the question owner, the owner can still confirm an
answer, in addition to adding a comment or reopening the question. This
path permits the question owner to state how the problem was solved,
then attribute an answerer as a contributor to the solution. The
answerer's message is attributed as the answer in this case.
>>> getAvailableActionNames(workflow_harness.view)
['comment', 'confirm', 'reopen']
>>> workflow_harness.submit(
... 'confirm', {'answer_id': answer_message_number,
... 'field.message': ''})
>>> print firefox_question.status.name
SOLVED
>>> print firefox_question.answerer.displayname
No Privileges Person
>>> print firefox_question.answer.owner.displayname
No Privileges Person
>>> answer_id = firefox_question.messages[answer_message_number].id
>>> firefox_question.answer.id == answer_id
True
>>> workflow_harness.redirectionTarget()
'.../+question/2'
QuestionMakeBugView
-------------------
The QuestionMakeBugView is used to handle the creation of a bug from a
question. In addition to creating a bug, this operation will also link
the bug to the question.
>>> login('foo.bar@canonical.com')
>>> request = LaunchpadTestRequest(
... form={'field.actions.create': 'Create',
... 'field.title': 'Bug title',
... 'field.description': 'Bug description.'})
>>> request.method = 'POST'
>>> makebug = getMultiAdapter((question_three, request), name='+makebug')
>>> question_three.bugs.count() == 0
True
>>> makebug.initialize()
>>> print question_three.bugs[0].title
Bug title
>>> print question_three.bugs[0].description
Bug description.
>>> print makebug.user.name
name16
>>> question_three.bugs[0].isSubscribed(makebug.user)
True
>>> new_bug_id = int(question_three.bugs[0].id)
>>> message = [n.message for n in request.notifications]
>>> message
[u'Thank you! Bug #... created.']
>>> 'Bug #%s created.' % new_bug_id in message[0]
True
If the question already has bugs linked to it, no new bug can be
created.
>>> request = LaunchpadTestRequest(
... form={'field.actions.create': 'create'})
>>> request.method = 'POST'
>>> makebug = getMultiAdapter((question_three, request), name='+makebug')
>>> makebug.initialize()
>>> for n in request.notifications:
... print n.message
You cannot create a bug report...
BugLinkView and BugsUnlinkView
------------------------------
Linking bug (+linkbug) to the question is managed through the
BugLinkView. Unlinking bugs from the question is managed through the
BugsUnlinkView. See 'buglinktarget-pages.txt' for their documentation.
The notifications sent along linking and unlinking bugs can be found in
'answer-tracker-notifications.txt'.
QuestionRejectView
------------------
That view is used by administrator and answer contacts to reject a
question.
>>> login('foo.bar@canonical.com')
>>> request = LaunchpadTestRequest(
... form={'field.actions.reject': 'Reject',
... 'field.message': 'Rejecting for the fun of it.'})
>>> request.method = 'POST'
>>> view = getMultiAdapter((firefox_question, request), name='+reject')
>>> view.initialize()
>>> for notice in request.notifications:
... print notice.message
You have rejected this question.
>>> print firefox_question.status.title
Invalid
QuestionChangeStatusView
------------------------
QuestionChangeStatusView is used by administrator to change the status
outside of the comment workflow.
>>> request = LaunchpadTestRequest(
... form={'field.actions.change-status': 'Change Status',
... 'field.status': 'SOLVED',
... 'field.message': 'Previous rejection was an error.'})
>>> request.method = 'POST'
>>> view = getMultiAdapter(
... (firefox_question, request), name='+change-status')
>>> view.initialize()
>>> for notice in request.notifications:
... print notice.message
Question status updated.
>>> print firefox_question.status.title
Solved
QuestionEditView
----------------
QuestionEditView available through '+edit' is used to edit most question
fields. It can be used to edit the question title and description and
also its metadata like language, assignee, distribution, source package,
product and whiteboard.
>>> login('test@canonical.com')
>>> request = LaunchpadTestRequest(form={
... 'field.actions.change': 'Continue',
... 'field.title': 'Better Title',
... 'field.language': 'en',
... 'field.description': 'A better description.',
... 'field.target': 'package',
... 'field.target.distribution': 'ubuntu',
... 'field.target.package': 'mozilla-firefox',
... 'field.assignee': 'name16',
... 'field.whiteboard': 'Some note'})
>>> request.method = 'POST'
>>> view = getMultiAdapter((question_three, request), name='+edit')
>>> view.initialize()
>>> question_three.title
u'Better Title'
>>> question_three.description
u'A better description.'
>>> print question_three.distribution.name
ubuntu
>>> print question_three.sourcepackagename.name
mozilla-firefox
>>> print question_three.product
None
Since a user must have launchpad.Moderator privilege to change the
assignee and launchpad.Admin privilege to change status whiteboard, the
values are unchanged.
>>> question_three.assignee is None
True
>>> question_three.whiteboard is None
True
If the user has the required permission, the assignee and whiteboard
fields will be updated:
>>> login('foo.bar@canonical.com')
>>> request = LaunchpadTestRequest(form={
... 'field.actions.change': 'Continue',
... 'field.language': 'en',
... 'field.title': 'Better Title',
... 'field.description': 'A better description.',
... 'field.target': 'package',
... 'field.target.distribution': 'ubuntu',
... 'field.target.package': 'mozilla-firefox',
... 'field.assignee': 'name16',
... 'field.whiteboard': 'Some note'})
>>> request.method = 'POST'
>>> view = getMultiAdapter((question_three, request), name='+edit')
>>> view.initialize()
>>> print question_three.assignee.displayname
Foo Bar
>>> print question_three.whiteboard
Some note
The question language can be set to any language registered with
Launchpad--it is not restricted to the user's preferred languages.
>>> view = create_initialized_view(question_three, name='+edit')
>>> view.widgets['language'].vocabulary
<lp.services.worlddata.vocabularies.LanguageVocabulary ...>
In a similar manner, the sourcepackagename field can only be updated on
a distribution question:
>>> request = LaunchpadTestRequest(form={
... 'field.actions.change': 'Continue',
... 'field.language': 'en',
... 'field.title': 'Better Title',
... 'field.description': 'A better description.',
... 'field.target': 'product',
... 'field.target.distribution': '',
... 'field.target.package': 'mozilla-firefox',
... 'field.target.product': 'firefox',
... 'field.assignee': '',
... 'field.whiteboard': ''})
>>> request.method = 'POST'
>>> view = getMultiAdapter((question_three, request), name='+edit')
>>> view.initialize()
>>> view.errors
[]
>>> question_three.sourcepackagename is None
True
>>> print question_three.distribution
None
>>> print question_three.sourcepackagename
None
>>> print question_three.product.name
firefox
# Reassign back the question to ubuntu
>>> question_three.target = ubuntu
The QuestionLanguage vocabulary
-------------------------------
The QuestionLanguageVocabularyFactory is an IContextSourceBinder which
is used in browser forms to create a vocabulary containing only the
languages that are likely to interest the user.
When the user has not configured his preferred languages, the vocabulary
will contain languages from the HTTP request, or the most likely
interesting languages based on GeoIP information.
For example, if the user doesn't log in and his browser is configured to
accept brazilian Portuguese, the vocabulary will contain the languages
spoken in South Africa (because the 127.0.0.1 IP address is mapped to
South Africa in the tests).
>>> login(ANONYMOUS)
>>> request = LaunchpadTestRequest(
... HTTP_ACCEPT_LANGUAGE='pt_BR')
>>> from lp.answers.browser.question import (
... QuestionLanguageVocabularyFactory)
>>> view = getMultiAdapter((firefox, request), name='+addticket')
>>> vocab = QuestionLanguageVocabularyFactory(view)(None)
>>> languages = [term.value for term in vocab]
>>> sorted([lang.code for lang in languages])
[u'af', u'en', u'pt_BR', u'st', u'xh', u'zu']
If the user logs in but didn't configure his preferred languages, the
same logic is used to find the languages:
>>> login('test@canonical.com')
>>> user = getUtility(ILaunchBag).user
>>> len(user.languages)
0
>>> vocab = QuestionLanguageVocabularyFactory(view)(None)
>>> languages = [term.value for term in vocab]
>>> sorted(lang.code for lang in languages)
[u'af', u'en', u'pt_BR', u'st', u'xh', u'zu']
But if the user configured his preferred languages, only these are used:
>>> login('carlos@canonical.com')
>>> user = getUtility(ILaunchBag).user
>>> sorted(lang.code for lang in user.languages)
[u'ca', u'en', u'es']
>>> vocab = QuestionLanguageVocabularyFactory(view)(None)
>>> languages = [term.value for term in vocab]
>>> sorted(lang.code for lang in languages)
[u'ca', u'en', u'es']
Note that all variants of English are always excluded from the
vocabulary (since we don't want to confuse people by providing multiple
English options).
Daf has en_GB listed among his languages:
>>> login('daf@canonical.com')
>>> user = getUtility(ILaunchBag).user
>>> sorted(lang.code for lang in user.languages)
[u'cy', u'en_GB', u'ja']
But the vocabulary made from this languages has substituted the English
variant with English:
>>> vocab = QuestionLanguageVocabularyFactory(view)(None)
>>> languages = [term.value for term in vocab]
>>> sorted(lang.code for lang in languages)
[u'cy', u'en', u'ja']
Note also that the vocabulary will always contain the current question's
language in the vocabulary, even if this language would not be selected
by the previous rules.
>>> from lp.services.worlddata.interfaces.language import ILanguageSet
>>> afar = getUtility(ILanguageSet)['aa_DJ']
>>> question_three.language = afar
>>> vocab = QuestionLanguageVocabularyFactory(view)(question_three)
>>> afar in vocab
True
# Clean up.
>>> question_three.language = getUtility(ILanguageSet)['en']
UserSupportLanguagesMixin
-------------------------
The UserSupportLanguagesMixin can be used by views that needs to
retrieve the set of languages in which the user is assumed to be
interested.
>>> from lp.answers.browser.questiontarget import (
... UserSupportLanguagesMixin)
>>> from lp.services.webapp import LaunchpadView
>>> class UserSupportLanguagesView(UserSupportLanguagesMixin,
... LaunchpadView):
... """View to test UserSupportLanguagesMixin."""
The set of languages to use for support is defined in the
'user_support_languages' attribute.
Like all operations involving languages in the Answer Tracker, we ignore
all other English variants.
When the user is not logged in, or didn't define his preferred
languages, the set will be initialized from the request. That's the
languages configured in the browser, plus other inferred from the GeoIP
database.
>>> request = LaunchpadTestRequest(
... HTTP_ACCEPT_LANGUAGE='fr, en_CA')
>>> login(ANONYMOUS)
>>> view = UserSupportLanguagesView(None, request)
For this request, the set of support languages contains French (from the
request), and the languages spoken in South Africa (inferred from the
GeoIP location of the request).
>>> sorted(language.code for language in view.user_support_languages)
[u'af', u'en', u'fr', u'st', u'xh', u'zu']
Same thing if the logged in user didn't have any preferred languages
set:
>>> login('test@canonical.com')
>>> view = UserSupportLanguagesView(None, request)
>>> sorted(language.code for language in view.user_support_languages)
[u'af', u'en', u'fr', u'st', u'xh', u'zu']
But when the user has some preferred languages set, these will be used
instead of the ones inferred from the request:
>>> login('carlos@canonical.com')
>>> view = UserSupportLanguagesView(None, request)
>>> sorted(language.code for language in view.user_support_languages)
[u'ca', u'en', u'es']
English variants included in the user's preferred languages are
excluded:
>>> login('daf@canonical.com')
>>> view = UserSupportLanguagesView(None, request)
>>> sorted(language.code for language in view.user_support_languages)
[u'cy', u'en', u'ja']
SearchQuestionsView
-------------------
This view is used as a base class to search for questions. It is
intended to be easily customizable to offer more specific reports, while
keeping those searchable.
# Define a subclass to demonstrate the customizability of the base
# view.
>>> from lp.answers.browser.questiontarget import SearchQuestionsView
>>> class MyCustomSearchQuestionsView(SearchQuestionsView):
...
... default_filter = {}
...
... def getDefaultFilter(self):
... return dict(**self.default_filter)
>>> search_view_harness = LaunchpadFormHarness(
... ubuntu, MyCustomSearchQuestionsView)
By default, that class provides widgets to search by text and by status.
>>> search_view = search_view_harness.view
>>> search_view.widgets.get('search_text') is not None
True
>>> search_view.widgets.get('language') is not None
True
>>> search_view.widgets.get('status') is not None
True
It also includes a widget to select the sort order.
>>> search_view.widgets.get('sort') is not None
True
The questions matching the search are available by using the
searchResults() method. The returned results are batched.
>>> questions = search_view.searchResults()
>>> questions
<lp.services.webapp.batching.BatchNavigator ...>
>>> for question in questions.batch:
... print question.title.encode('us-ascii', 'backslashreplace')
Problema al recompilar kernel con soporte smp (doble-n\xfacleo)
Continue playing after shutdown
Play DVDs in Totem
mailto: problem in webpage
Installation of Java Runtime Environment for Mozilla
These were the default results when no search is entered. The user can
tweak the search and filter the results:
>>> search_view_harness.submit('search', {
... 'field.status': ['SOLVED', 'OPEN'],
... 'field.search_text': 'firefox',
... 'field.language': ['en'],
... 'field.sort': 'by relevancy'})
>>> search_view = search_view_harness.view
>>> questions = search_view.searchResults()
>>> for question in questions.batch:
... print question.title, question.status.title
mailto: problem in webpage Solved
Specific views can provide a default filter by returning the default
search parameters to use in the getDefaultFilter() method:
>>> from lp.answers.enums import QuestionStatus
>>> MyCustomSearchQuestionsView.default_filter = {
... 'status': [QuestionStatus.SOLVED, QuestionStatus.INVALID],
... 'language' : search_view.user_support_languages}
>>> search_view_harness.submit('', {})
In this example, only the solved and invalid questions are listed by
default.
>>> search_view = search_view_harness.view
>>> questions = search_view.searchResults()
>>> for question in questions.batch:
... print question.title
mailto: problem in webpage
Better Title
The status widget displays the default criteria used:
>>> for status in search_view.widgets['status']._getFormValue():
... print status.title
Solved
Invalid
The user selected search parameters will override these default
criteria.
>>> search_view_harness.submit('search', {
... 'field.status': ['SOLVED'],
... 'field.search_text': 'firefox',
... 'field.language': ['en'],
... 'field.sort': 'by relevancy'})
>>> search_view = search_view_harness.view
>>> questions = search_view.searchResults()
>>> for question in questions.batch:
... print question.title
mailto: problem in webpage
>>> for status in search_view.widgets['status']._getFormValue():
... print status.title
Solved
The base view computes the page heading and the message displayed when
no results are found based on the selected search filter:
>>> from zope.i18n import translate
>>> search_view_harness.submit('', {})
>>> print translate(search_view_harness.view.page_title)
Questions for Ubuntu
>>> print translate(search_view_harness.view.empty_listing_message)
There are no questions for Ubuntu with the requested statuses.
>>> MyCustomSearchQuestionsView.default_filter = dict(
... status=[QuestionStatus.OPEN], search_text='Firefox')
>>> search_view_harness.submit('', {})
>>> print translate(search_view_harness.view.page_title)
Open questions matching "Firefox" for Ubuntu
>>> print translate(search_view_harness.view.empty_listing_message)
There are no open questions matching "Firefox" for Ubuntu.
It works also with user submitted values:
>>> search_view_harness.submit('search', {
... 'field.status': ['EXPIRED'],
... 'field.search_text': '',
... 'field.language': ['en'],
... 'field.sort': 'by relevancy'})
>>> print translate(search_view_harness.view.page_title)
Expired questions for Ubuntu
>>> print translate(search_view_harness.view.empty_listing_message)
There are no expired questions for Ubuntu.
>>> search_view_harness.submit('search', {
... 'field.status': ['OPEN', 'ANSWERED'],
... 'field.search_text': 'evolution',
... 'field.language': ['en'],
... 'field.sort': 'by relevancy'})
>>> print translate(search_view_harness.view.page_title)
Questions matching "evolution" for Ubuntu
>>> print translate(search_view_harness.view.empty_listing_message)
There are no questions matching "evolution" for Ubuntu with the
requested statuses.
Question listing table
......................
The SearchQuestionsView has two attributes that control the columns of
the question listing table. Products display the default columns of
Summary, Created, Submitter, Assignee, and Status.
>>> from lp.answers.publisher import AnswersLayer
>>> from lp.testing.pages import (
... extract_text, find_tag_by_id)
>>> view = create_initialized_view(
... firefox, name="+questions", layer=AnswersLayer,
... principal=question_three.owner)
>>> view.display_sourcepackage_column
False
>>> view.display_target_column
False
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
Summary Created Submitter Assignee Status
6 Newly installed... 2005-10-14 Sample Person — Answered ...
Distribution display the "Source Package" column. The name of the source
package is displayed if it exists.
>>> view = create_initialized_view(
... ubuntu, name="+questions", layer=AnswersLayer,
... principal=question_three.owner)
>>> view.display_sourcepackage_column
True
>>> view.display_target_column
False
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
Summary Created Submitter Source Package Assignee Status ...
8 ... 2006-07-20 Sample Person mozilla-firefox — Answered
7 ... 2005-10-14 Foo Bar — — Needs ...
ProjectGroups display the "In" column to show the product name.
>>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
>>> mozilla = getUtility(IProjectGroupSet).getByName('mozilla')
>>> view = create_initialized_view(
... mozilla, name="+questions", layer=AnswersLayer,
... principal=question_three.owner)
>>> view.display_sourcepackage_column
False
>>> view.display_target_column
True
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
Summary Created Submitter In Assignee Status
6 ... 2005-10-14 Sample Person Mozilla Firefox — Answered...
The Assignee column is always displayed. It contains The person assigned
to the question, or an m-dash if there is no assignee.
>>> question_six = firefox.getQuestion(6)
>>> question_six.assignee = factory.makePerson(
... name="bob", displayname="Bob")
>>> view = create_initialized_view(
... firefox, name="+questions", layer=AnswersLayer,
... principal=question_three.owner)
>>> view.display_sourcepackage_column
False
>>> view.display_target_column
False
>>> table = find_tag_by_id(view.render(), 'question-listing')
>>> for row in table.findAll('tr'):
... print extract_text(row)
Summary Created Submitter Assignee Status
6 ... 2005-10-14 Sample Person Bob Answered
4 ... 2005-09-05 Foo Bar — Open ...
QuestionCollectionOpenCountView
-------------------------------
There is a helper view that is available on all IQuestionCollection that
returns the number of questions in the Open and Needs information states
on that target.
>>> view = getMultiAdapter(
... (firefox, LaunchpadTestRequest()), name='+open_questions_count')
>>> view()
u'3'
ManageAnswerContactView
-----------------------
That view is used by a user to register himself or any team he
administrates as an answer contact for the project.
Jeff Waugh is an administrator for the Ubuntu Team. Thus he can register
himself or the Ubuntu Team as answer contact for ubuntu:
>>> list(ubuntu.answer_contacts)
[]
>>> login('jeff.waugh@ubuntulinux.com')
>>> jeff_waugh = getUtility(ILaunchBag).user
>>> from lp.registry.interfaces.person import IPersonSet
>>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
>>> jeff_waugh in ubuntu_team.getDirectAdministrators()
True
>>> request = LaunchpadTestRequest(
... method='POST', form={
... 'field.actions.update': 'Continue',
... 'field.want_to_be_answer_contact': 'on',
... 'field.answer_contact_teams': 'ubuntu-team'})
>>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
>>> view.initialize()
>>> sorted(
... [person.displayname for person in ubuntu.direct_answer_contacts])
[u'Jeff Waugh', u'Ubuntu Team']
The view adds notifications about the answer contacts added:
>>> for notification in request.notifications:
... print notification.message
<...Your preferred languages... were updated to include ...English (en).
You have been added as an answer contact for Ubuntu.
English was added to Ubuntu Team's ...preferred languages...
Ubuntu Team has been added as an answer contact for Ubuntu.
But Daniel Silverstone is only a regular member of Ubuntu Team, so he
can only subscribe himself:
>>> login('daniel.silverstone@canonical.com')
>>> kinnison = getUtility(ILaunchBag).user
>>> kinnison in ubuntu_team.getDirectAdministrators()
False
>>> request = LaunchpadTestRequest(
... method='POST', form={
... 'field.actions.update': 'Continue',
... 'field.want_to_be_answer_contact': 'on'})
>>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
>>> view.initialize()
>>> sorted(
... [person.displayname for person in ubuntu.direct_answer_contacts])
[u'Daniel Silverstone', u'Jeff Waugh', u'Ubuntu Team']
>>> for notification in request.notifications:
... print notification.message
<...Your preferred languages... were updated to include ...English (en).
You have been added as an answer contact for Ubuntu.
The same view is used to remove answer contact registrations. The user
can only remove his own registration.
>>> request = LaunchpadTestRequest(
... method='POST', form={
... 'field.actions.update': 'Continue',
... 'field.want_to_be_answer_contact': 'off'})
>>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
>>> view.initialize()
>>> sorted(
... [person.displayname for person in ubuntu.direct_answer_contacts])
[u'Jeff Waugh', u'Ubuntu Team']
>>> for notification in request.notifications:
... print notification.message
You have been removed as an answer contact for Ubuntu.
It can also be used to remove a team registration when the user is a
team administrator:
>>> login('jeff.waugh@ubuntulinux.com')
>>> request = LaunchpadTestRequest(
... method='POST', form={
... 'field.actions.update': 'Continue',
... 'field.want_to_be_answer_contact': 'on',
... 'field.answer_contact_teams-empty_marker': '1'})
>>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
>>> view.initialize()
>>> sorted(
... [person.displayname for person in ubuntu.direct_answer_contacts])
[u'Jeff Waugh']
>>> for notification in request.notifications:
... print notification.message
Ubuntu Team has been removed as an answer contact for Ubuntu.
|