Merge ~andrey-fedoseev/launchpad:bug-attachment-url into launchpad:master
- Git
- lp:~andrey-fedoseev/launchpad
- bug-attachment-url
- Merge into master
Proposed by
Andrey Fedoseev
Status: | Merged |
---|---|
Approved by: | Andrey Fedoseev |
Approved revision: | e33a250e8386acd41143f94f514d8757ce516a90 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~andrey-fedoseev/launchpad:bug-attachment-url |
Merge into: | launchpad:master |
Diff against target: |
1734 lines (+424/-216) 38 files modified
lib/canonical/launchpad/icing/css/components/sidebar_portlets.css (+6/-0) lib/lp/app/browser/configure.zcml (+6/-0) lib/lp/app/browser/tales.py (+38/-1) lib/lp/bugs/adapters/bugchange.py (+4/-10) lib/lp/bugs/browser/bug.py (+11/-21) lib/lp/bugs/browser/bugattachment.py (+37/-28) lib/lp/bugs/browser/bugcomment.py (+0/-7) lib/lp/bugs/browser/bugmessage.py (+39/-12) lib/lp/bugs/browser/bugtarget.py (+2/-5) lib/lp/bugs/browser/tests/bug-views.rst (+4/-4) lib/lp/bugs/browser/tests/test_bug_views.py (+19/-0) lib/lp/bugs/browser/tests/test_bugview.py (+2/-8) lib/lp/bugs/doc/bug-export.rst (+23/-5) lib/lp/bugs/doc/bug.rst (+1/-0) lib/lp/bugs/doc/bugattachments.rst (+13/-1) lib/lp/bugs/doc/bugcomment.rst (+6/-0) lib/lp/bugs/interfaces/bug.py (+9/-4) lib/lp/bugs/interfaces/bugattachment.py (+15/-3) lib/lp/bugs/interfaces/bugmessage.py (+2/-1) lib/lp/bugs/mail/handler.py (+1/-0) lib/lp/bugs/model/bug.py (+63/-37) lib/lp/bugs/model/bugattachment.py (+36/-4) lib/lp/bugs/model/tests/test_bugtasksearch.py (+2/-0) lib/lp/bugs/scripts/bugexport.py (+20/-18) lib/lp/bugs/scripts/bugimport.py (+1/-0) lib/lp/bugs/scripts/tests/test_bugnotification.py (+1/-1) lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.rst (+2/-0) lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst (+3/-0) lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst (+1/-0) lib/lp/bugs/stories/patches-view/patches-view.rst (+12/-14) lib/lp/bugs/stories/webservice/xx-bug.rst (+3/-0) lib/lp/bugs/templates/bug-attachment-macros.pt (+6/-0) lib/lp/bugs/templates/bug-portlet-attachments.pt (+8/-16) lib/lp/bugs/templates/bugcomment-box.pt (+6/-5) lib/lp/bugs/templates/bugtarget-patches.pt (+3/-4) lib/lp/bugs/tests/bugs-emailinterface.rst (+1/-1) lib/lp/scripts/tests/test_garbo.py (+4/-2) lib/lp/testing/factory.py (+14/-4) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+430983@code.launchpad.net |
Commit message
Add `url` to `BugAttachment`
Description of the change
`libraryfile` field of `BugAttachment` is now optional, an attachment can either contain a file, or have a URL
Add `displayed_url` property to `BugAttachment` that contains either a proxied attachment file URL, or the URL associated with the attachment.
Improve the `BugAttachment` links:
- Use the formatter API to create the links
- Use different icons depending on the link type: external URL, a patch, a downloadable file
- Add rel=nofollow to external URLs
To post a comment you must log in.
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote : | # |
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote : | # |
More screenshots:
Before:
https:/
After:
https:/
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
I like the use of the formatter API and the new `displayed_url` property.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css b/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css |
2 | index e4f4ff3..37487d2 100644 |
3 | --- a/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css |
4 | +++ b/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css |
5 | @@ -132,3 +132,9 @@ |
6 | padding-right: 0.25em; |
7 | } |
8 | |
9 | +/****************************************************************************** |
10 | + Attachments portlet |
11 | +*/ |
12 | +.side .portlet .download-attachment { |
13 | + word-break: break-word; |
14 | + } |
15 | diff --git a/lib/lp/app/browser/configure.zcml b/lib/lp/app/browser/configure.zcml |
16 | index 88985a9..cc40021 100644 |
17 | --- a/lib/lp/app/browser/configure.zcml |
18 | +++ b/lib/lp/app/browser/configure.zcml |
19 | @@ -932,6 +932,12 @@ |
20 | name="fmt" |
21 | /> |
22 | <adapter |
23 | + for="lp.bugs.interfaces.bugattachment.IBugAttachment" |
24 | + provides="zope.traversing.interfaces.IPathAdapter" |
25 | + factory="lp.app.browser.tales.BugAttachmentFormatterAPI" |
26 | + name="fmt" |
27 | + /> |
28 | + <adapter |
29 | for="lp.services.webapp.interfaces.ILink" |
30 | provides="zope.traversing.interfaces.IPathAdapter" |
31 | factory="lp.app.browser.tales.LinkFormatterAPI" |
32 | diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py |
33 | index a413347..89a29bc 100644 |
34 | --- a/lib/lp/app/browser/tales.py |
35 | +++ b/lib/lp/app/browser/tales.py |
36 | @@ -44,6 +44,7 @@ from lp.app.interfaces.launchpad import ( |
37 | from lp.blueprints.interfaces.specification import ISpecification |
38 | from lp.blueprints.interfaces.sprint import ISprint |
39 | from lp.bugs.interfaces.bug import IBug |
40 | +from lp.bugs.interfaces.bugattachment import IBugAttachment |
41 | from lp.buildmaster.enums import BuildStatus |
42 | from lp.code.enums import RevisionStatusResult |
43 | from lp.code.interfaces.branch import IBranch |
44 | @@ -726,7 +727,6 @@ class ObjectImageDisplayAPI: |
45 | def __init__(self, context): |
46 | self._context = context |
47 | |
48 | - # def default_icon_resource(self, context): |
49 | def sprite_css(self): |
50 | """Return the CSS class for the sprite""" |
51 | # XXX: mars 2008-08-22 bug=260468 |
52 | @@ -754,6 +754,13 @@ class ObjectImageDisplayAPI: |
53 | sprite_string = "meeting" |
54 | elif IBug.providedBy(context): |
55 | sprite_string = "bug" |
56 | + elif IBugAttachment.providedBy(context): |
57 | + if context.url: |
58 | + sprite_string = "external-link" |
59 | + elif context.is_patch: |
60 | + sprite_string = "haspatch-icon" |
61 | + else: |
62 | + sprite_string = "download-icon" |
63 | elif IPPA.providedBy(context): |
64 | if context.enabled: |
65 | sprite_string = "ppa-icon" |
66 | @@ -1866,6 +1873,36 @@ class BugTaskFormatterAPI(CustomizableFormatter): |
67 | return BugFormatterAPI(self._context.bug)._make_link_summary() |
68 | |
69 | |
70 | +class BugAttachmentFormatterAPI(CustomizableFormatter): |
71 | + |
72 | + final_traversable_names = dict( |
73 | + **CustomizableFormatter.final_traversable_names, |
74 | + attachment_link="attachment_link", |
75 | + ) |
76 | + |
77 | + def attachment_link(self): |
78 | + sprite = self.sprite_css() |
79 | + if sprite is None: |
80 | + css = "" |
81 | + else: |
82 | + css = ' class="' + sprite + '"' |
83 | + |
84 | + if self._context.url: |
85 | + rel = ' rel="nofollow"' |
86 | + else: |
87 | + rel = "" |
88 | + |
89 | + title = structured(self._context.title).escapedtext |
90 | + url = structured(self._context.displayed_url).escapedtext |
91 | + |
92 | + return """<a href="{url}"{css}{rel}>{title}</a>""".format( |
93 | + title=title, |
94 | + url=url, |
95 | + css=css, |
96 | + rel=rel, |
97 | + ) |
98 | + |
99 | + |
100 | class CodeImportFormatterAPI(CustomizableFormatter): |
101 | """Adapter providing fmt support for CodeImport objects""" |
102 | |
103 | diff --git a/lib/lp/bugs/adapters/bugchange.py b/lib/lp/bugs/adapters/bugchange.py |
104 | index c7babdc..e97d6e8 100644 |
105 | --- a/lib/lp/bugs/adapters/bugchange.py |
106 | +++ b/lib/lp/bugs/adapters/bugchange.py |
107 | @@ -63,7 +63,6 @@ from lp.bugs.interfaces.bugtask import ( |
108 | IBugTask, |
109 | ) |
110 | from lp.registry.interfaces.product import IProduct |
111 | -from lp.services.librarian.browser import ProxiedLibraryFileAlias |
112 | from lp.services.webapp.publisher import canonical_url |
113 | |
114 | # These are used lp.bugs.model.bugactivity.BugActivity.attribute to normalize |
115 | @@ -698,11 +697,6 @@ class BugTagsChange(AttributeChange): |
116 | return {"text": "\n".join(messages)} |
117 | |
118 | |
119 | -def download_url_of_bugattachment(attachment): |
120 | - """Return the URL of the ProxiedLibraryFileAlias for the attachment.""" |
121 | - return ProxiedLibraryFileAlias(attachment.libraryfile, attachment).http_url |
122 | - |
123 | - |
124 | class BugAttachmentChange(AttributeChange): |
125 | """Used to represent a change to an `IBug`'s attachments.""" |
126 | |
127 | @@ -712,13 +706,13 @@ class BugAttachmentChange(AttributeChange): |
128 | old_value = None |
129 | new_value = "%s %s" % ( |
130 | self.new_value.title, |
131 | - download_url_of_bugattachment(self.new_value), |
132 | + self.new_value.displayed_url, |
133 | ) |
134 | else: |
135 | what_changed = ATTACHMENT_REMOVED |
136 | old_value = "%s %s" % ( |
137 | self.old_value.title, |
138 | - download_url_of_bugattachment(self.old_value), |
139 | + self.old_value.displayed_url, |
140 | ) |
141 | new_value = None |
142 | |
143 | @@ -737,7 +731,7 @@ class BugAttachmentChange(AttributeChange): |
144 | message = '** %s added: "%s"\n %s' % ( |
145 | attachment_str, |
146 | self.new_value.title, |
147 | - download_url_of_bugattachment(self.new_value), |
148 | + self.new_value.displayed_url, |
149 | ) |
150 | else: |
151 | if self.old_value.is_patch: |
152 | @@ -747,7 +741,7 @@ class BugAttachmentChange(AttributeChange): |
153 | message = '** %s removed: "%s"\n %s' % ( |
154 | attachment_str, |
155 | self.old_value.title, |
156 | - download_url_of_bugattachment(self.old_value), |
157 | + self.old_value.displayed_url, |
158 | ) |
159 | |
160 | return {"text": message} |
161 | diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py |
162 | index 3a675a8..717f39c 100644 |
163 | --- a/lib/lp/bugs/browser/bug.py |
164 | +++ b/lib/lp/bugs/browser/bug.py |
165 | @@ -85,7 +85,6 @@ from lp.registry.interfaces.person import IPersonSet |
166 | from lp.services.compat import message_as_bytes |
167 | from lp.services.features import getFeatureFlag |
168 | from lp.services.fields import DuplicateBug |
169 | -from lp.services.librarian.browser import ProxiedLibraryFileAlias |
170 | from lp.services.mail.mailwrapper import MailWrapper |
171 | from lp.services.propertycache import cachedproperty |
172 | from lp.services.searchbuilder import any, not_equals |
173 | @@ -534,17 +533,11 @@ class BugViewMixin: |
174 | "other": [], |
175 | } |
176 | for attachment in self.context.attachments_unpopulated: |
177 | - info = { |
178 | - "attachment": attachment, |
179 | - "file": ProxiedLibraryFileAlias( |
180 | - attachment.libraryfile, attachment |
181 | - ), |
182 | - } |
183 | if attachment.type == BugAttachmentType.PATCH: |
184 | key = attachment.type |
185 | else: |
186 | key = "other" |
187 | - result[key].append(info) |
188 | + result[key].append(attachment) |
189 | return result |
190 | |
191 | @property |
192 | @@ -636,12 +629,6 @@ class BugView(LaunchpadView, BugViewMixin): |
193 | |
194 | return dupes |
195 | |
196 | - def proxiedUrlForLibraryFile(self, attachment): |
197 | - """Return the proxied download URL for a Librarian file.""" |
198 | - return ProxiedLibraryFileAlias( |
199 | - attachment.libraryfile, attachment |
200 | - ).http_url |
201 | - |
202 | |
203 | class BugActivity(BugView): |
204 | |
205 | @@ -1304,13 +1291,16 @@ class BugTextView(LaunchpadView): |
206 | |
207 | def attachment_text(self, attachment): |
208 | """Return a text representation of a bug attachment.""" |
209 | - mime_type = normalize_mime_type.sub( |
210 | - " ", attachment.libraryfile.mimetype |
211 | - ) |
212 | - download_url = ProxiedLibraryFileAlias( |
213 | - attachment.libraryfile, attachment |
214 | - ).http_url |
215 | - return "%s %s" % (download_url, mime_type) |
216 | + if attachment.url: |
217 | + if attachment.title != attachment.url: |
218 | + return "{}: {}".format(attachment.title, attachment.url) |
219 | + return attachment.url |
220 | + elif attachment.libraryfile: |
221 | + mime_type = normalize_mime_type.sub( |
222 | + " ", attachment.libraryfile.mimetype |
223 | + ) |
224 | + return "{} {}".format(attachment.displayed_url, mime_type) |
225 | + raise AssertionError() |
226 | |
227 | def comment_text(self): |
228 | """Return a text representation of bug comments.""" |
229 | diff --git a/lib/lp/bugs/browser/bugattachment.py b/lib/lp/bugs/browser/bugattachment.py |
230 | index 7d45e45..ce004b8 100644 |
231 | --- a/lib/lp/bugs/browser/bugattachment.py |
232 | +++ b/lib/lp/bugs/browser/bugattachment.py |
233 | @@ -25,10 +25,7 @@ from lp.bugs.interfaces.bugattachment import ( |
234 | IBugAttachmentIsPatchConfirmationForm, |
235 | IBugAttachmentSet, |
236 | ) |
237 | -from lp.services.librarian.browser import ( |
238 | - FileNavigationMixin, |
239 | - ProxiedLibraryFileAlias, |
240 | -) |
241 | +from lp.services.librarian.browser import FileNavigationMixin |
242 | from lp.services.librarian.interfaces import ILibraryFileAliasWithParent |
243 | from lp.services.webapp import GetitemNavigation, Navigation, canonical_url |
244 | from lp.services.webapp.authorization import check_permission |
245 | @@ -115,15 +112,22 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck): |
246 | self.next_url = self.cancel_url = canonical_url( |
247 | ICanonicalUrlData(context).inside |
248 | ) |
249 | + if not context.libraryfile: |
250 | + # Remove `contenttype` from the form if there's no file attached. |
251 | + self.field_names = [ |
252 | + name for name in self.field_names if name != "contenttype" |
253 | + ] |
254 | |
255 | @property |
256 | def initial_values(self): |
257 | attachment = self.context |
258 | - return dict( |
259 | + values = dict( |
260 | title=attachment.title, |
261 | patch=attachment.type == BugAttachmentType.PATCH, |
262 | - contenttype=attachment.libraryfile.mimetype, |
263 | ) |
264 | + if attachment.libraryfile: |
265 | + values["contenttype"] = attachment.libraryfile.mimetype |
266 | + return values |
267 | |
268 | def canEditAttachment(self, action): |
269 | return check_permission("launchpad.Edit", self.context) |
270 | @@ -135,30 +139,38 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck): |
271 | else: |
272 | new_type = BugAttachmentType.UNSPECIFIED |
273 | if new_type != self.context.type: |
274 | - filename = self.context.libraryfile.filename |
275 | - file_content = self.context.libraryfile.read() |
276 | - # We expect that users set data['patch'] to True only for |
277 | - # real patch data, indicated by guessed_content_type == |
278 | - # 'text/x-diff'. If there are inconsistencies, we don't |
279 | - # set the value automatically. Instead, we lead the user to |
280 | - # another form where we ask them if they are sure about their |
281 | - # choice of the patch flag. |
282 | - new_type_consistent_with_guessed_type = ( |
283 | - self.attachmentTypeConsistentWithContentType( |
284 | - new_type == BugAttachmentType.PATCH, filename, file_content |
285 | + if self.context.libraryfile: |
286 | + filename = self.context.libraryfile.filename |
287 | + file_content = self.context.libraryfile.read() |
288 | + # We expect that users set data['patch'] to True only for |
289 | + # real patch data, indicated by guessed_content_type == |
290 | + # 'text/x-diff'. If there are inconsistencies, we don't |
291 | + # set the value automatically. Instead, we lead the user to |
292 | + # another form where we ask them if they are sure about their |
293 | + # choice of the patch flag. |
294 | + new_type_consistent_with_guessed_type = ( |
295 | + self.attachmentTypeConsistentWithContentType( |
296 | + new_type == BugAttachmentType.PATCH, |
297 | + filename, |
298 | + file_content, |
299 | + ) |
300 | ) |
301 | - ) |
302 | - if new_type_consistent_with_guessed_type: |
303 | - self.context.type = new_type |
304 | + if new_type_consistent_with_guessed_type: |
305 | + self.context.type = new_type |
306 | + else: |
307 | + self.next_url = self.nextUrlForInconsistentPatchFlags( |
308 | + self.context |
309 | + ) |
310 | else: |
311 | - self.next_url = self.nextUrlForInconsistentPatchFlags( |
312 | - self.context |
313 | - ) |
314 | + self.context.type = new_type |
315 | |
316 | if data["title"] != self.context.title: |
317 | self.context.title = data["title"] |
318 | |
319 | - if self.context.libraryfile.mimetype != data["contenttype"]: |
320 | + if ( |
321 | + self.context.libraryfile |
322 | + and self.context.libraryfile.mimetype != data["contenttype"] |
323 | + ): |
324 | lfa_with_parent = getMultiAdapter( |
325 | (self.context.libraryfile, self.context), |
326 | ILibraryFileAliasWithParent, |
327 | @@ -167,14 +179,11 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck): |
328 | |
329 | @action("Delete Attachment", name="delete", condition=canEditAttachment) |
330 | def delete_action(self, action, data): |
331 | - libraryfile_url = ProxiedLibraryFileAlias( |
332 | - self.context.libraryfile, self.context |
333 | - ).http_url |
334 | self.request.response.addInfoNotification( |
335 | structured( |
336 | 'Attachment "<a href="%(url)s">%(name)s</a>" has been ' |
337 | "deleted.", |
338 | - url=libraryfile_url, |
339 | + url=self.context.displayed_url, |
340 | name=self.context.title, |
341 | ) |
342 | ) |
343 | diff --git a/lib/lp/bugs/browser/bugcomment.py b/lib/lp/bugs/browser/bugcomment.py |
344 | index 6a32ff1..0f653f6 100644 |
345 | --- a/lib/lp/bugs/browser/bugcomment.py |
346 | +++ b/lib/lp/bugs/browser/bugcomment.py |
347 | @@ -29,7 +29,6 @@ from lp.bugs.interfaces.bugmessage import IBugComment |
348 | from lp.services.comments.browser.comment import download_body |
349 | from lp.services.comments.browser.messagecomment import MessageComment |
350 | from lp.services.config import config |
351 | -from lp.services.librarian.browser import ProxiedLibraryFileAlias |
352 | from lp.services.messages.interfaces.message import IMessage |
353 | from lp.services.propertycache import cachedproperty, get_property_cache |
354 | from lp.services.webapp import ( |
355 | @@ -378,12 +377,6 @@ class BugCommentBoxViewMixin: |
356 | else: |
357 | return False |
358 | |
359 | - def proxiedUrlOfLibraryFileAlias(self, attachment): |
360 | - """Return the proxied URL for the Librarian file of the attachment.""" |
361 | - return ProxiedLibraryFileAlias( |
362 | - attachment.libraryfile, attachment |
363 | - ).http_url |
364 | - |
365 | |
366 | class BugCommentBoxView(LaunchpadView, BugCommentBoxViewMixin): |
367 | """Render a comment box with reply field collapsed.""" |
368 | diff --git a/lib/lp/bugs/browser/bugmessage.py b/lib/lp/bugs/browser/bugmessage.py |
369 | index b0267a1..30cff9e 100644 |
370 | --- a/lib/lp/bugs/browser/bugmessage.py |
371 | +++ b/lib/lp/bugs/browser/bugmessage.py |
372 | @@ -10,6 +10,7 @@ __all__ = [ |
373 | from io import BytesIO |
374 | |
375 | from zope.component import getUtility |
376 | +from zope.formlib.textwidgets import TextWidget |
377 | from zope.formlib.widget import CustomWidgetFactory |
378 | from zope.formlib.widgets import TextAreaWidget |
379 | |
380 | @@ -29,6 +30,9 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
381 | custom_widget_comment = CustomWidgetFactory( |
382 | TextAreaWidget, cssClass="comment-text" |
383 | ) |
384 | + custom_widget_attachment_url = CustomWidgetFactory( |
385 | + TextWidget, displayWidth=44, displayMaxWidth=250 |
386 | + ) |
387 | |
388 | page_title = "Add a comment or attachment" |
389 | |
390 | @@ -49,14 +53,19 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
391 | |
392 | def validate(self, data): |
393 | |
394 | - # Ensure either a comment or filecontent was provide, but only |
395 | - # if no errors have already been noted. |
396 | + # Ensure either a comment or filecontent or a URL was provided, |
397 | + # but only if no errors have already been noted. |
398 | if len(self.errors) == 0: |
399 | comment = data.get("comment") or "" |
400 | filecontent = data.get("filecontent", None) |
401 | - if not comment.strip() and not filecontent: |
402 | + attachment_url = data.get("attachment_url") or "" |
403 | + if ( |
404 | + not comment.strip() |
405 | + and not filecontent |
406 | + and not attachment_url.strip() |
407 | + ): |
408 | self.addError( |
409 | - "Either a comment or attachment " "must be provided." |
410 | + "Either a comment or attachment must be provided." |
411 | ) |
412 | |
413 | @action("Post Comment", name="save") |
414 | @@ -73,8 +82,10 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
415 | # Write proper FileUpload field and widget instead of this hack. |
416 | file_ = self.request.form.get(self.widgets["filecontent"].name) |
417 | |
418 | + attachment_url = data.get("attachment_url") |
419 | + |
420 | message = None |
421 | - if data["comment"] or file_: |
422 | + if data["comment"] or file_ or attachment_url: |
423 | bugwatch_id = data.get("bugwatch_id") |
424 | if bugwatch_id is not None: |
425 | bugwatch = getUtility(IBugWatchSet).get(bugwatch_id) |
426 | @@ -87,7 +98,7 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
427 | bugwatch=bugwatch, |
428 | ) |
429 | |
430 | - # A blank comment with only a subect line is always added |
431 | + # A blank comment with only a subject line is always added |
432 | # when the user attaches a file, so show the add comment |
433 | # feedback message only when the user actually added a |
434 | # comment. |
435 | @@ -97,6 +108,9 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
436 | ) |
437 | |
438 | self.next_url = canonical_url(self.context) |
439 | + |
440 | + attachment_description = data.get("attachment_description") |
441 | + |
442 | if file_: |
443 | |
444 | # Slashes in filenames cause problems, convert them to dashes |
445 | @@ -104,11 +118,8 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
446 | filename = file_.filename.replace("/", "-") |
447 | |
448 | # if no description was given use the converted filename |
449 | - file_description = None |
450 | - if "attachment_description" in data: |
451 | - file_description = data["attachment_description"] |
452 | - if not file_description: |
453 | - file_description = filename |
454 | + if not attachment_description: |
455 | + attachment_description = filename |
456 | |
457 | # Process the attachment. |
458 | # If the patch flag is not consistent with the result of |
459 | @@ -132,7 +143,8 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
460 | owner=self.user, |
461 | data=BytesIO(data["filecontent"]), |
462 | filename=filename, |
463 | - description=file_description, |
464 | + url=None, |
465 | + description=attachment_description, |
466 | comment=message, |
467 | is_patch=is_patch, |
468 | ) |
469 | @@ -146,6 +158,21 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck): |
470 | "Attachment %s added to bug." % filename |
471 | ) |
472 | |
473 | + elif attachment_url: |
474 | + is_patch = data["patch"] |
475 | + bug.addAttachment( |
476 | + owner=self.user, |
477 | + data=None, |
478 | + filename=None, |
479 | + url=attachment_url, |
480 | + description=attachment_description, |
481 | + comment=message, |
482 | + is_patch=is_patch, |
483 | + ) |
484 | + self.request.response.addNotification( |
485 | + "Attachment %s added to bug." % attachment_url |
486 | + ) |
487 | + |
488 | def shouldShowEmailMeWidget(self): |
489 | """Should the subscribe checkbox be shown?""" |
490 | return not self.context.bug.isSubscribed(self.user) |
491 | diff --git a/lib/lp/bugs/browser/bugtarget.py b/lib/lp/bugs/browser/bugtarget.py |
492 | index e7b3dc6..43e084f 100644 |
493 | --- a/lib/lp/bugs/browser/bugtarget.py |
494 | +++ b/lib/lp/bugs/browser/bugtarget.py |
495 | @@ -117,7 +117,6 @@ from lp.registry.vocabularies import ValidPersonOrTeamVocabulary |
496 | from lp.services.config import config |
497 | from lp.services.features import getFeatureFlag |
498 | from lp.services.job.interfaces.job import JobStatus |
499 | -from lp.services.librarian.browser import ProxiedLibraryFileAlias |
500 | from lp.services.propertycache import cachedproperty |
501 | from lp.services.webapp import LaunchpadView, canonical_url, urlappend |
502 | from lp.services.webapp.authorization import check_permission |
503 | @@ -673,6 +672,7 @@ class FileBugViewBase(LaunchpadFormView): |
504 | owner=self.user, |
505 | data=BytesIO(data["filecontent"]), |
506 | filename=filename, |
507 | + url=None, |
508 | description=file_description, |
509 | comment=attachment_comment, |
510 | is_patch=data["patch"], |
511 | @@ -686,6 +686,7 @@ class FileBugViewBase(LaunchpadFormView): |
512 | bug.linkAttachment( |
513 | owner=self.user, |
514 | file_alias=attachment["file_alias"], |
515 | + url=None, |
516 | description=attachment["description"], |
517 | comment=attachment_comment, |
518 | send_notifications=False, |
519 | @@ -1472,10 +1473,6 @@ class BugsPatchesView(LaunchpadView): |
520 | now = datetime.now(timezone("UTC")) |
521 | return now - patch.message.datecreated |
522 | |
523 | - def proxiedUrlForLibraryFile(self, patch): |
524 | - """Return the proxied download URL for a Librarian file.""" |
525 | - return ProxiedLibraryFileAlias(patch.libraryfile, patch).http_url |
526 | - |
527 | |
528 | class TargetSubscriptionView(LaunchpadView): |
529 | """A view to show all a person's structural subscriptions to a target.""" |
530 | diff --git a/lib/lp/bugs/browser/tests/bug-views.rst b/lib/lp/bugs/browser/tests/bug-views.rst |
531 | index 74a9fd7..008659a 100644 |
532 | --- a/lib/lp/bugs/browser/tests/bug-views.rst |
533 | +++ b/lib/lp/bugs/browser/tests/bug-views.rst |
534 | @@ -334,22 +334,22 @@ librarian file of the attachment. |
535 | ... ) |
536 | >>> view = BugView(bug_seven, request) |
537 | >>> for attachment in view.regular_attachments: |
538 | - ... print(attachment["attachment"].title) |
539 | + ... print(attachment.title) |
540 | ... |
541 | attachment 1 |
542 | attachment 2 |
543 | >>> for patch in view.patches: |
544 | - ... print(patch["attachment"].title) |
545 | + ... print(patch.title) |
546 | ... |
547 | patch 1 |
548 | patch 2 |
549 | >>> for attachment in view.regular_attachments: |
550 | - ... print(attachment["file"].http_url) |
551 | + ... print(attachment.displayed_url) |
552 | ... |
553 | http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a1 |
554 | http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a2 |
555 | >>> for patch in view.patches: |
556 | - ... print(patch["file"].http_url) |
557 | + ... print(patch.displayed_url) |
558 | ... |
559 | http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p1 |
560 | http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p2 |
561 | diff --git a/lib/lp/bugs/browser/tests/test_bug_views.py b/lib/lp/bugs/browser/tests/test_bug_views.py |
562 | index 2264af4..787b257 100644 |
563 | --- a/lib/lp/bugs/browser/tests/test_bug_views.py |
564 | +++ b/lib/lp/bugs/browser/tests/test_bug_views.py |
565 | @@ -779,6 +779,25 @@ class TestBugMessageAddFormView(TestCaseWithFactory): |
566 | ) |
567 | self.assertEqual(0, len(view.errors)) |
568 | |
569 | + def test_add_attachment_with_url(self): |
570 | + bug = self.factory.makeBug() |
571 | + form = { |
572 | + "field.comment": " ", |
573 | + "field.actions.save": "Post Comment", |
574 | + "field.attachment_url": "https://launchpad.net", |
575 | + "field.attachment_description": "description", |
576 | + "field.patch.used": "", |
577 | + } |
578 | + login_person(self.factory.makePerson()) |
579 | + view = create_initialized_view( |
580 | + bug.default_bugtask, "+addcomment", form=form |
581 | + ) |
582 | + self.assertEqual([], view.errors) |
583 | + attachments = list(bug.attachments) |
584 | + self.assertEqual(1, len(attachments)) |
585 | + self.assertEqual("description", attachments[0].title) |
586 | + self.assertEqual("https://launchpad.net", attachments[0].url) |
587 | + |
588 | |
589 | class TestBugMarkAsDuplicateView(TestCaseWithFactory): |
590 | """Tests for marking a bug as a duplicate.""" |
591 | diff --git a/lib/lp/bugs/browser/tests/test_bugview.py b/lib/lp/bugs/browser/tests/test_bugview.py |
592 | index 04d2e84..1124544 100644 |
593 | --- a/lib/lp/bugs/browser/tests/test_bugview.py |
594 | +++ b/lib/lp/bugs/browser/tests/test_bugview.py |
595 | @@ -35,10 +35,7 @@ class TestBugView(TestCaseWithFactory): |
596 | removeSecurityProxy(attachment.libraryfile).content = None |
597 | self.assertEqual( |
598 | ["regular attachment"], |
599 | - [ |
600 | - attachment["attachment"].title |
601 | - for attachment in self.view.regular_attachments |
602 | - ], |
603 | + [attachment.title for attachment in self.view.regular_attachments], |
604 | ) |
605 | |
606 | def test_patches_dont_include_invalid_records(self): |
607 | @@ -55,10 +52,7 @@ class TestBugView(TestCaseWithFactory): |
608 | removeSecurityProxy(patch.libraryfile).content = None |
609 | self.assertEqual( |
610 | ["patch"], |
611 | - [ |
612 | - attachment["attachment"].title |
613 | - for attachment in self.view.patches |
614 | - ], |
615 | + [attachment.title for attachment in self.view.patches], |
616 | ) |
617 | |
618 | |
619 | diff --git a/lib/lp/bugs/doc/bug-export.rst b/lib/lp/bugs/doc/bug-export.rst |
620 | index 3f466dd..afe9b09 100644 |
621 | --- a/lib/lp/bugs/doc/bug-export.rst |
622 | +++ b/lib/lp/bugs/doc/bug-export.rst |
623 | @@ -142,13 +142,23 @@ the file when we later serialise the bug: |
624 | >>> bug4 = getUtility(IBugSet).get(4) |
625 | >>> sampleperson = getUtility(IPersonSet).getByEmail("test@canonical.com") |
626 | >>> bug4.addAttachment( |
627 | - ... sampleperson, |
628 | - ... io.BytesIO(b"Hello World"), |
629 | - ... "Added attachment", |
630 | - ... "hello.txt", |
631 | + ... owner=sampleperson, |
632 | + ... data=io.BytesIO(b"Hello World"), |
633 | + ... comment="Added attachment", |
634 | + ... filename="hello.txt", |
635 | + ... url=None, |
636 | ... description='"Hello World" attachment', |
637 | ... ) |
638 | <lp.bugs.model.bugattachment.BugAttachment ...> |
639 | + >>> bug4.addAttachment( |
640 | + ... owner=sampleperson, |
641 | + ... data=None, |
642 | + ... comment="Added attachment with URL", |
643 | + ... filename=None, |
644 | + ... url="https://launchpad.net/", |
645 | + ... description=None, |
646 | + ... ) |
647 | + <lp.bugs.model.bugattachment.BugAttachment ...> |
648 | |
649 | >>> transaction.commit() |
650 | |
651 | @@ -166,13 +176,21 @@ attachment contents encoded using base-64: |
652 | <text>Added attachment</text> |
653 | <attachment href="http://bugs.launchpad.test/bugs/4/.../+files/hello.txt"> |
654 | <type>UNSPECIFIED</type> |
655 | - <filename>hello.txt</filename> |
656 | <title>"Hello World" attachment</title> |
657 | + <filename>hello.txt</filename> |
658 | <mimetype>text/plain</mimetype> |
659 | <contents>SGVsbG8gV29ybGQ= |
660 | </contents> |
661 | </attachment> |
662 | </comment> |
663 | + <comment> |
664 | + <sender name="name12">Sample Person</sender> |
665 | + <date>...</date> |
666 | + <text>Added attachment with URL</text> |
667 | + <attachment href="https://launchpad.net/"> |
668 | + <type>UNSPECIFIED</type> |
669 | + <title>https://launchpad.net/</title> |
670 | + </attachment> |
671 | ... |
672 | |
673 | |
674 | diff --git a/lib/lp/bugs/doc/bug.rst b/lib/lp/bugs/doc/bug.rst |
675 | index e28eff2..848028f 100644 |
676 | --- a/lib/lp/bugs/doc/bug.rst |
677 | +++ b/lib/lp/bugs/doc/bug.rst |
678 | @@ -742,6 +742,7 @@ Adding an attachment. |
679 | >>> attachment = attachmentset.create( |
680 | ... bug=firefox_bug, |
681 | ... filealias=filealias, |
682 | + ... url=None, |
683 | ... title="Some info.", |
684 | ... message=message, |
685 | ... ) |
686 | diff --git a/lib/lp/bugs/doc/bugattachments.rst b/lib/lp/bugs/doc/bugattachments.rst |
687 | index d2f99c8..58e16f1 100644 |
688 | --- a/lib/lp/bugs/doc/bugattachments.rst |
689 | +++ b/lib/lp/bugs/doc/bugattachments.rst |
690 | @@ -53,6 +53,7 @@ ObjectCreatedEvent in order to trigger email notifications: |
691 | ... owner=foobar, |
692 | ... data=data, |
693 | ... filename="foo.bar", |
694 | + ... url=None, |
695 | ... description="this fixes the bug", |
696 | ... comment=message, |
697 | ... is_patch=False, |
698 | @@ -77,6 +78,7 @@ passed in is often a file-like object, but can be bytes too. |
699 | ... owner=foobar, |
700 | ... data=data, |
701 | ... filename="foo.baz", |
702 | + ... url=None, |
703 | ... description="this fixes the bug", |
704 | ... comment="a string comment", |
705 | ... is_patch=False, |
706 | @@ -92,6 +94,7 @@ If no description is given, the title is set to the filename. |
707 | >>> screenshot = bug_four.addAttachment( |
708 | ... owner=foobar, |
709 | ... data=data, |
710 | + ... url=None, |
711 | ... filename="screenshot.jpg", |
712 | ... comment="a string comment", |
713 | ... is_patch=False, |
714 | @@ -110,6 +113,7 @@ The content type is guessed based on the information provided. |
715 | ... owner=foobar, |
716 | ... data=data, |
717 | ... filename="something.debdiff", |
718 | + ... url=None, |
719 | ... comment="something debdiffish", |
720 | ... is_patch=False, |
721 | ... ) |
722 | @@ -503,6 +507,7 @@ It's also possible to delete attachments. |
723 | ... owner=foobar, |
724 | ... data=data, |
725 | ... filename="foo.baz", |
726 | + ... url=None, |
727 | ... description="Attachment to be deleted", |
728 | ... comment="a string comment", |
729 | ... is_patch=False, |
730 | @@ -552,6 +557,7 @@ property returning True. |
731 | ... owner=foobar, |
732 | ... data=BytesIO(filecontent.getvalue()), |
733 | ... filename="foo.baz", |
734 | + ... url=None, |
735 | ... description="A non-patch attachment", |
736 | ... comment="a string comment", |
737 | ... is_patch=False, |
738 | @@ -564,6 +570,7 @@ property returning True. |
739 | ... owner=foobar, |
740 | ... data=BytesIO(filecontent.getvalue()), |
741 | ... filename="foo.baz", |
742 | + ... url=None, |
743 | ... description="A patch attachment", |
744 | ... comment="a string comment", |
745 | ... is_patch=True, |
746 | @@ -601,7 +608,10 @@ LibraryFileAlias.restricted and Bug.private. See also the section |
747 | |
748 | >>> bug = factory.makeBug() |
749 | >>> bug.linkAttachment( |
750 | - ... owner=bug.owner, file_alias=file_alias, comment="Some attachment" |
751 | + ... owner=bug.owner, |
752 | + ... file_alias=file_alias, |
753 | + ... url=None, |
754 | + ... comment="Some attachment", |
755 | ... ) |
756 | <lp.bugs.model.bugattachment.BugAttachment ...> |
757 | |
758 | @@ -631,6 +641,7 @@ meaningful description. |
759 | >>> bug.linkAttachment( |
760 | ... owner=bug.owner, |
761 | ... file_alias=file_alias, |
762 | + ... url=None, |
763 | ... comment="Some attachment", |
764 | ... is_patch=True, |
765 | ... description="An attachment of some sort", |
766 | @@ -688,6 +699,7 @@ its Librarian file is set. |
767 | ... owner=private_bug_owner, |
768 | ... data=b"secret", |
769 | ... filename="baz.txt", |
770 | + ... url=None, |
771 | ... comment="Some attachment", |
772 | ... ) |
773 | >>> private_attachment.libraryfile.restricted |
774 | diff --git a/lib/lp/bugs/doc/bugcomment.rst b/lib/lp/bugs/doc/bugcomment.rst |
775 | index 14a8b71..5157e55 100644 |
776 | --- a/lib/lp/bugs/doc/bugcomment.rst |
777 | +++ b/lib/lp/bugs/doc/bugcomment.rst |
778 | @@ -77,6 +77,7 @@ in activity_and_comments... |
779 | ... data=BytesIO(b"whatever"), |
780 | ... comment=bug_11.initial_message, |
781 | ... filename="test.txt", |
782 | + ... url=None, |
783 | ... is_patch=False, |
784 | ... content_type="text/plain", |
785 | ... description="sample data", |
786 | @@ -235,6 +236,7 @@ attachments to a bug to see this in action: |
787 | ... data=file_, |
788 | ... description="Ho", |
789 | ... filename="munchy", |
790 | + ... url=None, |
791 | ... comment="Hello there", |
792 | ... ) |
793 | >>> m6 = bug_three.newMessage( |
794 | @@ -436,6 +438,7 @@ not included in BugComment.patches. |
795 | ... data=BytesIO(b"whatever"), |
796 | ... comment=bug.initial_message, |
797 | ... filename="file1", |
798 | + ... url=None, |
799 | ... is_patch=False, |
800 | ... content_type="text/plain", |
801 | ... description="sample data 1", |
802 | @@ -445,6 +448,7 @@ not included in BugComment.patches. |
803 | ... data=BytesIO(b"whatever"), |
804 | ... comment=bug.initial_message, |
805 | ... filename="file2", |
806 | + ... url=None, |
807 | ... is_patch=False, |
808 | ... content_type="text/plain", |
809 | ... description="sample data 2", |
810 | @@ -454,6 +458,7 @@ not included in BugComment.patches. |
811 | ... data=BytesIO(b"whatever"), |
812 | ... comment=bug.initial_message, |
813 | ... filename="patch1", |
814 | + ... url=None, |
815 | ... is_patch=True, |
816 | ... content_type="text/plain", |
817 | ... description="patch 1", |
818 | @@ -463,6 +468,7 @@ not included in BugComment.patches. |
819 | ... data=BytesIO(b"whatever"), |
820 | ... comment=bug.initial_message, |
821 | ... filename="patch2", |
822 | + ... url=None, |
823 | ... is_patch=True, |
824 | ... content_type="text/plain", |
825 | ... description="patch 2", |
826 | diff --git a/lib/lp/bugs/interfaces/bug.py b/lib/lp/bugs/interfaces/bug.py |
827 | index 9697660..e02f2ff 100644 |
828 | --- a/lib/lp/bugs/interfaces/bug.py |
829 | +++ b/lib/lp/bugs/interfaces/bug.py |
830 | @@ -43,6 +43,7 @@ from lazr.restful.interface import copy_field |
831 | from zope.component import getUtility |
832 | from zope.interface import Attribute, Interface |
833 | from zope.schema import ( |
834 | + URI, |
835 | Bool, |
836 | Bytes, |
837 | Choice, |
838 | @@ -877,11 +878,12 @@ class IBugAppend(Interface): |
839 | |
840 | @call_with(owner=REQUEST_USER, from_api=True) |
841 | @operation_parameters( |
842 | - data=Bytes(constraint=attachment_size_constraint), |
843 | + data=Bytes(constraint=attachment_size_constraint, required=False), |
844 | comment=Text(), |
845 | - filename=TextLine(), |
846 | + filename=TextLine(required=False), |
847 | + url=URI(required=False), |
848 | is_patch=Bool(), |
849 | - content_type=TextLine(), |
850 | + content_type=TextLine(required=False), |
851 | description=Text(), |
852 | ) |
853 | @export_factory_operation(IBugAttachment, []) |
854 | @@ -891,6 +893,7 @@ class IBugAppend(Interface): |
855 | data, |
856 | comment, |
857 | filename, |
858 | + url, |
859 | is_patch=False, |
860 | content_type=None, |
861 | description=None, |
862 | @@ -903,6 +906,7 @@ class IBugAppend(Interface): |
863 | :description: A brief description of the attachment. |
864 | :comment: An IMessage or string. |
865 | :filename: A string. |
866 | + :url: External URL of the attachment |
867 | :is_patch: A boolean. |
868 | """ |
869 | |
870 | @@ -956,12 +960,13 @@ class IBugAppend(Interface): |
871 | """ |
872 | |
873 | def linkAttachment( |
874 | - owner, file_alias, comment, is_patch=False, description=None |
875 | + owner, file_alias, url, comment, is_patch=False, description=None |
876 | ): |
877 | """Link an `ILibraryFileAlias` to this bug. |
878 | |
879 | :owner: An IPerson. |
880 | :file_alias: The `ILibraryFileAlias` to link to this bug. |
881 | + :url: External URL of the attachment. |
882 | :description: A brief description of the attachment. |
883 | :comment: An IMessage or string. |
884 | :is_patch: A boolean. |
885 | diff --git a/lib/lp/bugs/interfaces/bugattachment.py b/lib/lp/bugs/interfaces/bugattachment.py |
886 | index 0ded472..4edb5de 100644 |
887 | --- a/lib/lp/bugs/interfaces/bugattachment.py |
888 | +++ b/lib/lp/bugs/interfaces/bugattachment.py |
889 | @@ -22,7 +22,7 @@ from lazr.restful.declarations import ( |
890 | ) |
891 | from lazr.restful.fields import Reference |
892 | from zope.interface import Interface |
893 | -from zope.schema import Bool, Bytes, Choice, Int, TextLine |
894 | +from zope.schema import URI, Bool, Bytes, Choice, Int, TextLine |
895 | |
896 | from lp import _ |
897 | from lp.bugs.interfaces.hasbug import IHasBug |
898 | @@ -90,10 +90,13 @@ class IBugAttachmentView(IHasBug): |
899 | ) |
900 | libraryfile = exported( |
901 | Bytes( |
902 | - title=_("The attachment content."), required=True, readonly=True |
903 | + title=_("The attachment content."), required=False, readonly=True |
904 | ), |
905 | exported_as="data", |
906 | ) |
907 | + url = exported( |
908 | + URI(title=_("Attachment URL"), required=False, readonly=True) |
909 | + ) |
910 | _message_id = Int(title=_("Message ID")) |
911 | message = exported( |
912 | Reference( |
913 | @@ -109,6 +112,12 @@ class IBugAttachmentView(IHasBug): |
914 | description=_("Is this attachment a patch?"), |
915 | readonly=True, |
916 | ) |
917 | + displayed_url = URI( |
918 | + title=_( |
919 | + "Download URL of the files or the external URL of the attachment" |
920 | + ), |
921 | + readonly=True, |
922 | + ) |
923 | |
924 | def getFileByName(filename): |
925 | """Return the `ILibraryFileAlias` for the given file name. |
926 | @@ -176,6 +185,7 @@ class IBugAttachmentSet(Interface): |
927 | def create( |
928 | bug, |
929 | filealias, |
930 | + url, |
931 | title, |
932 | message, |
933 | type=IBugAttachment["type"].default, |
934 | @@ -184,7 +194,9 @@ class IBugAttachmentSet(Interface): |
935 | """Create a new attachment and return it. |
936 | |
937 | :param bug: The `IBug` to which the new attachment belongs. |
938 | - :param filealias: The `IFilealias` containing the data. |
939 | + :param filealias: The `ILibraryFileAlias` containing |
940 | + the data (optional). |
941 | + :param url: External URL of the attachment (optional). |
942 | :param message: The `IMessage` to which this attachment belongs. |
943 | :param type: The type of attachment. See `BugAttachmentType`. |
944 | :param send_notifications: If True, a notification is sent to |
945 | diff --git a/lib/lp/bugs/interfaces/bugmessage.py b/lib/lp/bugs/interfaces/bugmessage.py |
946 | index 7768b07..22c8d39 100644 |
947 | --- a/lib/lp/bugs/interfaces/bugmessage.py |
948 | +++ b/lib/lp/bugs/interfaces/bugmessage.py |
949 | @@ -11,7 +11,7 @@ __all__ = [ |
950 | ] |
951 | |
952 | from zope.interface import Attribute, Interface |
953 | -from zope.schema import Bool, Bytes, Int, Object, Text, TextLine |
954 | +from zope.schema import URI, Bool, Bytes, Int, Object, Text, TextLine |
955 | |
956 | from lp.app.validators.attachment import attachment_size_constraint |
957 | from lp.bugs.interfaces.bug import IBug |
958 | @@ -103,6 +103,7 @@ class IBugMessageAddForm(Interface): |
959 | required=False, |
960 | constraint=attachment_size_constraint, |
961 | ) |
962 | + attachment_url = URI(title="URL", required=False) |
963 | patch = Bool( |
964 | title="This attachment contains a solution (patch) for this bug", |
965 | required=False, |
966 | diff --git a/lib/lp/bugs/mail/handler.py b/lib/lp/bugs/mail/handler.py |
967 | index 975af40..775b570 100644 |
968 | --- a/lib/lp/bugs/mail/handler.py |
969 | +++ b/lib/lp/bugs/mail/handler.py |
970 | @@ -410,6 +410,7 @@ class MaloneHandler: |
971 | getUtility(IBugAttachmentSet).create( |
972 | bug=bug, |
973 | filealias=blob, |
974 | + url=None, |
975 | attach_type=attach_type, |
976 | title=blob.filename, |
977 | message=message, |
978 | diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py |
979 | index 5b1465c..88f793d 100644 |
980 | --- a/lib/lp/bugs/model/bug.py |
981 | +++ b/lib/lp/bugs/model/bug.py |
982 | @@ -1608,6 +1608,7 @@ class Bug(SQLBase, InformationTypeMixin): |
983 | data, |
984 | comment, |
985 | filename, |
986 | + url, |
987 | is_patch=False, |
988 | content_type=None, |
989 | description=None, |
990 | @@ -1619,35 +1620,45 @@ class Bug(SQLBase, InformationTypeMixin): |
991 | # wrongly encoded. |
992 | if from_api: |
993 | data = get_raw_form_value_from_current_request(data, "data") |
994 | - if isinstance(data, bytes): |
995 | - filecontent = data |
996 | - else: |
997 | - filecontent = data.read() |
998 | |
999 | - if is_patch: |
1000 | - content_type = "text/plain" |
1001 | - else: |
1002 | - if content_type is None: |
1003 | - content_type, encoding = guess_content_type( |
1004 | - name=filename, body=filecontent |
1005 | - ) |
1006 | + if data: |
1007 | + if isinstance(data, bytes): |
1008 | + filecontent = data |
1009 | + else: |
1010 | + filecontent = data.read() |
1011 | |
1012 | - filealias = getUtility(ILibraryFileAliasSet).create( |
1013 | - name=filename, |
1014 | - size=len(filecontent), |
1015 | - file=BytesIO(filecontent), |
1016 | - contentType=content_type, |
1017 | - restricted=self.private, |
1018 | - ) |
1019 | + if is_patch: |
1020 | + content_type = "text/plain" |
1021 | + else: |
1022 | + if content_type is None: |
1023 | + content_type, encoding = guess_content_type( |
1024 | + name=filename, body=filecontent |
1025 | + ) |
1026 | + |
1027 | + file_alias = getUtility(ILibraryFileAliasSet).create( |
1028 | + name=filename, |
1029 | + size=len(filecontent), |
1030 | + file=BytesIO(filecontent), |
1031 | + contentType=content_type, |
1032 | + restricted=self.private, |
1033 | + ) |
1034 | + else: |
1035 | + file_alias = None |
1036 | |
1037 | return self.linkAttachment( |
1038 | - owner, filealias, comment, is_patch, description |
1039 | + owner=owner, |
1040 | + file_alias=file_alias, |
1041 | + url=url, |
1042 | + comment=comment, |
1043 | + is_patch=is_patch, |
1044 | + description=description, |
1045 | ) |
1046 | |
1047 | def linkAttachment( |
1048 | self, |
1049 | owner, |
1050 | file_alias, |
1051 | + url, |
1052 | comment, |
1053 | is_patch=False, |
1054 | description=None, |
1055 | @@ -1670,11 +1681,6 @@ class Bug(SQLBase, InformationTypeMixin): |
1056 | else: |
1057 | attach_type = BugAttachmentType.UNSPECIFIED |
1058 | |
1059 | - if description: |
1060 | - title = description |
1061 | - else: |
1062 | - title = file_alias.filename |
1063 | - |
1064 | if IMessage.providedBy(comment): |
1065 | message = comment |
1066 | else: |
1067 | @@ -1685,8 +1691,9 @@ class Bug(SQLBase, InformationTypeMixin): |
1068 | return getUtility(IBugAttachmentSet).create( |
1069 | bug=self, |
1070 | filealias=file_alias, |
1071 | + url=url, |
1072 | attach_type=attach_type, |
1073 | - title=title, |
1074 | + title=description, |
1075 | message=message, |
1076 | send_notifications=send_notifications, |
1077 | ) |
1078 | @@ -2206,9 +2213,10 @@ class Bug(SQLBase, InformationTypeMixin): |
1079 | # XXX: This should be a bulk update. RBC 20100827 |
1080 | # bug=https://bugs.launchpad.net/storm/+bug/625071 |
1081 | for attachment in self.attachments_unpopulated: |
1082 | - attachment.libraryfile.restricted = ( |
1083 | - information_type in PRIVATE_INFORMATION_TYPES |
1084 | - ) |
1085 | + if attachment.libraryfile: |
1086 | + attachment.libraryfile.restricted = ( |
1087 | + information_type in PRIVATE_INFORMATION_TYPES |
1088 | + ) |
1089 | |
1090 | self.information_type = information_type |
1091 | self._reconcileAccess() |
1092 | @@ -2558,16 +2566,34 @@ class Bug(SQLBase, InformationTypeMixin): |
1093 | |
1094 | def _attachments_query(self): |
1095 | """Helper for the attachments* properties.""" |
1096 | - # bug attachments with no LibraryFileContent have been deleted - the |
1097 | - # garbo_daily run will remove the LibraryFileAlias asynchronously. |
1098 | - # See bug 542274 for more details. |
1099 | + # get bug attachments that have either a URL, |
1100 | + # or associated LibraryFileContent |
1101 | + # bug attachments with LibraryFileAlias and no LibraryFileContent have |
1102 | + # been deleted - the garbo_daily run will remove the LibraryFileAlias |
1103 | + # asynchronously. See bug 542274 for more details. |
1104 | store = Store.of(self) |
1105 | - return store.find( |
1106 | - (BugAttachment, LibraryFileAlias, LibraryFileContent), |
1107 | - BugAttachment.bug == self, |
1108 | - BugAttachment.libraryfile == LibraryFileAlias.id, |
1109 | - LibraryFileContent.id == LibraryFileAlias.contentID, |
1110 | - ).order_by(BugAttachment.id) |
1111 | + return ( |
1112 | + store.using( |
1113 | + BugAttachment, |
1114 | + LeftJoin( |
1115 | + LibraryFileAlias, |
1116 | + BugAttachment.libraryfile == LibraryFileAlias.id, |
1117 | + ), |
1118 | + LeftJoin( |
1119 | + LibraryFileContent, |
1120 | + LibraryFileContent.id == LibraryFileAlias.contentID, |
1121 | + ), |
1122 | + ) |
1123 | + .find( |
1124 | + (BugAttachment, LibraryFileAlias, LibraryFileContent), |
1125 | + BugAttachment.bug == self, |
1126 | + Or( |
1127 | + BugAttachment.url != None, |
1128 | + LibraryFileAlias.contentID != None, |
1129 | + ), |
1130 | + ) |
1131 | + .order_by(BugAttachment.id) |
1132 | + ) |
1133 | |
1134 | @property |
1135 | def attachments(self): |
1136 | diff --git a/lib/lp/bugs/model/bugattachment.py b/lib/lp/bugs/model/bugattachment.py |
1137 | index 180c353..064482a 100644 |
1138 | --- a/lib/lp/bugs/model/bugattachment.py |
1139 | +++ b/lib/lp/bugs/model/bugattachment.py |
1140 | @@ -17,6 +17,7 @@ from lp.bugs.interfaces.bugattachment import ( |
1141 | from lp.services.database.enumcol import DBEnum |
1142 | from lp.services.database.interfaces import IStore |
1143 | from lp.services.database.stormbase import StormBase |
1144 | +from lp.services.librarian.browser import ProxiedLibraryFileAlias |
1145 | from lp.services.propertycache import cachedproperty |
1146 | |
1147 | |
1148 | @@ -35,9 +36,10 @@ class BugAttachment(StormBase): |
1149 | allow_none=False, |
1150 | default=IBugAttachment["type"].default, |
1151 | ) |
1152 | - title = Unicode(allow_none=False) |
1153 | - libraryfile_id = Int(name="libraryfile", allow_none=False) |
1154 | + _title = Unicode(name="title", allow_none=True) |
1155 | + libraryfile_id = Int(name="libraryfile", allow_none=True) |
1156 | libraryfile = Reference(libraryfile_id, "LibraryFileAlias.id") |
1157 | + url = Unicode(allow_none=True) |
1158 | _message_id = Int(name="message", allow_none=False) |
1159 | _message = Reference(_message_id, "Message.id") |
1160 | |
1161 | @@ -46,6 +48,7 @@ class BugAttachment(StormBase): |
1162 | bug, |
1163 | title, |
1164 | libraryfile, |
1165 | + url, |
1166 | message, |
1167 | type=IBugAttachment["type"].default, |
1168 | ): |
1169 | @@ -53,9 +56,22 @@ class BugAttachment(StormBase): |
1170 | self.bug = bug |
1171 | self.title = title |
1172 | self.libraryfile = libraryfile |
1173 | + self.url = url |
1174 | self._message = message |
1175 | self.type = type |
1176 | |
1177 | + @property |
1178 | + def title(self) -> str: |
1179 | + if self._title: |
1180 | + return self._title |
1181 | + if self.libraryfile: |
1182 | + return self.libraryfile.filename |
1183 | + return self.url |
1184 | + |
1185 | + @title.setter |
1186 | + def title(self, title) -> None: |
1187 | + self._title = title |
1188 | + |
1189 | @cachedproperty |
1190 | def message(self): |
1191 | """This is a cachedproperty to allow message to be an IIndexedMessage. |
1192 | @@ -81,15 +97,23 @@ class BugAttachment(StormBase): |
1193 | # Delete the reference to the LibraryFileContent record right now, |
1194 | # in order to avoid problems with not deleted files as described |
1195 | # in bug 387188. |
1196 | - self.libraryfile.content = None |
1197 | + if self.libraryfile: |
1198 | + self.libraryfile.content = None |
1199 | Store.of(self).remove(self) |
1200 | |
1201 | def getFileByName(self, filename): |
1202 | """See IBugAttachment.""" |
1203 | - if filename == self.libraryfile.filename: |
1204 | + if self.libraryfile and filename == self.libraryfile.filename: |
1205 | return self.libraryfile |
1206 | raise NotFoundError(filename) |
1207 | |
1208 | + @property |
1209 | + def displayed_url(self): |
1210 | + return ( |
1211 | + self.url |
1212 | + or ProxiedLibraryFileAlias(self.libraryfile, self).http_url |
1213 | + ) |
1214 | + |
1215 | |
1216 | @implementer(IBugAttachmentSet) |
1217 | class BugAttachmentSet: |
1218 | @@ -110,18 +134,26 @@ class BugAttachmentSet: |
1219 | self, |
1220 | bug, |
1221 | filealias, |
1222 | + url, |
1223 | title, |
1224 | message, |
1225 | attach_type=None, |
1226 | send_notifications=False, |
1227 | ): |
1228 | """See `IBugAttachmentSet`.""" |
1229 | + if not filealias and not url: |
1230 | + raise ValueError("Either filealias or url must be provided") |
1231 | + |
1232 | + if filealias and url: |
1233 | + raise ValueError("Only one of filealias or url may be provided") |
1234 | + |
1235 | if attach_type is None: |
1236 | # XXX kiko 2005-08-03 bug=1659: this should use DEFAULT. |
1237 | attach_type = IBugAttachment["type"].default |
1238 | attachment = BugAttachment( |
1239 | bug=bug, |
1240 | libraryfile=filealias, |
1241 | + url=url, |
1242 | type=attach_type, |
1243 | title=title, |
1244 | message=message, |
1245 | diff --git a/lib/lp/bugs/model/tests/test_bugtasksearch.py b/lib/lp/bugs/model/tests/test_bugtasksearch.py |
1246 | index db52d71..05793e0 100644 |
1247 | --- a/lib/lp/bugs/model/tests/test_bugtasksearch.py |
1248 | +++ b/lib/lp/bugs/model/tests/test_bugtasksearch.py |
1249 | @@ -268,6 +268,7 @@ class OnceTests: |
1250 | data=b"filedata", |
1251 | comment="a comment", |
1252 | filename="file1.txt", |
1253 | + url=None, |
1254 | is_patch=False, |
1255 | ) |
1256 | self.bugtasks[1].bug.addAttachment( |
1257 | @@ -275,6 +276,7 @@ class OnceTests: |
1258 | data=b"filedata", |
1259 | comment="a comment", |
1260 | filename="file1.txt", |
1261 | + url=None, |
1262 | is_patch=True, |
1263 | ) |
1264 | # We can search for bugs with non-patch attachments... |
1265 | diff --git a/lib/lp/bugs/scripts/bugexport.py b/lib/lp/bugs/scripts/bugexport.py |
1266 | index 3d3327a..c6c0c43 100644 |
1267 | --- a/lib/lp/bugs/scripts/bugexport.py |
1268 | +++ b/lib/lp/bugs/scripts/bugexport.py |
1269 | @@ -16,7 +16,6 @@ from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
1270 | from lp.bugs.browser.bugtask import get_comments_for_bugtask |
1271 | from lp.bugs.interfaces.bugtask import IBugTaskSet |
1272 | from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams |
1273 | -from lp.services.librarian.browser import ProxiedLibraryFileAlias |
1274 | |
1275 | BUGS_XMLNS = "https://launchpad.net/xmlns/2006/bugs" |
1276 | |
1277 | @@ -85,27 +84,30 @@ def serialise_bugtask(bugtask): |
1278 | attachment_node = ET.SubElement( |
1279 | comment_node, |
1280 | "attachment", |
1281 | - href=ProxiedLibraryFileAlias( |
1282 | - attachment.libraryfile, attachment |
1283 | - ).http_url, |
1284 | + href=attachment.displayed_url, |
1285 | ) |
1286 | attachment_node.text = attachment_node.tail = "\n" |
1287 | addnode(attachment_node, "type", attachment.type.name) |
1288 | - addnode( |
1289 | - attachment_node, "filename", attachment.libraryfile.filename |
1290 | - ) |
1291 | addnode(attachment_node, "title", attachment.title) |
1292 | - addnode( |
1293 | - attachment_node, "mimetype", attachment.libraryfile.mimetype |
1294 | - ) |
1295 | - # Attach the attachment file contents, base 64 encoded. |
1296 | - addnode( |
1297 | - attachment_node, |
1298 | - "contents", |
1299 | - base64.encodebytes(attachment.libraryfile.read()).decode( |
1300 | - "ASCII" |
1301 | - ), |
1302 | - ) |
1303 | + if attachment.libraryfile: |
1304 | + addnode( |
1305 | + attachment_node, |
1306 | + "filename", |
1307 | + attachment.libraryfile.filename, |
1308 | + ) |
1309 | + addnode( |
1310 | + attachment_node, |
1311 | + "mimetype", |
1312 | + attachment.libraryfile.mimetype, |
1313 | + ) |
1314 | + # Attach the attachment file contents, base 64 encoded. |
1315 | + addnode( |
1316 | + attachment_node, |
1317 | + "contents", |
1318 | + base64.encodebytes(attachment.libraryfile.read()).decode( |
1319 | + "ASCII" |
1320 | + ), |
1321 | + ) |
1322 | |
1323 | return bug_node |
1324 | |
1325 | diff --git a/lib/lp/bugs/scripts/bugimport.py b/lib/lp/bugs/scripts/bugimport.py |
1326 | index 323c9b1..38306fb 100644 |
1327 | --- a/lib/lp/bugs/scripts/bugimport.py |
1328 | +++ b/lib/lp/bugs/scripts/bugimport.py |
1329 | @@ -445,6 +445,7 @@ class BugImporter: |
1330 | getUtility(IBugAttachmentSet).create( |
1331 | bug=bug, |
1332 | filealias=filealias, |
1333 | + url=None, |
1334 | attach_type=attach_type, |
1335 | title=title, |
1336 | message=message, |
1337 | diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py |
1338 | index f9aa65d..80fee9c 100644 |
1339 | --- a/lib/lp/bugs/scripts/tests/test_bugnotification.py |
1340 | +++ b/lib/lp/bugs/scripts/tests/test_bugnotification.py |
1341 | @@ -1026,7 +1026,7 @@ class TestEmailNotificationsAttachments( |
1342 | # another five minutes. Therefore, we send out separate |
1343 | # change notifications. |
1344 | return self.bug.addAttachment( |
1345 | - self.person, b"content", "a comment", "stuff.txt" |
1346 | + self.person, b"content", "a comment", "stuff.txt", url=None |
1347 | ) |
1348 | |
1349 | old = cachedproperty("old")(_attachment) |
1350 | diff --git a/lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.rst b/lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.rst |
1351 | index 090ca83..9df6720 100644 |
1352 | --- a/lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.rst |
1353 | +++ b/lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.rst |
1354 | @@ -35,6 +35,7 @@ If we add an attachment to the bug report... |
1355 | ... data=BytesIO(b"whatever"), |
1356 | ... comment=bug_11.initial_message, |
1357 | ... filename="test.txt", |
1358 | + ... url=None, |
1359 | ... is_patch=False, |
1360 | ... content_type="text/plain", |
1361 | ... description="sample data", |
1362 | @@ -74,6 +75,7 @@ Patch attachments are shown before non-patch attachments |
1363 | ... data=BytesIO(b"patchy patch patch"), |
1364 | ... comment=bug_11.initial_message, |
1365 | ... filename="patch.txt", |
1366 | + ... url=None, |
1367 | ... is_patch=True, |
1368 | ... content_type="text/plain", |
1369 | ... description="a patch", |
1370 | diff --git a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst |
1371 | index 93e9cc6..bfa0b6e 100644 |
1372 | --- a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst |
1373 | +++ b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst |
1374 | @@ -23,6 +23,7 @@ We'll start by adding some attachments to the bug: |
1375 | ... content, |
1376 | ... "comment for file a", |
1377 | ... "file_a.txt", |
1378 | + ... url=None, |
1379 | ... content_type="text/html", |
1380 | ... ) |
1381 | >>> content = BytesIO(b"do we need to") |
1382 | @@ -31,6 +32,7 @@ We'll start by adding some attachments to the bug: |
1383 | ... content, |
1384 | ... "comment for file with space", |
1385 | ... "file with space.txt", |
1386 | + ... url=None, |
1387 | ... content_type='text/plain;\n name="file with space.txt"', |
1388 | ... ) |
1389 | >>> content = BytesIO(b"Yes we can!") |
1390 | @@ -39,6 +41,7 @@ We'll start by adding some attachments to the bug: |
1391 | ... content, |
1392 | ... "comment for patch", |
1393 | ... "bug-patch.diff", |
1394 | + ... url=None, |
1395 | ... is_patch=True, |
1396 | ... content_type="text/plain", |
1397 | ... description="a patch", |
1398 | diff --git a/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst b/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst |
1399 | index 12f5e92..bed6dba 100644 |
1400 | --- a/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst |
1401 | +++ b/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst |
1402 | @@ -311,6 +311,7 @@ Patches also appear as badges in bug listings. |
1403 | ... owner=foobar, |
1404 | ... data=BytesIO(b"file data"), |
1405 | ... filename="foo.bar", |
1406 | + ... url=None, |
1407 | ... description="this fixes the bug", |
1408 | ... comment=message, |
1409 | ... is_patch=True, |
1410 | diff --git a/lib/lp/bugs/stories/patches-view/patches-view.rst b/lib/lp/bugs/stories/patches-view/patches-view.rst |
1411 | index 8a5d5a7..786a75d 100644 |
1412 | --- a/lib/lp/bugs/stories/patches-view/patches-view.rst |
1413 | +++ b/lib/lp/bugs/stories/patches-view/patches-view.rst |
1414 | @@ -101,7 +101,7 @@ patches view. |
1415 | Bug Importance Status Patch Age |
1416 | Bug #...: bug_a title Undecided New ...second... |
1417 | From: Patchy Person |
1418 | - Link: patch_a.diff description of patch a |
1419 | + Link: description of patch a |
1420 | |
1421 | The page title and other wording in the page reflects the contents. |
1422 | |
1423 | @@ -205,13 +205,13 @@ attachments, and various statuses... |
1424 | Bug Importance Status Patch Age |
1425 | Bug #...: bug_c title Wishlist Fix Committed ...second... |
1426 | From: Patchy Person |
1427 | - Link: patch_f.diff description of patch f |
1428 | + Link: description of patch f |
1429 | Bug #...: bug_b title Critical Confirmed ...second... |
1430 | From: Patchy Person |
1431 | - Link: patch_c.diff description of patch c |
1432 | + Link: description of patch c |
1433 | Bug #...: bug_a title Undecided New ...second... |
1434 | From: Patchy Person |
1435 | - Link: patch_a.diff description of patch a |
1436 | + Link: description of patch a |
1437 | |
1438 | The default sort order is by patch age. We can sort patches by |
1439 | importance and status. |
1440 | @@ -224,13 +224,13 @@ importance and status. |
1441 | Bug Importance Status Patch Age |
1442 | Bug #...: bug_b title Critical Confirmed ...second... |
1443 | From: Patchy Person |
1444 | - Link: patch_c.diff description of patch c |
1445 | + Link: description of patch c |
1446 | Bug #...: bug_c title Wishlist Fix Committed ...second... |
1447 | From: Patchy Person |
1448 | - Link: patch_f.diff description of patch f |
1449 | + Link: description of patch f |
1450 | Bug #...: bug_a title Undecided New ...second... |
1451 | From: Patchy Person |
1452 | - Link: patch_a.diff description of patch a |
1453 | + Link: description of patch a |
1454 | |
1455 | >>> anon_browser.getControl(name="orderby").value = ["status"] |
1456 | >>> anon_browser.getControl("sort").click() |
1457 | @@ -240,13 +240,13 @@ importance and status. |
1458 | Bug Importance Status Patch Age |
1459 | Bug #...: bug_a title Undecided New ...second... |
1460 | From: Patchy Person |
1461 | - Link: patch_a.diff description of patch a |
1462 | + Link: description of patch a |
1463 | Bug #...: bug_b title Critical Confirmed ...second... |
1464 | From: Patchy Person |
1465 | - Link: patch_c.diff description of patch c |
1466 | + Link: description of patch c |
1467 | Bug #...: bug_c title Wishlist Fix Committed ...second... |
1468 | From: Patchy Person |
1469 | - Link: patch_f.diff description of patch f |
1470 | + Link: description of patch f |
1471 | |
1472 | But we can't sort by things that aren't validated by the view. |
1473 | |
1474 | @@ -327,12 +327,10 @@ Bugs in a product series show up in the patches view for that series. |
1475 | Bug Importance Status Patch Age |
1476 | Bug #...: bug_c title Wishlist Fix Committed ...second... |
1477 | From: Patchy Person |
1478 | - Link: patch_f.diff |
1479 | - description of patch f |
1480 | + Link: description of patch f |
1481 | Bug #...: bug_a title Undecided New ...second... |
1482 | From: Patchy Person |
1483 | - Link: patch_a.diff |
1484 | - description of patch a |
1485 | + Link: description of patch a |
1486 | |
1487 | The page title and other wording in the page reflects the contents. |
1488 | |
1489 | diff --git a/lib/lp/bugs/stories/webservice/xx-bug.rst b/lib/lp/bugs/stories/webservice/xx-bug.rst |
1490 | index 80c2f24..8266254 100644 |
1491 | --- a/lib/lp/bugs/stories/webservice/xx-bug.rst |
1492 | +++ b/lib/lp/bugs/stories/webservice/xx-bug.rst |
1493 | @@ -1531,6 +1531,7 @@ An attachment can be added to the bug: |
1494 | ... "addAttachment", |
1495 | ... data=io.BytesIO(b"12345"), |
1496 | ... filename="numbers.txt", |
1497 | + ... url=None, |
1498 | ... content_type="foo/bar", |
1499 | ... comment="The numbers you asked for.", |
1500 | ... ) |
1501 | @@ -1558,6 +1559,7 @@ Now, bug 1 has one attachment: |
1502 | self_link: 'http://.../bugs/1/+attachment/...' |
1503 | title: 'numbers.txt' |
1504 | type: 'Unspecified' |
1505 | + url: None |
1506 | web_link: 'http://bugs.../bugs/1/+attachment/...' |
1507 | --- |
1508 | |
1509 | @@ -1572,6 +1574,7 @@ The attachment can be fetched directly: |
1510 | self_link: 'http://.../bugs/1/+attachment/...' |
1511 | title: 'numbers.txt' |
1512 | type: 'Unspecified' |
1513 | + url: None |
1514 | web_link: 'http://bugs.../bugs/1/+attachment/...' |
1515 | |
1516 | Fetching the data actually yields a redirect to the Librarian, which |
1517 | diff --git a/lib/lp/bugs/templates/bug-attachment-macros.pt b/lib/lp/bugs/templates/bug-attachment-macros.pt |
1518 | index a06b835..d91d0c3 100644 |
1519 | --- a/lib/lp/bugs/templates/bug-attachment-macros.pt |
1520 | +++ b/lib/lp/bugs/templates/bug-attachment-macros.pt |
1521 | @@ -15,6 +15,12 @@ |
1522 | <metal:widget metal:use-macro="context/@@launchpad_form/widget_row" /> |
1523 | </tal:filecontent> |
1524 | |
1525 | + <tal:url |
1526 | + tal:define="widget nocall:view/widgets/attachment_url|nothing" |
1527 | + tal:condition="widget"> |
1528 | + <metal:widget metal:use-macro="context/@@launchpad_form/widget_row" /> |
1529 | + </tal:url> |
1530 | + |
1531 | <tal:patch |
1532 | tal:define="widget nocall:view/widgets/patch|nothing" |
1533 | tal:condition="widget"> |
1534 | diff --git a/lib/lp/bugs/templates/bug-portlet-attachments.pt b/lib/lp/bugs/templates/bug-portlet-attachments.pt |
1535 | index 5a79a9f..399ceff 100644 |
1536 | --- a/lib/lp/bugs/templates/bug-portlet-attachments.pt |
1537 | +++ b/lib/lp/bugs/templates/bug-portlet-attachments.pt |
1538 | @@ -8,14 +8,10 @@ |
1539 | <ul> |
1540 | <li class="download-attachment" |
1541 | tal:repeat="attachment view/patches"> |
1542 | - <a tal:attributes="href attachment/file/http_url" |
1543 | - tal:content="attachment/attachment/title" |
1544 | - class="sprite haspatch-icon"> |
1545 | - Attachment Title |
1546 | - </a> |
1547 | - <small> |
1548 | - (<a tal:attributes="href attachment/attachment/fmt:url">edit</a>) |
1549 | - </small> |
1550 | + <a tal:replace="structure attachment/fmt:attachment_link" /> |
1551 | + <a class="sprite edit action-icon" |
1552 | + title="Change patch details" |
1553 | + tal:attributes="href attachment/fmt:url">Edit</a> |
1554 | </li> |
1555 | </ul> |
1556 | <ul> |
1557 | @@ -31,14 +27,10 @@ |
1558 | <ul> |
1559 | <li class="download-attachment" |
1560 | tal:repeat="attachment view/regular_attachments"> |
1561 | - <a tal:attributes="href attachment/file/http_url" |
1562 | - tal:content="attachment/attachment/title" |
1563 | - class="sprite download-icon"> |
1564 | - Attachment Title |
1565 | - </a> |
1566 | - <small> |
1567 | - (<a tal:attributes="href attachment/attachment/fmt:url">edit</a>) |
1568 | - </small> |
1569 | + <a tal:replace="structure attachment/fmt:attachment_link" /> |
1570 | + <a class="sprite edit action-icon" |
1571 | + title="Change attachment details" |
1572 | + tal:attributes="href attachment/fmt:url">Edit</a> |
1573 | </li> |
1574 | </ul> |
1575 | <ul> |
1576 | diff --git a/lib/lp/bugs/templates/bugcomment-box.pt b/lib/lp/bugs/templates/bugcomment-box.pt |
1577 | index b25c482..58a9458 100644 |
1578 | --- a/lib/lp/bugs/templates/bugcomment-box.pt |
1579 | +++ b/lib/lp/bugs/templates/bugcomment-box.pt |
1580 | @@ -113,24 +113,25 @@ |
1581 | <ul tal:condition="comment/bugattachments" style="margin-bottom: 1em"> |
1582 | <li tal:repeat="attachment comment/bugattachments" |
1583 | class="download-attachment"> |
1584 | - <a tal:attributes="href python: view.proxiedUrlOfLibraryFileAlias(attachment)" |
1585 | - tal:content="attachment/title" |
1586 | - class="sprite download-icon">foo.txt</a> |
1587 | + <a tal:replace="structure attachment/fmt:attachment_link" /> |
1588 | <a tal:attributes="href attachment/fmt:url" class="sprite edit action-icon">Edit</a> |
1589 | + <tal:block condition="attachment/libraryfile"> |
1590 | (<span |
1591 | tal:replace="attachment/libraryfile/content/filesize/fmt:bytes" />, |
1592 | <span tal:replace="attachment/libraryfile/mimetype" />) |
1593 | + </tal:block> |
1594 | </li> |
1595 | </ul> |
1596 | |
1597 | <ul tal:condition="comment/patches" style="margin-bottom: 1em"> |
1598 | <li tal:repeat="attachment comment/patches" class="download-attachment"> |
1599 | - <a tal:attributes="href python: view.proxiedUrlOfLibraryFileAlias(attachment)" |
1600 | - tal:content="attachment/title" class="sprite haspatch-icon">foo.txt</a> |
1601 | + <a tal:replace="structure attachment/fmt:attachment_link" /> |
1602 | <a tal:attributes="href attachment/fmt:url" class="sprite edit action-icon">Edit</a> |
1603 | + <tal:block condition="attachment/libraryfile"> |
1604 | (<span |
1605 | tal:replace="attachment/libraryfile/content/filesize/fmt:bytes" />, |
1606 | <span tal:replace="attachment/libraryfile/mimetype" />) |
1607 | + </tal:block> |
1608 | </li> |
1609 | </ul> |
1610 | |
1611 | diff --git a/lib/lp/bugs/templates/bugtarget-patches.pt b/lib/lp/bugs/templates/bugtarget-patches.pt |
1612 | index fca68c7..b4a0f8d 100644 |
1613 | --- a/lib/lp/bugs/templates/bugtarget-patches.pt |
1614 | +++ b/lib/lp/bugs/templates/bugtarget-patches.pt |
1615 | @@ -74,7 +74,7 @@ |
1616 | tal:define="patch patch_task/bug/latest_patch; |
1617 | age python:view.patchAge(patch)" |
1618 | tal:attributes="id string:patch-cell-${repeat/patch_task/index}"> |
1619 | - <a tal:attributes="href python:view.proxiedUrlForLibraryFile(patch)" |
1620 | + <a tal:attributes="href patch/displayed_url; rel python:patch.url and 'nofollow' or None" |
1621 | tal:content="age/fmt:approximateduration"></a> |
1622 | <div class="popupTitle" |
1623 | tal:attributes="id string:patch-popup-${repeat/patch_task/index};"> |
1624 | @@ -82,9 +82,8 @@ |
1625 | <strong>From:</strong> |
1626 | <a tal:replace="structure submitter/fmt:link"></a><br/> |
1627 | <strong>Link:</strong> |
1628 | - <a tal:attributes="href python:view.proxiedUrlForLibraryFile(patch)" |
1629 | - tal:content="patch/libraryfile/filename"></a></p> |
1630 | - <p tal:content="string:${patch/title}"></p> |
1631 | + <a tal:replace="structure patch/fmt:attachment_link" /> |
1632 | + </p> |
1633 | </div> |
1634 | <script type="text/javascript" tal:content="string: |
1635 | LPJS.use('base', 'node', 'event', function(Y) { |
1636 | diff --git a/lib/lp/bugs/tests/bugs-emailinterface.rst b/lib/lp/bugs/tests/bugs-emailinterface.rst |
1637 | index 8e765d1..61b1ef7 100644 |
1638 | --- a/lib/lp/bugs/tests/bugs-emailinterface.rst |
1639 | +++ b/lib/lp/bugs/tests/bugs-emailinterface.rst |
1640 | @@ -657,7 +657,7 @@ otherwise permission to complete the operation will be denied.) |
1641 | We will also add an attachment to the bug. |
1642 | |
1643 | >>> bug_attachment = bug_four.addAttachment( |
1644 | - ... bug_four.owner, b"Attachment", "No comment", "test.txt" |
1645 | + ... bug_four.owner, b"Attachment", "No comment", "test.txt", url=None |
1646 | ... ) |
1647 | |
1648 | >>> submit_commands(bug_four, "private yes") |
1649 | diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py |
1650 | index b33159e..dbdedf5 100644 |
1651 | --- a/lib/lp/scripts/tests/test_garbo.py |
1652 | +++ b/lib/lp/scripts/tests/test_garbo.py |
1653 | @@ -1209,13 +1209,15 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory): |
1654 | switch_dbuser("testadmin") |
1655 | bug = self.factory.makeBug() |
1656 | attachment = self.factory.makeBugAttachment(bug=bug) |
1657 | + # Attachments with URLs are never deleted. |
1658 | + self.factory.makeBugAttachment(bug=bug, url="https://launchpad.net") |
1659 | transaction.commit() |
1660 | |
1661 | # Bug attachments that have a LibraryFileContent record are |
1662 | # not deleted. |
1663 | self.assertIsNot(attachment.libraryfile.content, None) |
1664 | self.runDaily() |
1665 | - self.assertEqual(bug.attachments.count(), 1) |
1666 | + self.assertEqual(bug.attachments.count(), 2) |
1667 | |
1668 | # But once we delete the LfC record, the attachment is deleted |
1669 | # in the next daily garbo run. |
1670 | @@ -1224,7 +1226,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory): |
1671 | transaction.commit() |
1672 | self.runDaily() |
1673 | switch_dbuser("testadmin") |
1674 | - self.assertEqual(bug.attachments.count(), 0) |
1675 | + self.assertEqual(bug.attachments.count(), 1) |
1676 | |
1677 | def test_TimeLimitedTokenPruner(self): |
1678 | # Ensure there are no tokens |
1679 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
1680 | index e25375f..226dc96 100644 |
1681 | --- a/lib/lp/testing/factory.py |
1682 | +++ b/lib/lp/testing/factory.py |
1683 | @@ -2565,6 +2565,7 @@ class LaunchpadObjectFactory(ObjectFactory): |
1684 | filename=None, |
1685 | content_type=None, |
1686 | description=None, |
1687 | + url=None, |
1688 | is_patch=_DEFAULT, |
1689 | ): |
1690 | """Create and return a new bug attachment. |
1691 | @@ -2581,23 +2582,31 @@ class LaunchpadObjectFactory(ObjectFactory): |
1692 | string will be used. |
1693 | :param content_type: The MIME-type of this file. |
1694 | :param description: The description of the attachment. |
1695 | + :param url: External URL of the attachment (a string or None) |
1696 | :param is_patch: If true, this attachment is a patch. |
1697 | :return: An `IBugAttachment`. |
1698 | """ |
1699 | + if url: |
1700 | + if data or filename: |
1701 | + raise ValueError( |
1702 | + "Either `url` or `data` / `filename` can be provided." |
1703 | + ) |
1704 | + else: |
1705 | + if data is None: |
1706 | + data = self.getUniqueBytes() |
1707 | + if filename is None: |
1708 | + filename = self.getUniqueString() |
1709 | + |
1710 | if bug is None: |
1711 | bug = self.makeBug() |
1712 | elif isinstance(bug, (int, str)): |
1713 | bug = getUtility(IBugSet).getByNameOrID(str(bug)) |
1714 | if owner is None: |
1715 | owner = self.makePerson() |
1716 | - if data is None: |
1717 | - data = self.getUniqueBytes() |
1718 | if description is None: |
1719 | description = self.getUniqueString() |
1720 | if comment is None: |
1721 | comment = self.getUniqueString() |
1722 | - if filename is None: |
1723 | - filename = self.getUniqueString() |
1724 | # If the default value of is_patch when creating a new |
1725 | # BugAttachment should ever change, we don't want to interfere |
1726 | # with that. So, we only override it if our caller explicitly |
1727 | @@ -2610,6 +2619,7 @@ class LaunchpadObjectFactory(ObjectFactory): |
1728 | data, |
1729 | comment, |
1730 | filename, |
1731 | + url, |
1732 | content_type=content_type, |
1733 | description=description, |
1734 | **other_params, |
Some screenshots:
https:/ /people. canonical. com/~afedoseev/ LP-912/ Screenshot_ 20221004_ 195626. png /people. canonical. com/~afedoseev/ LP-912/ Screenshot_ 20221004_ 195659. png /people. canonical. com/~afedoseev/ LP-912/ Screenshot_ 20221004_ 195724. png
https:/
https:/