48
52
IBugTarget, IOfficialBugTagTargetPublic, IOfficialBugTagTargetRestricted)
49
53
from lp.bugs.interfaces.bug import IBugSet
50
54
from lp.bugs.interfaces.bugtask import (
51
BugTaskStatus, IBugTaskSet, UNRESOLVED_BUGTASK_STATUSES)
55
BugTaskStatus, IBugTaskSet, UNRESOLVED_BUGTASK_STATUSES,
56
UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES)
52
57
from canonical.launchpad.interfaces.launchpad import (
53
58
IHasExternalBugTracker, ILaunchpadUsage)
54
59
from lp.hardwaredb.interfaces.hwdb import IHWSubmissionSet
55
60
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
56
from canonical.launchpad.interfaces.temporaryblobstorage import (
57
ITemporaryStorageManager)
58
61
from canonical.launchpad.searchbuilder import any
59
62
from canonical.launchpad.webapp import urlappend
60
63
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
61
from canonical.launchpad.webapp.interfaces import ILaunchBag, NotFoundError
64
from canonical.launchpad.webapp.interfaces import (
65
ILaunchBag, NotFoundError, UnexpectedFormData)
62
66
from lp.bugs.interfaces.bug import (
63
67
CreateBugParams, IBugAddForm, IProjectGroupBugAddForm)
64
68
from lp.bugs.interfaces.malone import IMaloneApplication
69
from lp.bugs.utilities.filebugdataparser import FileBugData
65
70
from lp.registry.interfaces.distribution import IDistribution
66
71
from lp.registry.interfaces.distributionsourcepackage import (
67
72
IDistributionSourcePackage)
68
73
from lp.registry.interfaces.distroseries import IDistroSeries
74
from lp.registry.interfaces.person import IPerson
69
75
from lp.registry.interfaces.product import IProduct
70
76
from lp.registry.interfaces.productseries import IProductSeries
71
77
from lp.registry.interfaces.projectgroup import IProjectGroup
72
78
from lp.registry.interfaces.sourcepackage import ISourcePackage
79
from lp.services.job.interfaces.job import JobStatus
73
80
from canonical.launchpad.webapp import (
74
81
LaunchpadEditFormView, LaunchpadFormView, LaunchpadView, action,
75
82
canonical_url, custom_widget, safe_action)
76
83
from canonical.launchpad.webapp.authorization import check_permission
84
from canonical.launchpad.webapp.batching import BatchNavigator
77
85
from canonical.launchpad.webapp.tales import BugTrackerFormatterAPI
78
86
from canonical.launchpad.validators.name import valid_name_pattern
79
87
from canonical.launchpad.webapp.menu import structured
84
92
from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
87
class FileBugDataParser:
88
"""Parser for a message containing extra bug information.
90
Applications like Apport upload such messages, before filing the
94
def __init__(self, blob_file):
95
self.blob_file = blob_file
98
self.extra_description = None
100
self.attachments = []
101
self.BUFFER_SIZE = 8192
103
def _consumeBytes(self, end_string):
104
"""Read bytes from the message up to the end_string.
106
The end_string is included in the output.
108
If end-of-file is reached, '' is returned.
110
while end_string not in self._buffer:
111
data = self.blob_file.read(self.BUFFER_SIZE)
113
if len(data) < self.BUFFER_SIZE:
115
if end_string not in self._buffer:
116
# If the end string isn't present, we return
118
buffer = self._buffer
122
end_index = self._buffer.index(end_string)
123
bytes = self._buffer[:end_index+len(end_string)]
124
self._buffer = self._buffer[end_index+len(end_string):]
127
def readHeaders(self):
128
"""Read the next set of headers of the message."""
129
header_text = self._consumeBytes('\n\n')
130
# Use the email package to return a dict-like object of the
131
# headers, so we don't have to parse the text ourselves.
132
return message_from_string(header_text)
135
"""Read a line of the message."""
136
data = self._consumeBytes('\n')
138
raise AssertionError('End of file reached.')
141
def _setDataFromHeaders(self, data, headers):
142
"""Set the data attributes from the message headers."""
143
if 'Subject' in headers:
144
data.initial_summary = unicode(headers['Subject'])
145
if 'Tags' in headers:
146
tags_string = unicode(headers['Tags'])
147
data.initial_tags = tags_string.lower().split()
148
if 'Private' in headers:
149
private = headers['Private']
150
if private.lower() == 'yes':
152
elif private.lower() == 'no':
155
# If the value is anything other than yes or no we just
156
# ignore it as we cannot currently give the user an error
158
if 'Subscribers' in headers:
159
subscribers_string = unicode(headers['Subscribers'])
160
data.subscribers = subscribers_string.lower().split()
161
if 'HWDB-Submission' in headers:
162
submission_string = unicode(headers['HWDB-Submission'])
163
data.hwdb_submission_keys = (
164
part.strip() for part in submission_string.split(','))
167
"""Parse the message and return a FileBugData instance.
169
* The Subject header is the initial bug summary.
170
* The Tags header specifies the initial bug tags.
171
* The Private header sets the visibility of the bug.
172
* The Subscribers header specifies additional initial subscribers
173
* The first inline part will be added to the description.
174
* All other inline parts will be added as separate comments.
175
* All attachment parts will be added as attachment.
177
When parsing each part of the message is stored in a temporary
178
file on the file system. After using the returned data,
179
removeTemporaryFiles() must be called.
181
headers = self.readHeaders()
183
self._setDataFromHeaders(data, headers)
185
# The headers is a Message instance.
186
boundary = "--" + headers.get_param("boundary")
187
line = self.readLine()
188
while not line.startswith(boundary + '--'):
189
part_file = tempfile.TemporaryFile()
190
part_headers = self.readHeaders()
191
content_encoding = part_headers.get('Content-Transfer-Encoding')
192
if content_encoding is not None and content_encoding != 'base64':
193
raise AssertionError(
194
"Unknown encoding: %r." % content_encoding)
195
line = self.readLine()
196
while not line.startswith(boundary):
198
if content_encoding is not None:
199
line = line.decode(content_encoding)
200
part_file.write(line)
201
line = self.readLine()
202
# Prepare the file for reading.
204
disposition = part_headers['Content-Disposition']
205
disposition = disposition.split(';')[0]
206
disposition = disposition.strip()
207
if disposition == 'inline':
208
assert part_headers.get_content_type() == 'text/plain', (
209
"Inline parts have to be plain text.")
210
charset = part_headers.get_content_charset()
212
"A charset has to be specified for text parts.")
213
inline_content = part_file.read().rstrip()
215
inline_content = inline_content.decode(charset)
217
if data.extra_description is None:
218
# The first inline part is extra description.
219
data.extra_description = inline_content
221
data.comments.append(inline_content)
222
elif disposition == 'attachment':
224
filename=unicode(part_headers.get_filename().strip("'")),
225
content_type=unicode(part_headers['Content-type']),
227
if 'Content-Description' in part_headers:
228
attachment['description'] = unicode(
229
part_headers['Content-Description'])
231
attachment['description'] = attachment['filename']
232
data.attachments.append(attachment)
234
# If the message include other disposition types,
235
# simply ignore them. We don't want to break just
236
# because some extra information is included.
242
"""Extra data to be added to the bug."""
245
self.initial_summary = None
246
self.initial_summary = None
247
self.initial_tags = []
249
self.subscribers = []
250
self.extra_description = None
252
self.attachments = []
253
self.hwdb_submission_keys = []
256
95
# A simple vocabulary for the subscribe_to_existing_bug form field.
257
96
SUBSCRIBE_TO_BUG_VOCABULARY = SimpleVocabulary.fromItems(
258
97
[('yes', True), ('no', False)])
1408
1268
class BugsVHostBreadcrumb(Breadcrumb):
1409
1269
rootsite = 'bugs'
1273
class BugsPatchesView(LaunchpadView):
1274
"""View list of patch attachments associated with bugs."""
1278
"""The display label for the view."""
1279
if IPerson.providedBy(self.context):
1280
return 'Patch attachments for %s' % self.context.displayname
1282
return 'Patch attachments in %s' % self.context.displayname
1285
def patch_task_orderings(self):
1286
"""The list of possible sort orderings for the patches view.
1288
The orderings are a list of tuples of the form:
1289
[(DisplayName, InternalOrderingName), ...]
1291
[("Patch age", "-latest_patch_uploaded"),
1292
("Importance", "-importance"),
1295
orderings = [("patch age", "-latest_patch_uploaded"),
1296
("importance", "-importance"),
1297
("status", "status"),
1298
("oldest first", "datecreated"),
1299
("newest first", "-datecreated")]
1300
targetname = self.targetName()
1301
if targetname is not None:
1302
# Lower case for consistency with the other orderings.
1303
orderings.append((targetname.lower(), "targetname"))
1307
def batchedPatchTasks(self):
1308
"""Return a BatchNavigator for bug tasks with patch attachments."""
1309
orderby = self.request.get("orderby", "-latest_patch_uploaded")
1310
if orderby not in [x[1] for x in self.patch_task_orderings]:
1311
raise UnexpectedFormData(
1312
"Unexpected value for field 'orderby': '%s'" % orderby)
1313
return BatchNavigator(
1314
self.context.searchTasks(
1315
None, user=self.user, order_by=orderby,
1316
status=UNRESOLVED_PLUS_FIXRELEASED_BUGTASK_STATUSES,
1317
omit_duplicates=True, has_patch=True),
1320
def targetName(self):
1321
"""Return the name of the current context's target type, or None.
1323
The name is something like "Package" or "Project" (meaning
1324
Product); it is intended to be appropriate to use as a column
1325
name in a web page, for example. If no target type is
1326
appropriate for the current context, then return None.
1328
if (IDistribution.providedBy(self.context) or
1329
IDistroSeries.providedBy(self.context)):
1331
elif (IProjectGroup.providedBy(self.context) or
1332
IPerson.providedBy(self.context)):
1333
# In the case of an IPerson, the target column can vary
1334
# row-by-row, showing both packages and products. We
1335
# decided to go with the table header "Project" for both,
1336
# as its meaning is broad and could conceivably cover
1337
# packages too. We also considered "Target", but rejected
1338
# it because it's used as a verb elsewhere in Launchpad's
1339
# UI, with a totally different meaning. If anyone can
1340
# think of a better term than "Project", please JFDI here.
1341
return "Project" # "Project" meaning Product, of course
1345
def patchAge(self, patch):
1346
"""Return a timedelta object for the age of a patch attachment."""
1347
now = datetime.now(timezone('UTC'))
1348
return now - patch.message.datecreated