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
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
|
# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""IBugTarget-related browser views."""
__metaclass__ = type
__all__ = [
"BugsVHostBreadcrumb",
"BugsPatchesView",
"BugTargetBugListingView",
"BugTargetBugTagsView",
"BugTargetBugsView",
"FileBugAdvancedView",
"FileBugGuidedView",
"FileBugViewBase",
"IProductBugConfiguration",
"OfficialBugTagsManageView",
"ProductConfigureBugTrackerView",
"ProjectFileBugGuidedView",
"product_to_productbugconfiguration",
]
import cgi
from cStringIO import StringIO
from datetime import datetime
from operator import itemgetter
import urllib
from urlparse import urljoin
from lazr.restful.interface import copy_field
from pytz import timezone
from simplejson import dumps
from sqlobject import SQLObjectNotFound
from z3c.ptcompat import ViewPageTemplateFile
from zope import formlib
from zope.app.form.browser import TextWidget
from zope.app.form.interfaces import InputErrors
from zope.component import getUtility
from zope.interface import (
alsoProvides,
implements,
Interface,
)
from zope.publisher.interfaces import NotFound
from zope.publisher.interfaces.browser import IBrowserPublisher
from zope.schema import (
Bool,
Choice,
)
from zope.schema.interfaces import TooLong
from zope.schema.vocabulary import SimpleVocabulary
from zope.security.proxy import removeSecurityProxy
from canonical.config import config
from canonical.launchpad import _
from canonical.launchpad.browser.feeds import (
BugFeedLink,
BugTargetLatestBugsFeedLink,
FeedsMixin,
)
from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
from canonical.launchpad.searchbuilder import any
from canonical.launchpad.webapp import (
canonical_url,
LaunchpadView,
urlappend,
)
from canonical.launchpad.webapp.authorization import check_permission
from canonical.launchpad.webapp.batching import BatchNavigator
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
from canonical.launchpad.webapp.interfaces import ILaunchBag
from canonical.launchpad.webapp.menu import structured
from canonical.launchpad.webapp.publisher import HTTP_MOVED_PERMANENTLY
from lp.app.browser.launchpadform import (
action,
custom_widget,
LaunchpadEditFormView,
LaunchpadFormView,
safe_action,
)
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.browser.tales import BugTrackerFormatterAPI
from lp.app.enums import ServiceUsage
from lp.app.errors import (
NotFoundError,
UnexpectedFormData,
)
from lp.app.interfaces.launchpad import (
ILaunchpadCelebrities,
ILaunchpadUsage,
IServiceUsage,
)
from lp.app.validators.name import valid_name_pattern
from lp.app.widgets.product import (
GhostCheckBoxWidget,
GhostWidget,
ProductBugTrackerWidget,
)
from lp.bugs.browser.bugrole import BugRoleMixin
from lp.bugs.browser.bugtask import BugTaskSearchListingView
from lp.bugs.browser.structuralsubscription import (
expose_structural_subscription_data_to_js,
)
from lp.bugs.browser.widgets.bug import (
BugTagsWidget,
LargeBugTagsWidget,
)
from lp.bugs.browser.widgets.bugtask import NewLineToSpacesWidget
from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource
from lp.bugs.interfaces.bug import (
CreateBugParams,
IBug,
IBugAddForm,
IBugSet,
IProjectGroupBugAddForm,
)
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
from lp.bugs.interfaces.bugtarget import (
IBugTarget,
IOfficialBugTagTargetPublic,
IOfficialBugTagTargetRestricted,
)
from lp.bugs.interfaces.bugtask import (
BugTaskSearchParams,
BugTaskStatus,
IBugTaskSet,
UNRESOLVED_BUGTASK_STATUSES,
)
from lp.bugs.interfaces.bugtracker import IBugTracker
from lp.bugs.interfaces.malone import IMaloneApplication
from lp.bugs.interfaces.securitycontact import IHasSecurityContact
from lp.bugs.model.bugtask import BugTask
from lp.bugs.model.structuralsubscription import (
get_structural_subscriptions_for_target,
)
from lp.bugs.publisher import BugsLayer
from lp.bugs.utilities.filebugdataparser import FileBugData
from lp.hardwaredb.interfaces.hwdb import IHWSubmissionSet
from lp.registry.browser.product import ProductConfigureBase
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
)
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.productseries import IProductSeries
from lp.registry.interfaces.projectgroup import IProjectGroup
from lp.registry.interfaces.sourcepackage import ISourcePackage
from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
from lp.services.job.interfaces.job import JobStatus
from lp.services.propertycache import cachedproperty
# A simple vocabulary for the subscribe_to_existing_bug form field.
SUBSCRIBE_TO_BUG_VOCABULARY = SimpleVocabulary.fromItems(
[('yes', True), ('no', False)])
class IProductBugConfiguration(Interface):
"""A composite schema for editing bug app configuration."""
bug_supervisor = copy_field(
IHasBugSupervisor['bug_supervisor'], readonly=False)
security_contact = copy_field(IHasSecurityContact['security_contact'])
official_malone = copy_field(ILaunchpadUsage['official_malone'])
enable_bug_expiration = copy_field(
ILaunchpadUsage['enable_bug_expiration'])
bugtracker = copy_field(IProduct['bugtracker'])
remote_product = copy_field(IProduct['remote_product'])
bug_reporting_guidelines = copy_field(
IBugTarget['bug_reporting_guidelines'])
bug_reported_acknowledgement = copy_field(
IBugTarget['bug_reported_acknowledgement'])
enable_bugfiling_duplicate_search = copy_field(
IBugTarget['enable_bugfiling_duplicate_search'])
def product_to_productbugconfiguration(product):
"""Adapts an `IProduct` into an `IProductBugConfiguration`."""
alsoProvides(
removeSecurityProxy(product), IProductBugConfiguration)
return product
class ProductConfigureBugTrackerView(BugRoleMixin, ProductConfigureBase):
"""View class to configure the bug tracker for a project."""
label = "Configure bug tracker"
schema = IProductBugConfiguration
# This ProductBugTrackerWidget renders enable_bug_expiration and
# remote_product as subordinate fields, so this view suppresses them.
custom_widget('bugtracker', ProductBugTrackerWidget)
custom_widget('enable_bug_expiration', GhostCheckBoxWidget)
custom_widget('remote_product', GhostWidget)
@property
def field_names(self):
"""Return the list of field names to display."""
field_names = [
"bugtracker",
"enable_bug_expiration",
"remote_product",
"bug_reporting_guidelines",
"bug_reported_acknowledgement",
"enable_bugfiling_duplicate_search",
]
if check_permission("launchpad.Edit", self.context):
field_names.extend(["bug_supervisor", "security_contact"])
return field_names
def validate(self, data):
"""Constrain bug expiration to Launchpad Bugs tracker."""
if check_permission("launchpad.Edit", self.context):
self.validateBugSupervisor(data)
self.validateSecurityContact(data)
# enable_bug_expiration is disabled by JavaScript when bugtracker
# is not 'In Launchpad'. The constraint is enforced here in case the
# JavaScript fails to activate or run. Note that the bugtracker
# name : values are {'In Launchpad' : object, 'Somewhere else' : None
# 'In a registered bug tracker' : IBugTracker}.
bugtracker = data.get('bugtracker', None)
if bugtracker is None or IBugTracker.providedBy(bugtracker):
data['enable_bug_expiration'] = False
@action("Change", name='change')
def change_action(self, action, data):
# bug_supervisor and security_contactrequires a transition method,
# so it must be handled separately and removed for the
# updateContextFromData to work as expected.
if check_permission("launchpad.Edit", self.context):
self.changeBugSupervisor(data['bug_supervisor'])
del data['bug_supervisor']
self.changeSecurityContact(data['security_contact'])
del data['security_contact']
self.updateContextFromData(data)
class FileBugReportingGuidelines(LaunchpadFormView):
"""Provides access to common bug reporting attributes.
Attributes provided are: security_related and bug_reporting_guidelines.
This view is a superclass of `FileBugViewBase` so that non-ajax browsers
can load the file bug form, and it is also invoked directly via an XHR
request to provide an HTML snippet for Javascript enabled browsers.
"""
schema = IBug
@property
def field_names(self):
"""Return the list of field names to display."""
return ['security_related']
def setUpFields(self):
"""Set up the form fields. See `LaunchpadFormView`."""
super(FileBugReportingGuidelines, self).setUpFields()
security_related_field = Bool(
__name__='security_related',
title=_("This bug is a security vulnerability"),
required=False, default=False)
self.form_fields = self.form_fields.omit('security_related')
self.form_fields += formlib.form.Fields(security_related_field)
@property
def bug_reporting_guidelines(self):
"""Guidelines for filing bugs in the current context.
Returns a list of dicts, with each dict containing values for
"preamble" and "content".
"""
def target_name(target):
# IProjectGroup can be considered the target of a bug during
# the bug filing process, but does not extend IBugTarget
# and ultimately cannot actually be the target of a
# bug. Hence this function to determine a suitable
# name/title to display. Hurrumph.
if IBugTarget.providedBy(target):
return target.bugtargetdisplayname
else:
return target.displayname
guidelines = []
bugtarget = self.context
if bugtarget is not None:
content = bugtarget.bug_reporting_guidelines
if content is not None and len(content) > 0:
guidelines.append({
"source": target_name(bugtarget),
"content": content,
})
# Distribution source packages are shown with both their
# own reporting guidelines and those of their
# distribution.
if IDistributionSourcePackage.providedBy(bugtarget):
distribution = bugtarget.distribution
content = distribution.bug_reporting_guidelines
if content is not None and len(content) > 0:
guidelines.append({
"source": target_name(distribution),
"content": content,
})
return guidelines
def getMainContext(self):
if IDistributionSourcePackage.providedBy(self.context):
return self.context.distribution
else:
return self.context
class FileBugViewBase(FileBugReportingGuidelines, LaunchpadFormView):
"""Base class for views related to filing a bug."""
implements(IBrowserPublisher)
extra_data_token = None
advanced_form = False
frontpage_form = False
data_parser = None
def __init__(self, context, request):
LaunchpadFormView.__init__(self, context, request)
self.extra_data = FileBugData()
def initialize(self):
LaunchpadFormView.initialize(self)
if (not self.redirect_ubuntu_filebug and
self.extra_data_token is not None and
not self.extra_data_to_process):
# self.extra_data has been initialized in publishTraverse().
if self.extra_data.initial_summary:
self.widgets['title'].setRenderedValue(
self.extra_data.initial_summary)
if self.extra_data.initial_tags:
self.widgets['tags'].setRenderedValue(
self.extra_data.initial_tags)
# XXX: Bjorn Tillenius 2006-01-15:
# We should include more details of what will be added
# to the bug report.
self.request.response.addNotification(
'Extra debug information will be added to the bug report'
' automatically.')
@cachedproperty
def redirect_ubuntu_filebug(self):
if IDistribution.providedBy(self.context):
bug_supervisor = self.context.bug_supervisor
elif (IDistributionSourcePackage.providedBy(self.context) or
ISourcePackage.providedBy(self.context)):
bug_supervisor = self.context.distribution.bug_supervisor
else:
bug_supervisor = None
# Work out whether the redirect should be overidden.
do_not_redirect = (
self.request.form.get('no-redirect') is not None or
[key for key in self.request.form.keys()
if 'field.actions' in key] != [] or
self.user.inTeam(bug_supervisor))
return (
config.malone.ubuntu_disable_filebug and
self.targetIsUbuntu() and
self.extra_data_token is None and
not do_not_redirect)
@property
def field_names(self):
"""Return the list of field names to display."""
context = self.context
field_names = ['title', 'comment', 'tags', 'security_related',
'bug_already_reported_as', 'filecontent', 'patch',
'attachment_description', 'subscribe_to_existing_bug']
if (IDistribution.providedBy(context) or
IDistributionSourcePackage.providedBy(context)):
field_names.append('packagename')
elif IMaloneApplication.providedBy(context):
field_names.append('bugtarget')
elif IProjectGroup.providedBy(context):
field_names.append('product')
elif not IProduct.providedBy(context):
raise AssertionError('Unknown context: %r' % context)
# If the context is a project group we want to render the optional
# fields since they will initially be hidden and later exposed if the
# selected project supports them.
include_extra_fields = IProjectGroup.providedBy(context)
if not include_extra_fields and IHasBugSupervisor.providedBy(context):
include_extra_fields = self.user.inTeam(context.bug_supervisor)
if include_extra_fields:
field_names.extend(
['assignee', 'importance', 'milestone', 'status'])
return field_names
@property
def initial_values(self):
"""Give packagename a default value, if applicable."""
if not IDistributionSourcePackage.providedBy(self.context):
return {}
return {'packagename': self.context.name}
def isPrivate(self):
"""Whether bug reports on this target are private by default."""
return IProduct.providedBy(self.context) and self.context.private_bugs
def contextIsProduct(self):
return IProduct.providedBy(self.context)
def contextIsProject(self):
return IProjectGroup.providedBy(self.context)
def targetIsUbuntu(self):
ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
return (self.context == ubuntu or
(IMaloneApplication.providedBy(self.context) and
self.request.form.get('field.bugtarget.distribution') ==
ubuntu.name))
def getPackageNameFieldCSSClass(self):
"""Return the CSS class for the packagename field."""
if self.widget_errors.get("packagename"):
return 'error'
else:
return ''
def validate(self, data):
"""Make sure the package name, if provided, exists in the distro."""
# The comment field is only required if filing a new bug.
if self.submit_bug_action.submitted():
comment = data.get('comment')
# The widget only exposes the error message. The private
# attr contains the real error.
widget_error = self.widgets.get('comment')._error
if widget_error and isinstance(widget_error.errors, TooLong):
self.setFieldError('comment',
'The description is too long. If you have lots '
'text to add, attach a file to the bug instead.')
elif not comment or widget_error is not None:
self.setFieldError(
'comment', "Provide details about the issue.")
# Check a bug has been selected when the user wants to
# subscribe to an existing bug.
elif self.this_is_my_bug_action.submitted():
if not data.get('bug_already_reported_as'):
self.setFieldError('bug_already_reported_as',
"Please choose a bug.")
else:
# We only care about those two actions.
pass
# We have to poke at the packagename value directly in the
# request, because if validation failed while getting the
# widget's data, it won't appear in the data dict.
form = self.request.form
if form.get("packagename_option") == "choose":
packagename = form.get("field.packagename")
if packagename:
if IDistribution.providedBy(self.context):
distribution = self.context
elif 'distribution' in data:
distribution = data['distribution']
else:
assert IDistributionSourcePackage.providedBy(self.context)
distribution = self.context.distribution
try:
distribution.guessPackageNames(packagename)
except NotFoundError:
if distribution.series:
# If a distribution doesn't have any series,
# it won't have any source packages published at
# all, so we set the error only if there are
# series.
packagename_error = (
'"%s" does not exist in %s. Please choose a '
"different package. If you're unsure, please "
'select "I don\'t know"' % (
packagename, distribution.displayname))
self.setFieldError("packagename", packagename_error)
else:
self.setFieldError("packagename",
"Please enter a package name")
# If we've been called from the frontpage filebug forms we must check
# that whatever product or distro is having a bug filed against it
# actually uses Malone for its bug tracking.
product_or_distro = self.getProductOrDistroFromContext()
if (product_or_distro is not None and
product_or_distro.bug_tracking_usage != ServiceUsage.LAUNCHPAD):
self.setFieldError(
'bugtarget',
"%s does not use Launchpad as its bug tracker " %
product_or_distro.displayname)
def setUpWidgets(self):
"""Customize the onKeyPress event of the package name chooser."""
LaunchpadFormView.setUpWidgets(self)
if "packagename" in self.field_names:
self.widgets["packagename"].onKeyPress = (
"selectWidget('choose', event)")
def setUpFields(self):
"""Set up the form fields. See `LaunchpadFormView`."""
super(FileBugViewBase, self).setUpFields()
# Override the vocabulary for the subscribe_to_existing_bug
# field.
subscribe_field = Choice(
__name__='subscribe_to_existing_bug',
title=u'Subscribe to this bug',
vocabulary=SUBSCRIBE_TO_BUG_VOCABULARY,
required=True, default=False)
self.form_fields = self.form_fields.omit('subscribe_to_existing_bug')
self.form_fields += formlib.form.Fields(subscribe_field)
def contextUsesMalone(self):
"""Does the context use Malone as its official bugtracker?"""
if IProjectGroup.providedBy(self.context):
products_using_malone = [
product for product in self.context.products
if product.bug_tracking_usage == ServiceUsage.LAUNCHPAD]
return len(products_using_malone) > 0
else:
bug_tracking_usage = self.getMainContext().bug_tracking_usage
return bug_tracking_usage == ServiceUsage.LAUNCHPAD
def shouldSelectPackageName(self):
"""Should the radio button to select a package be selected?"""
return (
self.request.form.get("field.packagename") or
self.initial_values.get("packagename"))
def handleSubmitBugFailure(self, action, data, errors):
return self.showFileBugForm()
@action("Submit Bug Report", name="submit_bug",
failure=handleSubmitBugFailure)
def submit_bug_action(self, action, data):
"""Add a bug to this IBugTarget."""
title = data["title"]
comment = data["comment"].rstrip()
packagename = data.get("packagename")
security_related = data.get("security_related", False)
distribution = data.get(
"distribution", getUtility(ILaunchBag).distribution)
context = self.context
if distribution is not None:
# We're being called from the generic bug filing form, so
# manually set the chosen distribution as the context.
context = distribution
elif IProjectGroup.providedBy(context):
context = data['product']
elif IMaloneApplication.providedBy(context):
context = data['bugtarget']
# Ensure that no package information is used, if the user
# enters a package name but then selects "I don't know".
if self.request.form.get("packagename_option") == "none":
packagename = None
# Security bugs are always private when filed, but can be disclosed
# after they've been reported.
if security_related:
private = True
else:
private = False
linkified_ack = structured(FormattersAPI(
self.getAcknowledgementMessage(self.context)).text_to_html(
last_paragraph_class="last"))
notifications = [linkified_ack]
params = CreateBugParams(
title=title, comment=comment, owner=self.user,
security_related=security_related, private=private,
tags=data.get('tags'))
if IDistribution.providedBy(context) and packagename:
# We don't know if the package name we got was a source or binary
# package name, so let the Soyuz API figure it out for us.
packagename = str(packagename.name)
try:
sourcepackagename, binarypackagename = (
context.guessPackageNames(packagename))
except NotFoundError:
# guessPackageNames may raise NotFoundError. It would be
# nicer to allow people to indicate a package even if
# never published, but the quick fix for now is to note
# the issue and move on.
notifications.append(
"The package %s is not published in %s; the "
"bug was targeted only to the distribution."
% (packagename, context.displayname))
params.comment += (
"\r\n\r\nNote: the original reporter indicated "
"the bug was in package %r; however, that package "
"was not published in %s." % (
packagename, context.displayname))
else:
context = context.getSourcePackage(sourcepackagename.name)
params.binarypackagename = binarypackagename
extra_data = self.extra_data
if extra_data.extra_description:
params.comment = "%s\n\n%s" % (
params.comment, extra_data.extra_description)
notifications.append(
'Additional information was added to the bug description.')
if extra_data.private:
params.private = extra_data.private
# Apply any extra options given by a bug supervisor.
if IHasBugSupervisor.providedBy(context):
if self.user.inTeam(context.bug_supervisor):
if 'assignee' in data:
params.assignee = data['assignee']
if 'status' in data:
params.status = data['status']
if 'importance' in data:
params.importance = data['importance']
if 'milestone' in data:
params.milestone = data['milestone']
self.added_bug = bug = context.createBug(params)
for comment in extra_data.comments:
bug.newMessage(self.user, bug.followup_subject(), comment)
notifications.append(
'A comment with additional information was added to the'
' bug report.')
# XXX 2007-01-19 gmb:
# We need to have a proper FileUpload widget rather than
# this rather hackish solution.
attachment = self.request.form.get(self.widgets['filecontent'].name)
if attachment or extra_data.attachments:
# Attach all the comments to a single empty comment.
attachment_comment = bug.newMessage(
owner=self.user, subject=bug.followup_subject(), content=None)
# Deal with attachments added in the filebug form.
if attachment:
# We convert slashes in filenames to hyphens to avoid
# problems.
filename = attachment.filename.replace('/', '-')
# If the user hasn't entered a description for the
# attachment we use its name.
file_description = None
if 'attachment_description' in data:
file_description = data['attachment_description']
if file_description is None:
file_description = filename
bug.addAttachment(
owner=self.user, data=StringIO(data['filecontent']),
filename=filename, description=file_description,
comment=attachment_comment, is_patch=data['patch'])
notifications.append(
'The file "%s" was attached to the bug report.' %
cgi.escape(filename))
for attachment in extra_data.attachments:
bug.linkAttachment(
owner=self.user, file_alias=attachment['file_alias'],
description=attachment['description'],
comment=attachment_comment,
send_notifications=False)
notifications.append(
'The file "%s" was attached to the bug report.' %
cgi.escape(attachment['file_alias'].filename))
if extra_data.subscribers:
# Subscribe additional subscribers to this bug
for subscriber in extra_data.subscribers:
valid_person_vocabulary = ValidPersonOrTeamVocabulary()
try:
person = valid_person_vocabulary.getTermByToken(
subscriber).value
except LookupError:
# We cannot currently pass this error up to the user, so
# we'll just ignore it.
pass
else:
bug.subscribe(person, self.user)
notifications.append(
'%s has been subscribed to this bug.' %
person.displayname)
submission_set = getUtility(IHWSubmissionSet)
for submission_key in extra_data.hwdb_submission_keys:
submission = submission_set.getBySubmissionKey(
submission_key, self.user)
if submission is not None:
bug.linkHWSubmission(submission)
# Give the user some feedback on the bug just opened.
for notification in notifications:
self.request.response.addNotification(notification)
if bug.security_related:
self.request.response.addNotification(
structured(
'Security-related bugs are by default private '
'(visible only to their direct subscribers). '
'You may choose to <a href="+secrecy">publicly '
'disclose</a> this bug.'))
if bug.private and not bug.security_related:
self.request.response.addNotification(
structured(
'This bug report has been marked private '
'(visible only to its direct subscribers). '
'You may choose to <a href="+secrecy">change this</a>.'))
self.request.response.redirect(canonical_url(bug.bugtasks[0]))
@action("Yes, this is the bug I'm trying to report",
name="this_is_my_bug", failure=handleSubmitBugFailure)
def this_is_my_bug_action(self, action, data):
"""Subscribe to the bug suggested."""
bug = data.get('bug_already_reported_as')
subscribe = data.get('subscribe_to_existing_bug')
if bug.isUserAffected(self.user):
self.request.response.addNotification(
"This bug is already marked as affecting you.")
else:
bug.markUserAffected(self.user)
self.request.response.addNotification(
"This bug has been marked as affecting you.")
# If the user wants to be subscribed, subscribe them, unless
# they're already subscribed.
if subscribe:
if bug.isSubscribed(self.user):
self.request.response.addNotification(
"You are already subscribed to this bug.")
else:
bug.subscribe(self.user, self.user)
self.request.response.addNotification(
"You have subscribed to this bug report.")
self.next_url = canonical_url(bug.bugtasks[0])
def showFileBugForm(self):
"""Override this method in base classes to show the filebug form."""
raise NotImplementedError
@property
def inline_filebug_base_url(self):
"""Return the base URL for the current request.
This allows us to build URLs in Javascript without guessing at
domains.
"""
return self.request.getRootURL(None)
@property
def inline_filebug_form_url(self):
"""Return the URL to the inline filebug form.
If a token was passed to this view, it will be be passed through
to the inline bug filing form via the returned URL.
"""
url = canonical_url(self.context, view_name='+filebug-inline-form')
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
@property
def duplicate_search_url(self):
"""Return the URL to the inline duplicate search view."""
url = canonical_url(self.context, view_name='+filebug-show-similar')
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
def publishTraverse(self, request, name):
"""See IBrowserPublisher."""
if self.extra_data_token is not None:
# publishTraverse() has already been called once before,
# which means that he URL contains more path components than
# expected.
raise NotFound(self, name, request=request)
self.extra_data_token = name
if self.extra_data_processing_job is None:
# The URL might be mistyped, or the blob has expired.
# XXX: Bjorn Tillenius 2006-01-15:
# We should handle this case better, since a user might
# come to this page when finishing his account
# registration. In that case we should inform the user
# that the blob has expired.
raise NotFound(self, name, request=request)
else:
self.extra_data = self.extra_data_processing_job.getFileBugData()
return self
def browserDefault(self, request):
"""See IBrowserPublisher."""
return self, ()
def getProductOrDistroFromContext(self):
"""Return the product or distribution relative to the context.
For instance, if the context is an IDistroSeries, return the
distribution related to it. Will return None if the context is
not related to a product or a distro.
"""
context = self.context
if IProduct.providedBy(context) or IDistribution.providedBy(context):
return context
elif IProductSeries.providedBy(context):
return context.product
elif (IDistroSeries.providedBy(context) or
IDistributionSourcePackage.providedBy(context)):
return context.distribution
else:
return None
def showOptionalMarker(self, field_name):
"""See `LaunchpadFormView`."""
# The comment field _is_ required, but only when filing the
# bug. Since the same form is also used for subscribing to a
# bug, the comment field in the schema cannot be marked
# required=True. Instead it's validated in
# FileBugViewBase.validate. So... we need to suppress the
# "(Optional)" marker.
if field_name == 'comment':
return False
else:
return LaunchpadFormView.showOptionalMarker(self, field_name)
def getRelevantBugTask(self, bug):
"""Return the first bugtask from this bug that's relevant in the
current context.
This is a pragmatic function, not general purpose. It tries to
find a bugtask that can be used to pretty-up the page, making
it more user-friendly and informative. It's not concerned by
total accuracy, and will return the first 'relevant' bugtask
it finds even if there are other candidates. Be warned!
"""
context = self.context
if IProjectGroup.providedBy(context):
contexts = set(context.products)
else:
contexts = [context]
for bugtask in bug.bugtasks:
if bugtask.target in contexts or bugtask.pillar in contexts:
return bugtask
return None
@property
def bugtarget(self):
"""The bugtarget we're currently assuming.
The same as the context.
"""
return self.context
default_bug_reported_acknowledgement = "Thank you for your bug report."
def getAcknowledgementMessage(self, context):
"""An acknowlegement message displayed to the user."""
# If a given context doesnot have a custom message, we go up in the
# "object hierachy" until we find one. If no cusotmized messages
# exist for any conext, a default message is returned.
#
# bug_reported_acknowledgement is defined as a "real" property
# for IDistribution, IDistributionSourcePackage, IProduct and
# IProjectGroup. Other IBugTarget implementations inherit this
# property from their parents. For these classes, we can directly
# try to find a custom message farther up in the hierarchy.
message = context.bug_reported_acknowledgement
if message is not None and len(message.strip()) > 0:
return message
next_context = None
if IProductSeries.providedBy(context):
# we don't need to look at
# context.product.bug_reported_acknowledgement because a
# product series inherits this property from the product.
next_context = context.product.project
elif IProduct.providedBy(context):
next_context = context.project
elif IDistributionSourcePackage.providedBy(context):
next_context = context.distribution
# IDistroseries and ISourcePackage inherit
# bug_reported_acknowledgement from their IDistribution, so we
# don't need to look up this property in IDistribution.
# IDistribution and IProjectGroup don't have any parents.
elif (IDistribution.providedBy(context) or
IProjectGroup.providedBy(context) or
IDistroSeries.providedBy(context) or
ISourcePackage.providedBy(context)):
pass
else:
raise TypeError("Unexpected bug target: %r" % context)
if next_context is not None:
return self.getAcknowledgementMessage(next_context)
return self.default_bug_reported_acknowledgement
@cachedproperty
def extra_data_processing_job(self):
"""Return the ProcessApportBlobJob for a given BLOB token."""
if self.extra_data_token is None:
# If there's no extra data token, don't bother looking for a
# ProcessApportBlobJob.
return None
try:
return getUtility(IProcessApportBlobJobSource).getByBlobUUID(
self.extra_data_token)
except SQLObjectNotFound:
return None
@property
def extra_data_to_process(self):
"""Return True if there is extra data to process."""
apport_processing_job = self.extra_data_processing_job
if apport_processing_job is None:
return False
elif apport_processing_job.job.status == JobStatus.COMPLETED:
return False
else:
return True
class FileBugInlineFormView(FileBugViewBase):
"""A browser view for displaying the inline filebug form."""
schema = IBugAddForm
class FileBugAdvancedView(FileBugViewBase):
"""Browser view for filing a bug.
This view exists only to redirect from +filebug-advanced to +filebug.
"""
def initialize(self):
filebug_url = canonical_url(
self.context, rootsite='bugs', view_name='+filebug')
self.request.response.redirect(
filebug_url, status=HTTP_MOVED_PERMANENTLY)
class FilebugShowSimilarBugsView(FileBugViewBase):
"""A view for showing possible dupes for a bug.
This view will only be used to populate asynchronously-driven parts
of a page.
"""
schema = IBugAddForm
# XXX: Brad Bollenbach 2006-10-04: This assignment to actions is a
# hack to make the action decorator Just Work across inheritance.
actions = FileBugViewBase.actions
custom_widget('title', TextWidget, displayWidth=40)
custom_widget('tags', BugTagsWidget)
_MATCHING_BUGS_LIMIT = 10
show_summary_in_results = False
@property
def action_url(self):
"""Return the +filebug page as the action URL.
This enables better validation error handling,
since the form is always used inline on the +filebug page.
"""
url = '%s/+filebug' % canonical_url(self.context)
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
@property
def search_context(self):
"""Return the context used to search for similar bugs."""
return self.context
@property
def search_text(self):
"""Return the search string entered by the user."""
return self.request.get('title')
@cachedproperty
def similar_bugs(self):
"""Return the similar bugs based on the user search."""
title = self.search_text
if not title:
return []
search_context = self.search_context
if search_context is None:
return []
elif IProduct.providedBy(search_context):
context_params = {'product': search_context}
elif IDistribution.providedBy(search_context):
context_params = {'distribution': search_context}
else:
assert IDistributionSourcePackage.providedBy(search_context), (
'Unknown search context: %r' % search_context)
context_params = {
'distribution': search_context.distribution,
'sourcepackagename': search_context.sourcepackagename}
matching_bugtasks = getUtility(IBugTaskSet).findSimilar(
self.user, title, **context_params)
matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(
matching_bugtasks, self.user, self._MATCHING_BUGS_LIMIT)
return matching_bugs
@property
def show_duplicate_list(self):
"""Return whether or not to show the duplicate list.
We only show the dupes if:
- The context uses Malone AND
- There are dupes to show AND
- There are no widget errors.
"""
return (
self.contextUsesMalone and
len(self.similar_bugs) > 0 and
len(self.widget_errors) == 0)
class FileBugGuidedView(FilebugShowSimilarBugsView):
_SEARCH_FOR_DUPES = ViewPageTemplateFile(
"../templates/bugtarget-filebug-search.pt")
_PROJECTGROUP_SEARCH_FOR_DUPES = ViewPageTemplateFile(
"../templates/projectgroup-filebug-search.pt")
_FILEBUG_FORM = ViewPageTemplateFile(
"../templates/bugtarget-filebug-submit-bug.pt")
# XXX 2009-07-17 Graham Binns
# As above, this assignment to actions is to make sure that the
# actions from the ancestor views are preserved, otherwise they
# get nuked.
actions = FilebugShowSimilarBugsView.actions
template = _SEARCH_FOR_DUPES
focused_element_id = 'field.title'
show_summary_in_results = True
def initialize(self):
FilebugShowSimilarBugsView.initialize(self)
if self.redirect_ubuntu_filebug:
# The user is trying to file a new Ubuntu bug via the web
# interface and without using apport. Redirect to a page
# explaining the preferred bug-filing procedure.
self.request.response.redirect(
config.malone.ubuntu_bug_filing_url)
@safe_action
@action("Continue", name="projectgroupsearch",
validator="validate_search")
def projectgroup_search_action(self, action, data):
"""Search for similar bug reports."""
# Don't give focus to any widget, to ensure that the browser
# won't scroll past the "possible duplicates" list.
self.initial_focus_widget = None
return self._PROJECTGROUP_SEARCH_FOR_DUPES()
@safe_action
@action("Continue", name="search", validator="validate_search")
def search_action(self, action, data):
"""Search for similar bug reports."""
# Don't give focus to any widget, to ensure that the browser
# won't scroll past the "possible duplicates" list.
self.initial_focus_widget = None
return self.showFileBugForm()
@property
def search_context(self):
"""Return the context used to search for similar bugs."""
if IDistributionSourcePackage.providedBy(self.context):
return self.context
search_context = self.getMainContext()
if IProjectGroup.providedBy(search_context):
assert self.widgets['product'].hasValidInput(), (
"This method should be called only when we know which"
" product the user selected.")
search_context = self.widgets['product'].getInputValue()
elif IMaloneApplication.providedBy(search_context):
if self.widgets['bugtarget'].hasValidInput():
search_context = self.widgets['bugtarget'].getInputValue()
else:
search_context = None
return search_context
@property
def search_text(self):
"""Return the search string entered by the user."""
try:
return self.widgets['title'].getInputValue()
except InputErrors:
return None
def validate_search(self, action, data):
"""Make sure some keywords are provided."""
try:
data['title'] = self.widgets['title'].getInputValue()
except InputErrors, error:
self.setFieldError("title", "A summary is required.")
return [error]
# Return an empty list of errors to satisfy the validation API,
# and say "we've handled the validation and found no errors."
return []
def validate_no_dupe_found(self, action, data):
return ()
@action("Continue", name="continue",
validator="validate_no_dupe_found")
def continue_action(self, action, data):
"""The same action as no-dupe-found, with a different label."""
return self.showFileBugForm()
def showFileBugForm(self):
return self._FILEBUG_FORM()
class ProjectFileBugGuidedView(FileBugGuidedView):
"""Guided filebug pages for IProjectGroup."""
# Make inheriting the base class' actions work.
actions = FileBugGuidedView.actions
schema = IProjectGroupBugAddForm
@cachedproperty
def products_using_malone(self):
return [
product for product in self.context.products
if product.bug_tracking_usage == ServiceUsage.LAUNCHPAD]
@property
def default_product(self):
if len(self.products_using_malone) > 0:
return self.products_using_malone[0]
else:
return None
@property
def inline_filebug_form_url(self):
"""Return the URL to the inline filebug form.
If a token was passed to this view, it will be be passed through
to the inline bug filing form via the returned URL.
The URL returned will be the URL of the first of the current
ProjectGroup's products, since that's the product that will be
selected by default when the view is rendered.
"""
url = canonical_url(
self.default_product, view_name='+filebug-inline-form')
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
@property
def duplicate_search_url(self):
"""Return the URL to the inline duplicate search view.
The URL returned will be the URL of the first of the current
ProjectGroup's products, since that's the product that will be
selected by default when the view is rendered.
"""
url = canonical_url(
self.default_product, view_name='+filebug-show-similar')
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
class BugTargetBugListingView:
"""Helper methods for rendering bug listings."""
@property
def series_list(self):
if IDistribution(self.context, None):
series = self.context.series
elif IProduct(self.context, None):
series = self.context.series
elif IDistroSeries(self.context, None):
series = self.context.distribution.series
elif IProductSeries(self.context, None):
series = self.context.product.series
else:
raise AssertionError("series_list called with illegal context")
return list(series)
@property
def milestones_list(self):
if IDistribution(self.context, None):
milestone_resultset = self.context.milestones
elif IProduct(self.context, None):
milestone_resultset = self.context.milestones
elif IDistroSeries(self.context, None):
milestone_resultset = self.context.distribution.milestones
elif IProductSeries(self.context, None):
milestone_resultset = self.context.product.milestones
else:
raise AssertionError("series_list called with illegal context")
return list(milestone_resultset)
@property
def series_buglistings(self):
"""Return a buglisting for each series.
The list is sorted newest series to oldest.
The count only considers bugs that the user would actually be
able to see in a listing.
"""
series_buglistings = []
bug_task_set = getUtility(IBugTaskSet)
series_list = self.series_list
if not series_list:
return series_buglistings
open_bugs = bug_task_set.open_bugtask_search
open_bugs.setTarget(any(*series_list))
# This would be better as delegation not a case statement.
if IDistribution(self.context, None):
backlink = BugTask.distroseriesID
elif IProduct(self.context, None):
backlink = BugTask.productseriesID
elif IDistroSeries(self.context, None):
backlink = BugTask.distroseriesID
elif IProductSeries(self.context, None):
backlink = BugTask.productseriesID
else:
raise AssertionError("illegal context %r" % self.context)
counts = bug_task_set.countBugs(open_bugs, (backlink,))
for series in series_list:
series_bug_count = counts.get((series.id,), 0)
if series_bug_count > 0:
series_buglistings.append(
dict(
title=series.name,
url=canonical_url(series) + "/+bugs",
count=series_bug_count,
))
return series_buglistings
@property
def milestone_buglistings(self):
"""Return a buglisting for each milestone."""
milestone_buglistings = []
bug_task_set = getUtility(IBugTaskSet)
milestones = self.milestones_list
if not milestones:
return milestone_buglistings
open_bugs = bug_task_set.open_bugtask_search
open_bugs.setTarget(any(*milestones))
counts = bug_task_set.countBugs(open_bugs, (BugTask.milestoneID,))
for milestone in milestones:
milestone_bug_count = counts.get((milestone.id,), 0)
if milestone_bug_count > 0:
milestone_buglistings.append(
dict(
title=milestone.name,
url=canonical_url(milestone),
count=milestone_bug_count,
))
return milestone_buglistings
class BugCountDataItem:
"""Data about bug count for a status."""
def __init__(self, label, count, color):
self.label = label
self.count = count
if color.startswith('#'):
self.color = 'MochiKit.Color.Color.fromHexString("%s")' % color
else:
self.color = 'MochiKit.Color.Color["%sColor"]()' % color
class BugTargetBugsView(BugTaskSearchListingView, FeedsMixin):
"""View for the Bugs front page."""
# We have a custom searchtext widget here so that we can set the
# width of the search box properly.
custom_widget('searchtext', NewLineToSpacesWidget, displayWidth=36)
# Only include <link> tags for bug feeds when using this view.
feed_types = (
BugFeedLink,
BugTargetLatestBugsFeedLink,
)
# XXX: Bjorn Tillenius 2007-02-13:
# These colors should be changed. It's the same colors that are used
# to color statuses in buglistings using CSS, but there should be one
# unique color for each status in the pie chart
status_color = {
BugTaskStatus.NEW: '#993300',
BugTaskStatus.INCOMPLETE: 'red',
BugTaskStatus.CONFIRMED: 'orange',
BugTaskStatus.TRIAGED: 'black',
BugTaskStatus.INPROGRESS: 'blue',
BugTaskStatus.FIXCOMMITTED: 'green',
BugTaskStatus.FIXRELEASED: 'magenta',
BugTaskStatus.INVALID: 'yellow',
BugTaskStatus.UNKNOWN: 'purple',
}
override_title_breadcrumbs = True
@property
def label(self):
"""The display label for the view."""
return 'Bugs in %s' % self.context.title
def initialize(self):
super(BugTargetBugsView, self).initialize()
bug_statuses_to_show = list(UNRESOLVED_BUGTASK_STATUSES)
if IDistroSeries.providedBy(self.context):
bug_statuses_to_show.append(BugTaskStatus.FIXRELEASED)
expose_structural_subscription_data_to_js(
self.context, self.request, self.user)
@property
def can_have_external_bugtracker(self):
return (IProduct.providedBy(self.context)
or IProductSeries.providedBy(self.context))
@property
def bug_tracking_usage(self):
"""Whether the context tracks bugs in launchpad.
:returns: ServiceUsage enum value
"""
service_usage = IServiceUsage(self.context)
return service_usage.bug_tracking_usage
@property
def bugtracker(self):
"""Description of the context's bugtracker.
:returns: str which may contain HTML.
"""
if self.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
return 'Launchpad'
elif self.external_bugtracker:
return BugTrackerFormatterAPI(self.external_bugtracker).link(None)
else:
return 'None specified'
@cachedproperty
def hot_bugs_info(self):
"""Return a dict of the 10 hottest tasks and a has_more_bugs flag."""
has_more_bugs = False
params = BugTaskSearchParams(
orderby=['-heat', 'task'], omit_dupes=True,
user=self.user, status=any(*UNRESOLVED_BUGTASK_STATUSES))
# Use 4x as many tasks as bugs that are needed to improve performance.
bugtasks = self.context.searchTasks(params)[:40]
hot_bugtasks = []
hot_bugs = []
for task in bugtasks:
# Use hot_bugs list to ensure a bug is only listed once.
if task.bug not in hot_bugs:
if len(hot_bugtasks) < 10:
hot_bugtasks.append(task)
hot_bugs.append(task.bug)
else:
has_more_bugs = True
break
return {'has_more_bugs': has_more_bugs, 'bugtasks': hot_bugtasks}
class BugTargetBugTagsView(LaunchpadView):
"""Helper methods for rendering the bug tags portlet."""
def _getSearchURL(self, tag):
"""Return the search URL for the tag."""
# Use path_only here to reduce the size of the rendered page.
return "+bugs?field.tag=%s" % urllib.quote(tag)
def _calculateFactor(self, tag, count, max_count, official_tags):
bonus = 1.5 if tag in official_tags else 1
return (count / max_count) + bonus
@property
def tags_cloud_data(self):
"""The data for rendering a tags cloud"""
official_tags = self.context.official_bug_tags
tags = self.context.getUsedBugTagsWithOpenCounts(
self.user, 10, official_tags)
max_count = float(max([1] + tags.values()))
return sorted(
[dict(
tag=tag,
factor=self._calculateFactor(
tag, count, max_count, official_tags),
url=self._getSearchURL(tag),
)
for (tag, count) in tags.iteritems()],
key=itemgetter('tag'))
@property
def show_manage_tags_link(self):
"""Should a link to a "manage official tags" page be shown?"""
return (IOfficialBugTagTargetRestricted.providedBy(self.context) and
check_permission('launchpad.BugSupervisor', self.context))
class OfficialBugTagsManageView(LaunchpadEditFormView):
"""View class for management of official bug tags."""
schema = IOfficialBugTagTargetPublic
custom_widget('official_bug_tags', LargeBugTagsWidget)
@property
def label(self):
"""The form label."""
return 'Manage official bug tags for %s' % self.context.title
@property
def page_title(self):
"""The page title."""
return self.label
@action('Save', name='save')
def save_action(self, action, data):
"""Action for saving new official bug tags."""
self.context.official_bug_tags = data['official_bug_tags']
self.next_url = canonical_url(self.context)
@property
def tags_js_data(self):
"""Return the JSON representation of the bug tags."""
# The model returns dict and list respectively but dumps blows up on
# security proxied objects.
used_tags = removeSecurityProxy(
self.context.getUsedBugTagsWithOpenCounts(self.user))
official_tags = removeSecurityProxy(self.context.official_bug_tags)
return """<script type="text/javascript">
var used_bug_tags = %s;
var official_bug_tags = %s;
var valid_name_pattern = %s;
</script>
""" % (
dumps(used_tags),
dumps(official_tags),
dumps(valid_name_pattern.pattern))
@property
def cancel_url(self):
"""The URL the user is sent to when clicking the "cancel" link."""
return canonical_url(self.context)
class BugsVHostBreadcrumb(Breadcrumb):
rootsite = 'bugs'
text = 'Bugs'
class BugsPatchesView(LaunchpadView):
"""View list of patch attachments associated with bugs."""
@property
def label(self):
"""The display label for the view."""
if IPerson.providedBy(self.context):
return 'Patch attachments for %s' % self.context.displayname
else:
return 'Patch attachments in %s' % self.context.displayname
@property
def patch_task_orderings(self):
"""The list of possible sort orderings for the patches view.
The orderings are a list of tuples of the form:
[(DisplayName, InternalOrderingName), ...]
For example:
[("Patch age", "-latest_patch_uploaded"),
("Importance", "-importance"),
...]
"""
orderings = [("patch age", "-latest_patch_uploaded"),
("importance", "-importance"),
("status", "status"),
("oldest first", "datecreated"),
("newest first", "-datecreated")]
targetname = self.targetName()
if targetname is not None:
# Lower case for consistency with the other orderings.
orderings.append((targetname.lower(), "targetname"))
return orderings
def batchedPatchTasks(self):
"""Return a BatchNavigator for bug tasks with patch attachments."""
orderby = self.request.get("orderby", "-latest_patch_uploaded")
if orderby not in [x[1] for x in self.patch_task_orderings]:
raise UnexpectedFormData(
"Unexpected value for field 'orderby': '%s'" % orderby)
return BatchNavigator(
self.context.searchTasks(
None, user=self.user, order_by=orderby,
status=UNRESOLVED_BUGTASK_STATUSES,
omit_duplicates=True, has_patch=True),
self.request)
def targetName(self):
"""Return the name of the current context's target type, or None.
The name is something like "Package" or "Project" (meaning
Product); it is intended to be appropriate to use as a column
name in a web page, for example. If no target type is
appropriate for the current context, then return None.
"""
if (IDistribution.providedBy(self.context) or
IDistroSeries.providedBy(self.context)):
return "Package"
elif (IProjectGroup.providedBy(self.context) or
IPerson.providedBy(self.context)):
# In the case of an IPerson, the target column can vary
# row-by-row, showing both packages and products. We
# decided to go with the table header "Project" for both,
# as its meaning is broad and could conceivably cover
# packages too. We also considered "Target", but rejected
# it because it's used as a verb elsewhere in Launchpad's
# UI, with a totally different meaning. If anyone can
# think of a better term than "Project", please JFDI here.
return "Project" # "Project" meaning Product, of course
else:
return None
def patchAge(self, patch):
"""Return a timedelta object for the age of a patch attachment."""
now = datetime.now(timezone('UTC'))
return now - patch.message.datecreated
def proxiedUrlForLibraryFile(self, patch):
"""Return the proxied download URL for a Librarian file."""
return ProxiedLibraryFileAlias(patch.libraryfile, patch).http_url
class TargetSubscriptionView(LaunchpadView):
"""A view to show all a person's structural subscriptions to a target."""
def initialize(self):
super(TargetSubscriptionView, self).initialize()
# Some resources such as help files are only provided on the bugs
# rootsite. So if we got here via another, possibly hand-crafted, URL
# redirect to the equivalent URL on the bugs rootsite.
if not BugsLayer.providedBy(self.request):
new_url = urljoin(
self.request.getRootURL('bugs'), self.request['PATH_INFO'])
self.request.response.redirect(new_url)
return
expose_structural_subscription_data_to_js(
self.context, self.request, self.user, self.subscriptions)
@property
def subscriptions(self):
return get_structural_subscriptions_for_target(
self.context, self.user)
@property
def label(self):
return "Your subscriptions to %s" % (self.context.displayname,)
page_title = label
|