launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29295
[Merge] ~andrey-fedoseev/launchpad:uct-import-export-patches into launchpad:master
Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:uct-import-export-patches into launchpad:master.
Commit message:
UCT import/export: handle patch URLs
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/431262
Patch URLs are imported as `BugAttachments` with URLs
Attachment title consists of:
1. package name, e.g. "linux"
2. patch type, e.g. "upstream" or "other"
3. optional notes, e.g. "1.5.4"
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:uct-import-export-patches into launchpad:master.
diff --git a/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css b/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css
index e4f4ff3..37487d2 100644
--- a/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css
+++ b/lib/canonical/launchpad/icing/css/components/sidebar_portlets.css
@@ -132,3 +132,9 @@
padding-right: 0.25em;
}
+/******************************************************************************
+ Attachments portlet
+*/
+.side .portlet .download-attachment {
+ word-break: break-word;
+ }
diff --git a/lib/lp/bugs/adapters/bugchange.py b/lib/lp/bugs/adapters/bugchange.py
index c7babdc..e97d6e8 100644
--- a/lib/lp/bugs/adapters/bugchange.py
+++ b/lib/lp/bugs/adapters/bugchange.py
@@ -63,7 +63,6 @@ from lp.bugs.interfaces.bugtask import (
IBugTask,
)
from lp.registry.interfaces.product import IProduct
-from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.webapp.publisher import canonical_url
# These are used lp.bugs.model.bugactivity.BugActivity.attribute to normalize
@@ -698,11 +697,6 @@ class BugTagsChange(AttributeChange):
return {"text": "\n".join(messages)}
-def download_url_of_bugattachment(attachment):
- """Return the URL of the ProxiedLibraryFileAlias for the attachment."""
- return ProxiedLibraryFileAlias(attachment.libraryfile, attachment).http_url
-
-
class BugAttachmentChange(AttributeChange):
"""Used to represent a change to an `IBug`'s attachments."""
@@ -712,13 +706,13 @@ class BugAttachmentChange(AttributeChange):
old_value = None
new_value = "%s %s" % (
self.new_value.title,
- download_url_of_bugattachment(self.new_value),
+ self.new_value.displayed_url,
)
else:
what_changed = ATTACHMENT_REMOVED
old_value = "%s %s" % (
self.old_value.title,
- download_url_of_bugattachment(self.old_value),
+ self.old_value.displayed_url,
)
new_value = None
@@ -737,7 +731,7 @@ class BugAttachmentChange(AttributeChange):
message = '** %s added: "%s"\n %s' % (
attachment_str,
self.new_value.title,
- download_url_of_bugattachment(self.new_value),
+ self.new_value.displayed_url,
)
else:
if self.old_value.is_patch:
@@ -747,7 +741,7 @@ class BugAttachmentChange(AttributeChange):
message = '** %s removed: "%s"\n %s' % (
attachment_str,
self.old_value.title,
- download_url_of_bugattachment(self.old_value),
+ self.old_value.displayed_url,
)
return {"text": message}
diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py
index 3a675a8..717f39c 100644
--- a/lib/lp/bugs/browser/bug.py
+++ b/lib/lp/bugs/browser/bug.py
@@ -85,7 +85,6 @@ from lp.registry.interfaces.person import IPersonSet
from lp.services.compat import message_as_bytes
from lp.services.features import getFeatureFlag
from lp.services.fields import DuplicateBug
-from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.mail.mailwrapper import MailWrapper
from lp.services.propertycache import cachedproperty
from lp.services.searchbuilder import any, not_equals
@@ -534,17 +533,11 @@ class BugViewMixin:
"other": [],
}
for attachment in self.context.attachments_unpopulated:
- info = {
- "attachment": attachment,
- "file": ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment
- ),
- }
if attachment.type == BugAttachmentType.PATCH:
key = attachment.type
else:
key = "other"
- result[key].append(info)
+ result[key].append(attachment)
return result
@property
@@ -636,12 +629,6 @@ class BugView(LaunchpadView, BugViewMixin):
return dupes
- def proxiedUrlForLibraryFile(self, attachment):
- """Return the proxied download URL for a Librarian file."""
- return ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment
- ).http_url
-
class BugActivity(BugView):
@@ -1304,13 +1291,16 @@ class BugTextView(LaunchpadView):
def attachment_text(self, attachment):
"""Return a text representation of a bug attachment."""
- mime_type = normalize_mime_type.sub(
- " ", attachment.libraryfile.mimetype
- )
- download_url = ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment
- ).http_url
- return "%s %s" % (download_url, mime_type)
+ if attachment.url:
+ if attachment.title != attachment.url:
+ return "{}: {}".format(attachment.title, attachment.url)
+ return attachment.url
+ elif attachment.libraryfile:
+ mime_type = normalize_mime_type.sub(
+ " ", attachment.libraryfile.mimetype
+ )
+ return "{} {}".format(attachment.displayed_url, mime_type)
+ raise AssertionError()
def comment_text(self):
"""Return a text representation of bug comments."""
diff --git a/lib/lp/bugs/browser/bugattachment.py b/lib/lp/bugs/browser/bugattachment.py
index 7d45e45..5b0aa05 100644
--- a/lib/lp/bugs/browser/bugattachment.py
+++ b/lib/lp/bugs/browser/bugattachment.py
@@ -25,10 +25,7 @@ from lp.bugs.interfaces.bugattachment import (
IBugAttachmentIsPatchConfirmationForm,
IBugAttachmentSet,
)
-from lp.services.librarian.browser import (
- FileNavigationMixin,
- ProxiedLibraryFileAlias,
-)
+from lp.services.librarian.browser import FileNavigationMixin
from lp.services.librarian.interfaces import ILibraryFileAliasWithParent
from lp.services.webapp import GetitemNavigation, Navigation, canonical_url
from lp.services.webapp.authorization import check_permission
@@ -115,15 +112,19 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
self.next_url = self.cancel_url = canonical_url(
ICanonicalUrlData(context).inside
)
+ if not context.libraryfile:
+ self.field_names = ["title", "patch"]
@property
def initial_values(self):
attachment = self.context
- return dict(
+ values = dict(
title=attachment.title,
patch=attachment.type == BugAttachmentType.PATCH,
- contenttype=attachment.libraryfile.mimetype,
)
+ if attachment.libraryfile:
+ values["contenttype"] = attachment.libraryfile.mimetype
+ return values
def canEditAttachment(self, action):
return check_permission("launchpad.Edit", self.context)
@@ -135,30 +136,38 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
else:
new_type = BugAttachmentType.UNSPECIFIED
if new_type != self.context.type:
- filename = self.context.libraryfile.filename
- file_content = self.context.libraryfile.read()
- # We expect that users set data['patch'] to True only for
- # real patch data, indicated by guessed_content_type ==
- # 'text/x-diff'. If there are inconsistencies, we don't
- # set the value automatically. Instead, we lead the user to
- # another form where we ask them if they are sure about their
- # choice of the patch flag.
- new_type_consistent_with_guessed_type = (
- self.attachmentTypeConsistentWithContentType(
- new_type == BugAttachmentType.PATCH, filename, file_content
+ if self.context.libraryfile:
+ filename = self.context.libraryfile.filename
+ file_content = self.context.libraryfile.read()
+ # We expect that users set data['patch'] to True only for
+ # real patch data, indicated by guessed_content_type ==
+ # 'text/x-diff'. If there are inconsistencies, we don't
+ # set the value automatically. Instead, we lead the user to
+ # another form where we ask them if they are sure about their
+ # choice of the patch flag.
+ new_type_consistent_with_guessed_type = (
+ self.attachmentTypeConsistentWithContentType(
+ new_type == BugAttachmentType.PATCH,
+ filename,
+ file_content,
+ )
)
- )
- if new_type_consistent_with_guessed_type:
- self.context.type = new_type
+ if new_type_consistent_with_guessed_type:
+ self.context.type = new_type
+ else:
+ self.next_url = self.nextUrlForInconsistentPatchFlags(
+ self.context
+ )
else:
- self.next_url = self.nextUrlForInconsistentPatchFlags(
- self.context
- )
+ self.context.type = new_type
if data["title"] != self.context.title:
self.context.title = data["title"]
- if self.context.libraryfile.mimetype != data["contenttype"]:
+ if (
+ self.context.libraryfile
+ and self.context.libraryfile.mimetype != data["contenttype"]
+ ):
lfa_with_parent = getMultiAdapter(
(self.context.libraryfile, self.context),
ILibraryFileAliasWithParent,
@@ -167,14 +176,11 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
@action("Delete Attachment", name="delete", condition=canEditAttachment)
def delete_action(self, action, data):
- libraryfile_url = ProxiedLibraryFileAlias(
- self.context.libraryfile, self.context
- ).http_url
self.request.response.addInfoNotification(
structured(
'Attachment "<a href="%(url)s">%(name)s</a>" has been '
"deleted.",
- url=libraryfile_url,
+ url=self.context.displayed_url,
name=self.context.title,
)
)
diff --git a/lib/lp/bugs/browser/bugcomment.py b/lib/lp/bugs/browser/bugcomment.py
index 6a32ff1..0f653f6 100644
--- a/lib/lp/bugs/browser/bugcomment.py
+++ b/lib/lp/bugs/browser/bugcomment.py
@@ -29,7 +29,6 @@ from lp.bugs.interfaces.bugmessage import IBugComment
from lp.services.comments.browser.comment import download_body
from lp.services.comments.browser.messagecomment import MessageComment
from lp.services.config import config
-from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.messages.interfaces.message import IMessage
from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.webapp import (
@@ -378,12 +377,6 @@ class BugCommentBoxViewMixin:
else:
return False
- def proxiedUrlOfLibraryFileAlias(self, attachment):
- """Return the proxied URL for the Librarian file of the attachment."""
- return ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment
- ).http_url
-
class BugCommentBoxView(LaunchpadView, BugCommentBoxViewMixin):
"""Render a comment box with reply field collapsed."""
diff --git a/lib/lp/bugs/browser/bugmessage.py b/lib/lp/bugs/browser/bugmessage.py
index b0267a1..30cff9e 100644
--- a/lib/lp/bugs/browser/bugmessage.py
+++ b/lib/lp/bugs/browser/bugmessage.py
@@ -10,6 +10,7 @@ __all__ = [
from io import BytesIO
from zope.component import getUtility
+from zope.formlib.textwidgets import TextWidget
from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import TextAreaWidget
@@ -29,6 +30,9 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
custom_widget_comment = CustomWidgetFactory(
TextAreaWidget, cssClass="comment-text"
)
+ custom_widget_attachment_url = CustomWidgetFactory(
+ TextWidget, displayWidth=44, displayMaxWidth=250
+ )
page_title = "Add a comment or attachment"
@@ -49,14 +53,19 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
def validate(self, data):
- # Ensure either a comment or filecontent was provide, but only
- # if no errors have already been noted.
+ # Ensure either a comment or filecontent or a URL was provided,
+ # but only if no errors have already been noted.
if len(self.errors) == 0:
comment = data.get("comment") or ""
filecontent = data.get("filecontent", None)
- if not comment.strip() and not filecontent:
+ attachment_url = data.get("attachment_url") or ""
+ if (
+ not comment.strip()
+ and not filecontent
+ and not attachment_url.strip()
+ ):
self.addError(
- "Either a comment or attachment " "must be provided."
+ "Either a comment or attachment must be provided."
)
@action("Post Comment", name="save")
@@ -73,8 +82,10 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
# Write proper FileUpload field and widget instead of this hack.
file_ = self.request.form.get(self.widgets["filecontent"].name)
+ attachment_url = data.get("attachment_url")
+
message = None
- if data["comment"] or file_:
+ if data["comment"] or file_ or attachment_url:
bugwatch_id = data.get("bugwatch_id")
if bugwatch_id is not None:
bugwatch = getUtility(IBugWatchSet).get(bugwatch_id)
@@ -87,7 +98,7 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
bugwatch=bugwatch,
)
- # A blank comment with only a subect line is always added
+ # A blank comment with only a subject line is always added
# when the user attaches a file, so show the add comment
# feedback message only when the user actually added a
# comment.
@@ -97,6 +108,9 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
)
self.next_url = canonical_url(self.context)
+
+ attachment_description = data.get("attachment_description")
+
if file_:
# Slashes in filenames cause problems, convert them to dashes
@@ -104,11 +118,8 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
filename = file_.filename.replace("/", "-")
# if no description was given use the converted filename
- file_description = None
- if "attachment_description" in data:
- file_description = data["attachment_description"]
- if not file_description:
- file_description = filename
+ if not attachment_description:
+ attachment_description = filename
# Process the attachment.
# If the patch flag is not consistent with the result of
@@ -132,7 +143,8 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
owner=self.user,
data=BytesIO(data["filecontent"]),
filename=filename,
- description=file_description,
+ url=None,
+ description=attachment_description,
comment=message,
is_patch=is_patch,
)
@@ -146,6 +158,21 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
"Attachment %s added to bug." % filename
)
+ elif attachment_url:
+ is_patch = data["patch"]
+ bug.addAttachment(
+ owner=self.user,
+ data=None,
+ filename=None,
+ url=attachment_url,
+ description=attachment_description,
+ comment=message,
+ is_patch=is_patch,
+ )
+ self.request.response.addNotification(
+ "Attachment %s added to bug." % attachment_url
+ )
+
def shouldShowEmailMeWidget(self):
"""Should the subscribe checkbox be shown?"""
return not self.context.bug.isSubscribed(self.user)
diff --git a/lib/lp/bugs/browser/bugtarget.py b/lib/lp/bugs/browser/bugtarget.py
index e7b3dc6..43e084f 100644
--- a/lib/lp/bugs/browser/bugtarget.py
+++ b/lib/lp/bugs/browser/bugtarget.py
@@ -117,7 +117,6 @@ from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
from lp.services.config import config
from lp.services.features import getFeatureFlag
from lp.services.job.interfaces.job import JobStatus
-from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.propertycache import cachedproperty
from lp.services.webapp import LaunchpadView, canonical_url, urlappend
from lp.services.webapp.authorization import check_permission
@@ -673,6 +672,7 @@ class FileBugViewBase(LaunchpadFormView):
owner=self.user,
data=BytesIO(data["filecontent"]),
filename=filename,
+ url=None,
description=file_description,
comment=attachment_comment,
is_patch=data["patch"],
@@ -686,6 +686,7 @@ class FileBugViewBase(LaunchpadFormView):
bug.linkAttachment(
owner=self.user,
file_alias=attachment["file_alias"],
+ url=None,
description=attachment["description"],
comment=attachment_comment,
send_notifications=False,
@@ -1472,10 +1473,6 @@ class BugsPatchesView(LaunchpadView):
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."""
diff --git a/lib/lp/bugs/browser/tests/bug-views.rst b/lib/lp/bugs/browser/tests/bug-views.rst
index 74a9fd7..008659a 100644
--- a/lib/lp/bugs/browser/tests/bug-views.rst
+++ b/lib/lp/bugs/browser/tests/bug-views.rst
@@ -334,22 +334,22 @@ librarian file of the attachment.
... )
>>> view = BugView(bug_seven, request)
>>> for attachment in view.regular_attachments:
- ... print(attachment["attachment"].title)
+ ... print(attachment.title)
...
attachment 1
attachment 2
>>> for patch in view.patches:
- ... print(patch["attachment"].title)
+ ... print(patch.title)
...
patch 1
patch 2
>>> for attachment in view.regular_attachments:
- ... print(attachment["file"].http_url)
+ ... print(attachment.displayed_url)
...
http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a1
http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a2
>>> for patch in view.patches:
- ... print(patch["file"].http_url)
+ ... print(patch.displayed_url)
...
http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p1
http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p2
diff --git a/lib/lp/bugs/browser/tests/test_bug_views.py b/lib/lp/bugs/browser/tests/test_bug_views.py
index 2264af4..787b257 100644
--- a/lib/lp/bugs/browser/tests/test_bug_views.py
+++ b/lib/lp/bugs/browser/tests/test_bug_views.py
@@ -779,6 +779,25 @@ class TestBugMessageAddFormView(TestCaseWithFactory):
)
self.assertEqual(0, len(view.errors))
+ def test_add_attachment_with_url(self):
+ bug = self.factory.makeBug()
+ form = {
+ "field.comment": " ",
+ "field.actions.save": "Post Comment",
+ "field.attachment_url": "https://launchpad.net",
+ "field.attachment_description": "description",
+ "field.patch.used": "",
+ }
+ login_person(self.factory.makePerson())
+ view = create_initialized_view(
+ bug.default_bugtask, "+addcomment", form=form
+ )
+ self.assertEqual([], view.errors)
+ attachments = list(bug.attachments)
+ self.assertEqual(1, len(attachments))
+ self.assertEqual("description", attachments[0].title)
+ self.assertEqual("https://launchpad.net", attachments[0].url)
+
class TestBugMarkAsDuplicateView(TestCaseWithFactory):
"""Tests for marking a bug as a duplicate."""
diff --git a/lib/lp/bugs/browser/tests/test_bugview.py b/lib/lp/bugs/browser/tests/test_bugview.py
index 04d2e84..1124544 100644
--- a/lib/lp/bugs/browser/tests/test_bugview.py
+++ b/lib/lp/bugs/browser/tests/test_bugview.py
@@ -35,10 +35,7 @@ class TestBugView(TestCaseWithFactory):
removeSecurityProxy(attachment.libraryfile).content = None
self.assertEqual(
["regular attachment"],
- [
- attachment["attachment"].title
- for attachment in self.view.regular_attachments
- ],
+ [attachment.title for attachment in self.view.regular_attachments],
)
def test_patches_dont_include_invalid_records(self):
@@ -55,10 +52,7 @@ class TestBugView(TestCaseWithFactory):
removeSecurityProxy(patch.libraryfile).content = None
self.assertEqual(
["patch"],
- [
- attachment["attachment"].title
- for attachment in self.view.patches
- ],
+ [attachment.title for attachment in self.view.patches],
)
diff --git a/lib/lp/bugs/doc/bug-export.rst b/lib/lp/bugs/doc/bug-export.rst
index 3f466dd..0be1dd9 100644
--- a/lib/lp/bugs/doc/bug-export.rst
+++ b/lib/lp/bugs/doc/bug-export.rst
@@ -146,6 +146,7 @@ the file when we later serialise the bug:
... io.BytesIO(b"Hello World"),
... "Added attachment",
... "hello.txt",
+ ... url=None,
... description='"Hello World" attachment',
... )
<lp.bugs.model.bugattachment.BugAttachment ...>
@@ -166,8 +167,8 @@ attachment contents encoded using base-64:
<text>Added attachment</text>
<attachment href="http://bugs.launchpad.test/bugs/4/.../+files/hello.txt">
<type>UNSPECIFIED</type>
- <filename>hello.txt</filename>
<title>"Hello World" attachment</title>
+ <filename>hello.txt</filename>
<mimetype>text/plain</mimetype>
<contents>SGVsbG8gV29ybGQ=
</contents>
diff --git a/lib/lp/bugs/doc/bug.rst b/lib/lp/bugs/doc/bug.rst
index e28eff2..848028f 100644
--- a/lib/lp/bugs/doc/bug.rst
+++ b/lib/lp/bugs/doc/bug.rst
@@ -742,6 +742,7 @@ Adding an attachment.
>>> attachment = attachmentset.create(
... bug=firefox_bug,
... filealias=filealias,
+ ... url=None,
... title="Some info.",
... message=message,
... )
diff --git a/lib/lp/bugs/doc/bugattachments.rst b/lib/lp/bugs/doc/bugattachments.rst
index d2f99c8..58e16f1 100644
--- a/lib/lp/bugs/doc/bugattachments.rst
+++ b/lib/lp/bugs/doc/bugattachments.rst
@@ -53,6 +53,7 @@ ObjectCreatedEvent in order to trigger email notifications:
... owner=foobar,
... data=data,
... filename="foo.bar",
+ ... url=None,
... description="this fixes the bug",
... comment=message,
... is_patch=False,
@@ -77,6 +78,7 @@ passed in is often a file-like object, but can be bytes too.
... owner=foobar,
... data=data,
... filename="foo.baz",
+ ... url=None,
... description="this fixes the bug",
... comment="a string comment",
... is_patch=False,
@@ -92,6 +94,7 @@ If no description is given, the title is set to the filename.
>>> screenshot = bug_four.addAttachment(
... owner=foobar,
... data=data,
+ ... url=None,
... filename="screenshot.jpg",
... comment="a string comment",
... is_patch=False,
@@ -110,6 +113,7 @@ The content type is guessed based on the information provided.
... owner=foobar,
... data=data,
... filename="something.debdiff",
+ ... url=None,
... comment="something debdiffish",
... is_patch=False,
... )
@@ -503,6 +507,7 @@ It's also possible to delete attachments.
... owner=foobar,
... data=data,
... filename="foo.baz",
+ ... url=None,
... description="Attachment to be deleted",
... comment="a string comment",
... is_patch=False,
@@ -552,6 +557,7 @@ property returning True.
... owner=foobar,
... data=BytesIO(filecontent.getvalue()),
... filename="foo.baz",
+ ... url=None,
... description="A non-patch attachment",
... comment="a string comment",
... is_patch=False,
@@ -564,6 +570,7 @@ property returning True.
... owner=foobar,
... data=BytesIO(filecontent.getvalue()),
... filename="foo.baz",
+ ... url=None,
... description="A patch attachment",
... comment="a string comment",
... is_patch=True,
@@ -601,7 +608,10 @@ LibraryFileAlias.restricted and Bug.private. See also the section
>>> bug = factory.makeBug()
>>> bug.linkAttachment(
- ... owner=bug.owner, file_alias=file_alias, comment="Some attachment"
+ ... owner=bug.owner,
+ ... file_alias=file_alias,
+ ... url=None,
+ ... comment="Some attachment",
... )
<lp.bugs.model.bugattachment.BugAttachment ...>
@@ -631,6 +641,7 @@ meaningful description.
>>> bug.linkAttachment(
... owner=bug.owner,
... file_alias=file_alias,
+ ... url=None,
... comment="Some attachment",
... is_patch=True,
... description="An attachment of some sort",
@@ -688,6 +699,7 @@ its Librarian file is set.
... owner=private_bug_owner,
... data=b"secret",
... filename="baz.txt",
+ ... url=None,
... comment="Some attachment",
... )
>>> private_attachment.libraryfile.restricted
diff --git a/lib/lp/bugs/doc/bugcomment.rst b/lib/lp/bugs/doc/bugcomment.rst
index 14a8b71..5157e55 100644
--- a/lib/lp/bugs/doc/bugcomment.rst
+++ b/lib/lp/bugs/doc/bugcomment.rst
@@ -77,6 +77,7 @@ in activity_and_comments...
... data=BytesIO(b"whatever"),
... comment=bug_11.initial_message,
... filename="test.txt",
+ ... url=None,
... is_patch=False,
... content_type="text/plain",
... description="sample data",
@@ -235,6 +236,7 @@ attachments to a bug to see this in action:
... data=file_,
... description="Ho",
... filename="munchy",
+ ... url=None,
... comment="Hello there",
... )
>>> m6 = bug_three.newMessage(
@@ -436,6 +438,7 @@ not included in BugComment.patches.
... data=BytesIO(b"whatever"),
... comment=bug.initial_message,
... filename="file1",
+ ... url=None,
... is_patch=False,
... content_type="text/plain",
... description="sample data 1",
@@ -445,6 +448,7 @@ not included in BugComment.patches.
... data=BytesIO(b"whatever"),
... comment=bug.initial_message,
... filename="file2",
+ ... url=None,
... is_patch=False,
... content_type="text/plain",
... description="sample data 2",
@@ -454,6 +458,7 @@ not included in BugComment.patches.
... data=BytesIO(b"whatever"),
... comment=bug.initial_message,
... filename="patch1",
+ ... url=None,
... is_patch=True,
... content_type="text/plain",
... description="patch 1",
@@ -463,6 +468,7 @@ not included in BugComment.patches.
... data=BytesIO(b"whatever"),
... comment=bug.initial_message,
... filename="patch2",
+ ... url=None,
... is_patch=True,
... content_type="text/plain",
... description="patch 2",
diff --git a/lib/lp/bugs/interfaces/bug.py b/lib/lp/bugs/interfaces/bug.py
index 9697660..e02f2ff 100644
--- a/lib/lp/bugs/interfaces/bug.py
+++ b/lib/lp/bugs/interfaces/bug.py
@@ -43,6 +43,7 @@ from lazr.restful.interface import copy_field
from zope.component import getUtility
from zope.interface import Attribute, Interface
from zope.schema import (
+ URI,
Bool,
Bytes,
Choice,
@@ -877,11 +878,12 @@ class IBugAppend(Interface):
@call_with(owner=REQUEST_USER, from_api=True)
@operation_parameters(
- data=Bytes(constraint=attachment_size_constraint),
+ data=Bytes(constraint=attachment_size_constraint, required=False),
comment=Text(),
- filename=TextLine(),
+ filename=TextLine(required=False),
+ url=URI(required=False),
is_patch=Bool(),
- content_type=TextLine(),
+ content_type=TextLine(required=False),
description=Text(),
)
@export_factory_operation(IBugAttachment, [])
@@ -891,6 +893,7 @@ class IBugAppend(Interface):
data,
comment,
filename,
+ url,
is_patch=False,
content_type=None,
description=None,
@@ -903,6 +906,7 @@ class IBugAppend(Interface):
:description: A brief description of the attachment.
:comment: An IMessage or string.
:filename: A string.
+ :url: External URL of the attachment
:is_patch: A boolean.
"""
@@ -956,12 +960,13 @@ class IBugAppend(Interface):
"""
def linkAttachment(
- owner, file_alias, comment, is_patch=False, description=None
+ owner, file_alias, url, comment, is_patch=False, description=None
):
"""Link an `ILibraryFileAlias` to this bug.
:owner: An IPerson.
:file_alias: The `ILibraryFileAlias` to link to this bug.
+ :url: External URL of the attachment.
:description: A brief description of the attachment.
:comment: An IMessage or string.
:is_patch: A boolean.
diff --git a/lib/lp/bugs/interfaces/bugattachment.py b/lib/lp/bugs/interfaces/bugattachment.py
index 0ded472..b4b7599 100644
--- a/lib/lp/bugs/interfaces/bugattachment.py
+++ b/lib/lp/bugs/interfaces/bugattachment.py
@@ -22,7 +22,7 @@ from lazr.restful.declarations import (
)
from lazr.restful.fields import Reference
from zope.interface import Interface
-from zope.schema import Bool, Bytes, Choice, Int, TextLine
+from zope.schema import URI, Bool, Bytes, Choice, Int, TextLine
from lp import _
from lp.bugs.interfaces.hasbug import IHasBug
@@ -90,10 +90,13 @@ class IBugAttachmentView(IHasBug):
)
libraryfile = exported(
Bytes(
- title=_("The attachment content."), required=True, readonly=True
+ title=_("The attachment content."), required=False, readonly=True
),
exported_as="data",
)
+ url = exported(
+ URI(title=_("Attachment URL"), required=False, readonly=True)
+ )
_message_id = Int(title=_("Message ID"))
message = exported(
Reference(
@@ -109,6 +112,12 @@ class IBugAttachmentView(IHasBug):
description=_("Is this attachment a patch?"),
readonly=True,
)
+ displayed_url = URI(
+ title=_(
+ "Download URL of the files or the external URL of the attachment"
+ ),
+ readonly=True,
+ )
def getFileByName(filename):
"""Return the `ILibraryFileAlias` for the given file name.
@@ -176,6 +185,7 @@ class IBugAttachmentSet(Interface):
def create(
bug,
filealias,
+ url,
title,
message,
type=IBugAttachment["type"].default,
@@ -184,7 +194,8 @@ class IBugAttachmentSet(Interface):
"""Create a new attachment and return it.
:param bug: The `IBug` to which the new attachment belongs.
- :param filealias: The `IFilealias` containing the data.
+ :param filealias: The `IFilealias` containing the data (optional).
+ :param url: External URL of the attachment (optional).
:param message: The `IMessage` to which this attachment belongs.
:param type: The type of attachment. See `BugAttachmentType`.
:param send_notifications: If True, a notification is sent to
diff --git a/lib/lp/bugs/interfaces/bugmessage.py b/lib/lp/bugs/interfaces/bugmessage.py
index 7768b07..22c8d39 100644
--- a/lib/lp/bugs/interfaces/bugmessage.py
+++ b/lib/lp/bugs/interfaces/bugmessage.py
@@ -11,7 +11,7 @@ __all__ = [
]
from zope.interface import Attribute, Interface
-from zope.schema import Bool, Bytes, Int, Object, Text, TextLine
+from zope.schema import URI, Bool, Bytes, Int, Object, Text, TextLine
from lp.app.validators.attachment import attachment_size_constraint
from lp.bugs.interfaces.bug import IBug
@@ -103,6 +103,7 @@ class IBugMessageAddForm(Interface):
required=False,
constraint=attachment_size_constraint,
)
+ attachment_url = URI(title="URL", required=False)
patch = Bool(
title="This attachment contains a solution (patch) for this bug",
required=False,
diff --git a/lib/lp/bugs/mail/handler.py b/lib/lp/bugs/mail/handler.py
index 975af40..775b570 100644
--- a/lib/lp/bugs/mail/handler.py
+++ b/lib/lp/bugs/mail/handler.py
@@ -410,6 +410,7 @@ class MaloneHandler:
getUtility(IBugAttachmentSet).create(
bug=bug,
filealias=blob,
+ url=None,
attach_type=attach_type,
title=blob.filename,
message=message,
diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py
index 5b1465c..8e99ff0 100644
--- a/lib/lp/bugs/model/bug.py
+++ b/lib/lp/bugs/model/bug.py
@@ -1608,6 +1608,7 @@ class Bug(SQLBase, InformationTypeMixin):
data,
comment,
filename,
+ url,
is_patch=False,
content_type=None,
description=None,
@@ -1619,35 +1620,45 @@ class Bug(SQLBase, InformationTypeMixin):
# wrongly encoded.
if from_api:
data = get_raw_form_value_from_current_request(data, "data")
- if isinstance(data, bytes):
- filecontent = data
- else:
- filecontent = data.read()
- if is_patch:
- content_type = "text/plain"
- else:
- if content_type is None:
- content_type, encoding = guess_content_type(
- name=filename, body=filecontent
- )
+ if data:
+ if isinstance(data, bytes):
+ filecontent = data
+ else:
+ filecontent = data.read()
- filealias = getUtility(ILibraryFileAliasSet).create(
- name=filename,
- size=len(filecontent),
- file=BytesIO(filecontent),
- contentType=content_type,
- restricted=self.private,
- )
+ if is_patch:
+ content_type = "text/plain"
+ else:
+ if content_type is None:
+ content_type, encoding = guess_content_type(
+ name=filename, body=filecontent
+ )
+
+ file_alias = getUtility(ILibraryFileAliasSet).create(
+ name=filename,
+ size=len(filecontent),
+ file=BytesIO(filecontent),
+ contentType=content_type,
+ restricted=self.private,
+ )
+ else:
+ file_alias = None
return self.linkAttachment(
- owner, filealias, comment, is_patch, description
+ owner=owner,
+ file_alias=file_alias,
+ url=url,
+ comment=comment,
+ is_patch=is_patch,
+ description=description,
)
def linkAttachment(
self,
owner,
file_alias,
+ url,
comment,
is_patch=False,
description=None,
@@ -1672,8 +1683,10 @@ class Bug(SQLBase, InformationTypeMixin):
if description:
title = description
- else:
+ elif file_alias:
title = file_alias.filename
+ else:
+ title = url
if IMessage.providedBy(comment):
message = comment
@@ -1685,6 +1698,7 @@ class Bug(SQLBase, InformationTypeMixin):
return getUtility(IBugAttachmentSet).create(
bug=self,
filealias=file_alias,
+ url=url,
attach_type=attach_type,
title=title,
message=message,
@@ -2206,9 +2220,10 @@ class Bug(SQLBase, InformationTypeMixin):
# XXX: This should be a bulk update. RBC 20100827
# bug=https://bugs.launchpad.net/storm/+bug/625071
for attachment in self.attachments_unpopulated:
- attachment.libraryfile.restricted = (
- information_type in PRIVATE_INFORMATION_TYPES
- )
+ if attachment.libraryfile:
+ attachment.libraryfile.restricted = (
+ information_type in PRIVATE_INFORMATION_TYPES
+ )
self.information_type = information_type
self._reconcileAccess()
@@ -2558,16 +2573,34 @@ class Bug(SQLBase, InformationTypeMixin):
def _attachments_query(self):
"""Helper for the attachments* properties."""
- # bug attachments with no LibraryFileContent have been deleted - the
- # garbo_daily run will remove the LibraryFileAlias asynchronously.
- # See bug 542274 for more details.
+ # get bug attachments that have either a URL,
+ # or associated LibraryFileContent
+ # bug attachments with LibraryFileAlias and no LibraryFileContent have
+ # been deleted - the garbo_daily run will remove the LibraryFileAlias
+ # asynchronously. See bug 542274 for more details.
store = Store.of(self)
- return store.find(
- (BugAttachment, LibraryFileAlias, LibraryFileContent),
- BugAttachment.bug == self,
- BugAttachment.libraryfile == LibraryFileAlias.id,
- LibraryFileContent.id == LibraryFileAlias.contentID,
- ).order_by(BugAttachment.id)
+ return (
+ store.using(
+ BugAttachment,
+ LeftJoin(
+ LibraryFileAlias,
+ BugAttachment.libraryfile == LibraryFileAlias.id,
+ ),
+ LeftJoin(
+ LibraryFileContent,
+ LibraryFileContent.id == LibraryFileAlias.contentID,
+ ),
+ )
+ .find(
+ (BugAttachment, LibraryFileAlias, LibraryFileContent),
+ BugAttachment.bug == self,
+ Or(
+ BugAttachment.url != None,
+ LibraryFileAlias.contentID != None,
+ ),
+ )
+ .order_by(BugAttachment.id)
+ )
@property
def attachments(self):
diff --git a/lib/lp/bugs/model/bugattachment.py b/lib/lp/bugs/model/bugattachment.py
index 180c353..0a570d4 100644
--- a/lib/lp/bugs/model/bugattachment.py
+++ b/lib/lp/bugs/model/bugattachment.py
@@ -17,6 +17,7 @@ from lp.bugs.interfaces.bugattachment import (
from lp.services.database.enumcol import DBEnum
from lp.services.database.interfaces import IStore
from lp.services.database.stormbase import StormBase
+from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.propertycache import cachedproperty
@@ -36,8 +37,9 @@ class BugAttachment(StormBase):
default=IBugAttachment["type"].default,
)
title = Unicode(allow_none=False)
- libraryfile_id = Int(name="libraryfile", allow_none=False)
+ libraryfile_id = Int(name="libraryfile")
libraryfile = Reference(libraryfile_id, "LibraryFileAlias.id")
+ url = Unicode()
_message_id = Int(name="message", allow_none=False)
_message = Reference(_message_id, "Message.id")
@@ -46,6 +48,7 @@ class BugAttachment(StormBase):
bug,
title,
libraryfile,
+ url,
message,
type=IBugAttachment["type"].default,
):
@@ -53,6 +56,7 @@ class BugAttachment(StormBase):
self.bug = bug
self.title = title
self.libraryfile = libraryfile
+ self.url = url
self._message = message
self.type = type
@@ -81,15 +85,23 @@ class BugAttachment(StormBase):
# Delete the reference to the LibraryFileContent record right now,
# in order to avoid problems with not deleted files as described
# in bug 387188.
- self.libraryfile.content = None
+ if self.libraryfile:
+ self.libraryfile.content = None
Store.of(self).remove(self)
def getFileByName(self, filename):
"""See IBugAttachment."""
- if filename == self.libraryfile.filename:
+ if self.libraryfile and filename == self.libraryfile.filename:
return self.libraryfile
raise NotFoundError(filename)
+ @property
+ def displayed_url(self):
+ return (
+ self.url
+ or ProxiedLibraryFileAlias(self.libraryfile, self).http_url
+ )
+
@implementer(IBugAttachmentSet)
class BugAttachmentSet:
@@ -110,18 +122,26 @@ class BugAttachmentSet:
self,
bug,
filealias,
+ url,
title,
message,
attach_type=None,
send_notifications=False,
):
"""See `IBugAttachmentSet`."""
+ if not filealias and not url:
+ raise ValueError("Either filealias or url must be provided")
+
+ if filealias and url:
+ raise ValueError("Only one of filealias or url may be provided")
+
if attach_type is None:
# XXX kiko 2005-08-03 bug=1659: this should use DEFAULT.
attach_type = IBugAttachment["type"].default
attachment = BugAttachment(
bug=bug,
libraryfile=filealias,
+ url=url,
type=attach_type,
title=title,
message=message,
diff --git a/lib/lp/bugs/model/tests/test_bugtasksearch.py b/lib/lp/bugs/model/tests/test_bugtasksearch.py
index db52d71..05793e0 100644
--- a/lib/lp/bugs/model/tests/test_bugtasksearch.py
+++ b/lib/lp/bugs/model/tests/test_bugtasksearch.py
@@ -268,6 +268,7 @@ class OnceTests:
data=b"filedata",
comment="a comment",
filename="file1.txt",
+ url=None,
is_patch=False,
)
self.bugtasks[1].bug.addAttachment(
@@ -275,6 +276,7 @@ class OnceTests:
data=b"filedata",
comment="a comment",
filename="file1.txt",
+ url=None,
is_patch=True,
)
# We can search for bugs with non-patch attachments...
diff --git a/lib/lp/bugs/scripts/bugexport.py b/lib/lp/bugs/scripts/bugexport.py
index 3d3327a..c6c0c43 100644
--- a/lib/lp/bugs/scripts/bugexport.py
+++ b/lib/lp/bugs/scripts/bugexport.py
@@ -16,7 +16,6 @@ from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.bugs.browser.bugtask import get_comments_for_bugtask
from lp.bugs.interfaces.bugtask import IBugTaskSet
from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
-from lp.services.librarian.browser import ProxiedLibraryFileAlias
BUGS_XMLNS = "https://launchpad.net/xmlns/2006/bugs"
@@ -85,27 +84,30 @@ def serialise_bugtask(bugtask):
attachment_node = ET.SubElement(
comment_node,
"attachment",
- href=ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment
- ).http_url,
+ href=attachment.displayed_url,
)
attachment_node.text = attachment_node.tail = "\n"
addnode(attachment_node, "type", attachment.type.name)
- addnode(
- attachment_node, "filename", attachment.libraryfile.filename
- )
addnode(attachment_node, "title", attachment.title)
- addnode(
- attachment_node, "mimetype", attachment.libraryfile.mimetype
- )
- # Attach the attachment file contents, base 64 encoded.
- addnode(
- attachment_node,
- "contents",
- base64.encodebytes(attachment.libraryfile.read()).decode(
- "ASCII"
- ),
- )
+ if attachment.libraryfile:
+ addnode(
+ attachment_node,
+ "filename",
+ attachment.libraryfile.filename,
+ )
+ addnode(
+ attachment_node,
+ "mimetype",
+ attachment.libraryfile.mimetype,
+ )
+ # Attach the attachment file contents, base 64 encoded.
+ addnode(
+ attachment_node,
+ "contents",
+ base64.encodebytes(attachment.libraryfile.read()).decode(
+ "ASCII"
+ ),
+ )
return bug_node
diff --git a/lib/lp/bugs/scripts/bugimport.py b/lib/lp/bugs/scripts/bugimport.py
index 323c9b1..38306fb 100644
--- a/lib/lp/bugs/scripts/bugimport.py
+++ b/lib/lp/bugs/scripts/bugimport.py
@@ -445,6 +445,7 @@ class BugImporter:
getUtility(IBugAttachmentSet).create(
bug=bug,
filealias=filealias,
+ url=None,
attach_type=attach_type,
title=title,
message=message,
diff --git a/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222 b/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
index 30574d6..95a74f0 100644
--- a/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
+++ b/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
@@ -30,6 +30,8 @@ CVSS:
Patches_linux:
break-fix: 457f44363a8894135c85b7a9afd2bd8196db24ab c25b2ae136039ffa820c26138ed4a5e5f3ab3841|local-CVE-2022-23222-fix
+ upstream: https://github.com/389ds/389-ds-base/commit/58dbf084a63e6dbbd999bf6a70475fad8255f26a (1.4.4)
+ upstream: https://github.com/389ds/389-ds-base/commit/2e5b526012612d1d6ccace46398bee679a730271
upstream_linux: released (5.17~rc1)
impish_linux: released (5.13.0-37.42)
devel_linux: not-affected (5.15.0-25.25)
diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py
index f9aa65d..80fee9c 100644
--- a/lib/lp/bugs/scripts/tests/test_bugnotification.py
+++ b/lib/lp/bugs/scripts/tests/test_bugnotification.py
@@ -1026,7 +1026,7 @@ class TestEmailNotificationsAttachments(
# another five minutes. Therefore, we send out separate
# change notifications.
return self.bug.addAttachment(
- self.person, b"content", "a comment", "stuff.txt"
+ self.person, b"content", "a comment", "stuff.txt", url=None
)
old = cachedproperty("old")(_attachment)
diff --git a/lib/lp/bugs/scripts/tests/test_uct.py b/lib/lp/bugs/scripts/tests/test_uct.py
index 00bc2c8..04580b8 100644
--- a/lib/lp/bugs/scripts/tests/test_uct.py
+++ b/lib/lp/bugs/scripts/tests/test_uct.py
@@ -120,7 +120,19 @@ class TestUCTRecord(TestCase):
"c25b2ae136039ffa820c26138ed4a5e5f3ab3841|"
"local-CVE-2022-23222-fix"
),
- )
+ ),
+ UCTRecord.Patch(
+ patch_type="upstream",
+ entry=(
+ "https://github.com/389ds/389-ds-base/commit/58dbf084a63e6dbbd999bf6a70475fad8255f26a (1.4.4)" # noqa: 501
+ ),
+ ),
+ UCTRecord.Patch(
+ patch_type="upstream",
+ entry=(
+ "https://github.com/389ds/389-ds-base/commit/2e5b526012612d1d6ccace46398bee679a730271" # noqa: 501
+ ),
+ ),
],
),
UCTRecord.Package(
@@ -260,7 +272,22 @@ class TestCVE(TestCaseWithFactory):
],
priority=None,
tags=set(),
- patches=[],
+ patches=[
+ UCTRecord.Patch(
+ patch_type="upstream",
+ entry=(
+ "https://github.com/389ds/389-ds-base/"
+ "commit/123 (1.4.4)"
+ ),
+ ),
+ UCTRecord.Patch(
+ patch_type="upstream",
+ entry=(
+ "https://github.com/389ds/389-ds-base/"
+ "commit/456"
+ ),
+ ),
+ ],
),
UCTRecord.Package(
name=dsp2.sourcepackagename.name,
@@ -417,6 +444,20 @@ class TestCVE(TestCaseWithFactory):
),
),
],
+ patch_urls=[
+ CVE.PatchURL(
+ package_name=dsp1.sourcepackagename,
+ type="upstream",
+ url="https://github.com/389ds/389-ds-base/" "commit/123",
+ notes="1.4.4",
+ ),
+ CVE.PatchURL(
+ package_name=dsp1.sourcepackagename,
+ type="upstream",
+ url="https://github.com/389ds/389-ds-base/" "commit/456",
+ notes=None,
+ ),
+ ],
)
def test_make_from_uct_record(self):
@@ -428,6 +469,40 @@ class TestCVE(TestCaseWithFactory):
self.assertListEqual(self.uct_record.packages, uct_record.packages)
self.assertDictEqual(self.uct_record.__dict__, uct_record.__dict__)
+ def test_get_patches(self):
+ spn = self.factory.makeSourcePackageName()
+ self.assertListEqual(
+ [
+ CVE.PatchURL(
+ package_name=spn,
+ url="https://github.com/repo/1",
+ type="upstream",
+ notes=None,
+ ),
+ CVE.PatchURL(
+ package_name=spn,
+ url="https://github.com/repo/2",
+ type="upstream",
+ notes="1.2.3",
+ ),
+ ],
+ list(
+ CVE.get_patch_urls(
+ spn,
+ [
+ UCTRecord.Patch("break-fix", "- -"),
+ UCTRecord.Patch(
+ "upstream", "https://github.com/repo/1"
+ ),
+ UCTRecord.Patch(
+ "upstream", "https://github.com/repo/2 (1.2.3)"
+ ),
+ UCTRecord.Patch("other", "foo"),
+ ],
+ )
+ ),
+ )
+
class TestUCTImporterExporter(TestCaseWithFactory):
@@ -610,6 +685,20 @@ class TestUCTImporterExporter(TestCaseWithFactory):
),
),
],
+ patch_urls=[
+ CVE.PatchURL(
+ package_name=self.ubuntu_package.sourcepackagename,
+ type="upstream",
+ url="https://github.com/389ds/389-ds-base/" "commit/123",
+ notes="1.4.4",
+ ),
+ CVE.PatchURL(
+ package_name=self.ubuntu_package.sourcepackagename,
+ type="upstream",
+ url="https://github.com/389ds/389-ds-base/" "commit/456",
+ notes=None,
+ ),
+ ],
)
self.importer = UCTImporter()
self.exporter = UCTExporter()
@@ -630,6 +719,8 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.assertEqual(len(cve.bug_urls), len(watches))
self.assertEqual(sorted(cve.bug_urls), sorted(w.url for w in watches))
+ self.checkBugAttachments(bug, cve)
+
def checkBugTasks(self, bug: Bug, cve: CVE):
bug_tasks = bug.bugtasks # type: List[BugTask]
@@ -690,6 +781,20 @@ class TestUCTImporterExporter(TestCaseWithFactory):
for t in bug_tasks:
self.assertEqual(cve.assignee, t.assignee)
+ def checkBugAttachments(self, bug: Bug, cve: CVE):
+ attachments_by_url = {a.url: a for a in bug.attachments if a.url}
+ for patch_url in cve.patch_urls:
+ self.assertIn(patch_url.url, attachments_by_url)
+ attachment = attachments_by_url[patch_url.url]
+ expected_title = "{}/{}".format(
+ patch_url.package_name.name, patch_url.type
+ )
+ if patch_url.notes:
+ expected_title = "{}/{}".format(
+ expected_title, patch_url.notes
+ )
+ self.assertEqual(expected_title, attachment.title)
+
def checkVulnerabilities(self, bug: Bug, cve: CVE):
vulnerabilities = bug.vulnerabilities
@@ -758,6 +863,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.assertEqual(expected.notes, actual.notes)
self.assertEqual(expected.mitigation, actual.mitigation)
self.assertListEqual(expected.cvss, actual.cvss)
+ self.assertListEqual(expected.patch_urls, actual.patch_urls)
def test_create_bug(self):
bug = self.importer.create_bug(self.cve, self.lp_cve)
@@ -769,7 +875,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.assertEqual([self.lp_cve], bug.cves)
activities = list(bug.activity)
- self.assertEqual(4, len(activities))
+ self.assertEqual(6, len(activities))
import_bug_activity = activities[-1]
self.assertEqual(self.bug_importer, import_bug_activity.person)
self.assertEqual("bug", import_bug_activity.whatchanged)
@@ -1005,6 +1111,22 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.importer.update_bug(bug, cve, self.lp_cve)
self.checkBug(bug, cve)
+ def test_update_patch_urls(self):
+ bug = self.importer.create_bug(self.cve, self.lp_cve)
+ cve = self.cve
+
+ # Add new patch URL
+ cve.patch_urls.append(
+ CVE.PatchURL(
+ package_name=cve.distro_packages[0].package_name,
+ type="upstream",
+ url="https://github.com/123",
+ notes=None,
+ )
+ )
+ self.importer.update_bug(bug, cve, self.lp_cve)
+ self.checkBug(bug, cve)
+
def test_import_cve(self):
self.importer.import_cve(self.cve)
self.assertIsNotNone(
diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
index b7b7f6e..a63c805 100644
--- a/lib/lp/bugs/scripts/uct/models.py
+++ b/lib/lp/bugs/scripts/uct/models.py
@@ -2,16 +2,29 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
import logging
+import re
from collections import OrderedDict, defaultdict
from datetime import datetime
from enum import Enum
from pathlib import Path
-from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, Union
+from typing import (
+ Any,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Set,
+ Tuple,
+ Union,
+)
from typing.io import TextIO
import dateutil.parser
from contrib.cve_lib import load_cve
from zope.component import getUtility
+from zope.schema import URI
+from zope.schema.interfaces import InvalidURI
from lp.bugs.enums import VulnerabilityStatus
from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus
@@ -448,6 +461,21 @@ class CVE:
),
)
+ PatchURL = NamedTuple(
+ "PatchURL",
+ (
+ ("package_name", SourcePackageName),
+ ("type", str),
+ ("url", str),
+ ("notes", Optional[str]),
+ ),
+ )
+
+ # Example:
+ # https://github.com/389ds/389-ds-base/commit/123 (1.4.4)
+ # https://github.com/389ds/389-ds-base/commit/345
+ PATCH_URL_RE = re.compile(r"^(?P<url>.+?)(\s+\((?P<notes>.+)\))?$")
+
PRIORITY_MAP = {
UCTRecord.Priority.CRITICAL: BugTaskImportance.CRITICAL,
UCTRecord.Priority.HIGH: BugTaskImportance.HIGH,
@@ -502,6 +530,7 @@ class CVE:
notes: str,
mitigation: str,
cvss: List[CVSS],
+ patch_urls: Optional[List[PatchURL]] = None,
):
self.sequence = sequence
self.date_made_public = date_made_public
@@ -521,6 +550,7 @@ class CVE:
self.notes = notes
self.mitigation = mitigation
self.cvss = cvss
+ self.patch_urls = patch_urls or [] # type: List[CVE.PatchURL]
@classmethod
def make_from_uct_record(cls, uct_record: UCTRecord) -> "CVE":
@@ -532,6 +562,7 @@ class CVE:
distro_packages = []
series_packages = []
+ patch_urls = []
spn_set = getUtility(ISourcePackageNameSet)
@@ -541,6 +572,11 @@ class CVE:
for uct_package in uct_record.packages:
source_package_name = spn_set.getOrCreateByName(uct_package.name)
+
+ patch_urls.extend(
+ cls.get_patch_urls(source_package_name, uct_package.patches)
+ )
+
package_importance = (
cls.PRIORITY_MAP[uct_package.priority]
if uct_package.priority
@@ -660,6 +696,7 @@ class CVE:
notes=uct_record.notes,
mitigation=uct_record.mitigation,
cvss=uct_record.cvss,
+ patch_urls=patch_urls,
)
def to_uct_record(self) -> UCTRecord:
@@ -749,6 +786,17 @@ class CVE:
patches=[],
)
+ for patch_url in self.patch_urls:
+ entry = patch_url.url
+ if patch_url.notes:
+ entry = "{} ({})".format(entry, patch_url.notes)
+ packages_by_name[patch_url.package_name.name].patches.append(
+ UCTRecord.Patch(
+ patch_type=patch_url.type,
+ entry=entry,
+ )
+ )
+
return UCTRecord(
parent_dir=self.VULNERABILITY_STATUS_MAP_REVERSE.get(
self.status, ""
@@ -826,3 +874,33 @@ class CVE:
"Could not find the distro series: %s", distro_series_name
)
return distro_series
+
+ @classmethod
+ def get_patch_urls(
+ cls,
+ source_package_name: SourcePackageName,
+ patches: List[UCTRecord.Patch],
+ ) -> Iterable[PatchURL]:
+ for patch in patches:
+ if patch.patch_type == "break-fix":
+ continue
+ match = cls.PATCH_URL_RE.match(patch.entry)
+ if not match:
+ logger.warning(
+ "Could not parse the patch entry: %s", patch.entry
+ )
+ continue
+
+ try:
+ url = URI().fromUnicode(match.groupdict()["url"])
+ except InvalidURI:
+ logger.error("Invalid patch URL: %s", patch.entry)
+ continue
+
+ notes = match.groupdict().get("notes")
+ yield cls.PatchURL(
+ package_name=source_package_name,
+ type=patch.patch_type,
+ url=url,
+ notes=notes,
+ )
diff --git a/lib/lp/bugs/scripts/uct/uctexport.py b/lib/lp/bugs/scripts/uct/uctexport.py
index d4b5b45..8a1dd27 100644
--- a/lib/lp/bugs/scripts/uct/uctexport.py
+++ b/lib/lp/bugs/scripts/uct/uctexport.py
@@ -2,6 +2,7 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
import logging
+import re
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional
@@ -10,6 +11,7 @@ from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
from lp.bugs.interfaces.bug import IBugSet
+from lp.bugs.interfaces.bugattachment import BugAttachmentType
from lp.bugs.model.bug import Bug as BugModel
from lp.bugs.model.bugtask import BugTask
from lp.bugs.model.cve import Cve as CveModel
@@ -44,6 +46,13 @@ class UCTExporter:
),
)
+ # Example:
+ # linux/upstream
+ # linux/upstream/5.19.1
+ BUG_ATTACHMENT_TITLE_RE = re.compile(
+ "^(?P<package_name>.+?)/(?P<patch_type>.+?)(/(?P<notes>.+?))?$"
+ )
+
def export_bug_to_uct_file(
self, bug_id: int, output_dir: Path
) -> Optional[Path]:
@@ -192,6 +201,31 @@ class UCTExporter:
)
)
+ packages_by_name = {
+ p.name: p for p in package_name_by_product.values()
+ }
+ patch_urls = []
+ for attachment in bug.attachments:
+ if (
+ not attachment.url
+ or not attachment.type == BugAttachmentType.PATCH
+ ):
+ continue
+ title_match = self.BUG_ATTACHMENT_TITLE_RE.match(attachment.title)
+ if not title_match:
+ continue
+ spn = packages_by_name.get(title_match.groupdict()["package_name"])
+ if not spn:
+ continue
+ patch_urls.append(
+ CVE.PatchURL(
+ package_name=spn,
+ type=title_match.groupdict()["patch_type"],
+ url=attachment.url,
+ notes=title_match.groupdict().get("notes"),
+ )
+ )
+
return CVE(
sequence="CVE-{}".format(lp_cve.sequence),
date_made_public=vulnerability.date_made_public,
@@ -217,6 +251,7 @@ class UCTExporter:
)
for authority, vector_string in lp_cve.cvss.items()
],
+ patch_urls=patch_urls,
)
def _parse_bug_description(
diff --git a/lib/lp/bugs/scripts/uct/uctimport.py b/lib/lp/bugs/scripts/uct/uctimport.py
index 12bc601..5bc8411 100644
--- a/lib/lp/bugs/scripts/uct/uctimport.py
+++ b/lib/lp/bugs/scripts/uct/uctimport.py
@@ -38,6 +38,7 @@ from lp.app.enums import InformationType
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
from lp.bugs.interfaces.bugactivity import IBugActivitySet
+from lp.bugs.interfaces.bugattachment import BugAttachmentType
from lp.bugs.interfaces.bugtask import BugTaskImportance, IBugTaskSet
from lp.bugs.interfaces.bugwatch import IBugWatchSet
from lp.bugs.interfaces.cve import ICveSet
@@ -166,6 +167,7 @@ class UCTImporter:
) # type: BugModel
self._update_external_bug_urls(bug, cve.bug_urls)
+ self._update_patches(bug, cve.patch_urls)
self._create_bug_tasks(
bug,
@@ -222,6 +224,7 @@ class UCTImporter:
)
self._assign_bug_tasks(bug, cve.assignee)
self._update_external_bug_urls(bug, cve.bug_urls)
+ self._update_patches(bug, cve.patch_urls)
# Update or add new Vulnerabilities
vulnerabilities_by_distro = {
@@ -429,6 +432,29 @@ class UCTImporter:
for external_bug_url in bug_urls:
bug_watch_set.fromText(external_bug_url, bug, self.bug_importer)
+ def _update_patches(self, bug: BugModel, patch_urls: List[CVE.PatchURL]):
+ attachments_by_url = {a.url: a for a in bug.attachments if a.url}
+ for patch_url in patch_urls:
+ title = "{}/{}".format(patch_url.package_name.name, patch_url.type)
+ if patch_url.notes:
+ title = "{}/{}".format(title, patch_url.notes)
+ if patch_url in attachments_by_url:
+ attachment = removeSecurityProxy(
+ attachments_by_url[patch_url.url]
+ )
+ attachment.title = title
+ attachment.type = BugAttachmentType.PATCH
+ else:
+ bug.addAttachment(
+ owner=bug.owner,
+ data=None,
+ comment=None,
+ filename=None,
+ url=patch_url.url,
+ is_patch=True,
+ description=title,
+ )
+
def _make_bug_description(self, cve: CVE) -> str:
"""
Some `CVE` fields can't be mapped to Launchpad models.
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
index 090ca83..9df6720 100644
--- a/lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.rst
+++ b/lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.rst
@@ -35,6 +35,7 @@ If we add an attachment to the bug report...
... data=BytesIO(b"whatever"),
... comment=bug_11.initial_message,
... filename="test.txt",
+ ... url=None,
... is_patch=False,
... content_type="text/plain",
... description="sample data",
@@ -74,6 +75,7 @@ Patch attachments are shown before non-patch attachments
... data=BytesIO(b"patchy patch patch"),
... comment=bug_11.initial_message,
... filename="patch.txt",
+ ... url=None,
... is_patch=True,
... content_type="text/plain",
... description="a patch",
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst
index 93e9cc6..bfa0b6e 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst
+++ b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.rst
@@ -23,6 +23,7 @@ We'll start by adding some attachments to the bug:
... content,
... "comment for file a",
... "file_a.txt",
+ ... url=None,
... content_type="text/html",
... )
>>> content = BytesIO(b"do we need to")
@@ -31,6 +32,7 @@ We'll start by adding some attachments to the bug:
... content,
... "comment for file with space",
... "file with space.txt",
+ ... url=None,
... content_type='text/plain;\n name="file with space.txt"',
... )
>>> content = BytesIO(b"Yes we can!")
@@ -39,6 +41,7 @@ We'll start by adding some attachments to the bug:
... content,
... "comment for patch",
... "bug-patch.diff",
+ ... url=None,
... is_patch=True,
... content_type="text/plain",
... description="a patch",
diff --git a/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst b/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst
index 12f5e92..bed6dba 100644
--- a/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst
+++ b/lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.rst
@@ -311,6 +311,7 @@ Patches also appear as badges in bug listings.
... owner=foobar,
... data=BytesIO(b"file data"),
... filename="foo.bar",
+ ... url=None,
... description="this fixes the bug",
... comment=message,
... is_patch=True,
diff --git a/lib/lp/bugs/stories/webservice/xx-bug.rst b/lib/lp/bugs/stories/webservice/xx-bug.rst
index 80c2f24..8266254 100644
--- a/lib/lp/bugs/stories/webservice/xx-bug.rst
+++ b/lib/lp/bugs/stories/webservice/xx-bug.rst
@@ -1531,6 +1531,7 @@ An attachment can be added to the bug:
... "addAttachment",
... data=io.BytesIO(b"12345"),
... filename="numbers.txt",
+ ... url=None,
... content_type="foo/bar",
... comment="The numbers you asked for.",
... )
@@ -1558,6 +1559,7 @@ Now, bug 1 has one attachment:
self_link: 'http://.../bugs/1/+attachment/...'
title: 'numbers.txt'
type: 'Unspecified'
+ url: None
web_link: 'http://bugs.../bugs/1/+attachment/...'
---
@@ -1572,6 +1574,7 @@ The attachment can be fetched directly:
self_link: 'http://.../bugs/1/+attachment/...'
title: 'numbers.txt'
type: 'Unspecified'
+ url: None
web_link: 'http://bugs.../bugs/1/+attachment/...'
Fetching the data actually yields a redirect to the Librarian, which
diff --git a/lib/lp/bugs/templates/bug-attachment-macros.pt b/lib/lp/bugs/templates/bug-attachment-macros.pt
index a06b835..d91d0c3 100644
--- a/lib/lp/bugs/templates/bug-attachment-macros.pt
+++ b/lib/lp/bugs/templates/bug-attachment-macros.pt
@@ -15,6 +15,12 @@
<metal:widget metal:use-macro="context/@@launchpad_form/widget_row" />
</tal:filecontent>
+ <tal:url
+ tal:define="widget nocall:view/widgets/attachment_url|nothing"
+ tal:condition="widget">
+ <metal:widget metal:use-macro="context/@@launchpad_form/widget_row" />
+ </tal:url>
+
<tal:patch
tal:define="widget nocall:view/widgets/patch|nothing"
tal:condition="widget">
diff --git a/lib/lp/bugs/templates/bug-portlet-attachments.pt b/lib/lp/bugs/templates/bug-portlet-attachments.pt
index 5a79a9f..552ede8 100644
--- a/lib/lp/bugs/templates/bug-portlet-attachments.pt
+++ b/lib/lp/bugs/templates/bug-portlet-attachments.pt
@@ -8,13 +8,13 @@
<ul>
<li class="download-attachment"
tal:repeat="attachment view/patches">
- <a tal:attributes="href attachment/file/http_url"
- tal:content="attachment/attachment/title"
+ <a tal:attributes="href attachment/displayed_url"
+ tal:content="attachment/title"
class="sprite haspatch-icon">
Attachment Title
</a>
<small>
- (<a tal:attributes="href attachment/attachment/fmt:url">edit</a>)
+ (<a tal:attributes="href attachment/fmt:url">edit</a>)
</small>
</li>
</ul>
@@ -31,13 +31,13 @@
<ul>
<li class="download-attachment"
tal:repeat="attachment view/regular_attachments">
- <a tal:attributes="href attachment/file/http_url"
- tal:content="attachment/attachment/title"
+ <a tal:attributes="href attachment/displayed_url"
+ tal:content="attachment/title"
class="sprite download-icon">
Attachment Title
</a>
<small>
- (<a tal:attributes="href attachment/attachment/fmt:url">edit</a>)
+ (<a tal:attributes="href attachment/fmt:url">edit</a>)
</small>
</li>
</ul>
diff --git a/lib/lp/bugs/templates/bugcomment-box.pt b/lib/lp/bugs/templates/bugcomment-box.pt
index b25c482..2163a3d 100644
--- a/lib/lp/bugs/templates/bugcomment-box.pt
+++ b/lib/lp/bugs/templates/bugcomment-box.pt
@@ -113,24 +113,28 @@
<ul tal:condition="comment/bugattachments" style="margin-bottom: 1em">
<li tal:repeat="attachment comment/bugattachments"
class="download-attachment">
- <a tal:attributes="href python: view.proxiedUrlOfLibraryFileAlias(attachment)"
+ <a tal:attributes="href attachment/displayed_url"
tal:content="attachment/title"
class="sprite download-icon">foo.txt</a>
<a tal:attributes="href attachment/fmt:url" class="sprite edit action-icon">Edit</a>
+ <tal:block condition="attachment/libraryfile">
(<span
tal:replace="attachment/libraryfile/content/filesize/fmt:bytes" />,
<span tal:replace="attachment/libraryfile/mimetype" />)
+ </tal:block>
</li>
</ul>
<ul tal:condition="comment/patches" style="margin-bottom: 1em">
<li tal:repeat="attachment comment/patches" class="download-attachment">
- <a tal:attributes="href python: view.proxiedUrlOfLibraryFileAlias(attachment)"
+ <a tal:attributes="href attachment/displayed_url"
tal:content="attachment/title" class="sprite haspatch-icon">foo.txt</a>
<a tal:attributes="href attachment/fmt:url" class="sprite edit action-icon">Edit</a>
+ <tal:block condition="attachment/libraryfile">
(<span
tal:replace="attachment/libraryfile/content/filesize/fmt:bytes" />,
<span tal:replace="attachment/libraryfile/mimetype" />)
+ </tal:block>
</li>
</ul>
diff --git a/lib/lp/bugs/templates/bugtarget-patches.pt b/lib/lp/bugs/templates/bugtarget-patches.pt
index fca68c7..38c8aad 100644
--- a/lib/lp/bugs/templates/bugtarget-patches.pt
+++ b/lib/lp/bugs/templates/bugtarget-patches.pt
@@ -74,7 +74,7 @@
tal:define="patch patch_task/bug/latest_patch;
age python:view.patchAge(patch)"
tal:attributes="id string:patch-cell-${repeat/patch_task/index}">
- <a tal:attributes="href python:view.proxiedUrlForLibraryFile(patch)"
+ <a tal:attributes="href patch/displayed_url"
tal:content="age/fmt:approximateduration"></a>
<div class="popupTitle"
tal:attributes="id string:patch-popup-${repeat/patch_task/index};">
@@ -82,8 +82,8 @@
<strong>From:</strong>
<a tal:replace="structure submitter/fmt:link"></a><br/>
<strong>Link:</strong>
- <a tal:attributes="href python:view.proxiedUrlForLibraryFile(patch)"
- tal:content="patch/libraryfile/filename"></a></p>
+ <a tal:attributes="href patch/displayed_url"
+ tal:content="patch/libraryfile/filename | patch/url"></a></p>
<p tal:content="string:${patch/title}"></p>
</div>
<script type="text/javascript" tal:content="string:
diff --git a/lib/lp/bugs/tests/bugs-emailinterface.rst b/lib/lp/bugs/tests/bugs-emailinterface.rst
index 8e765d1..61b1ef7 100644
--- a/lib/lp/bugs/tests/bugs-emailinterface.rst
+++ b/lib/lp/bugs/tests/bugs-emailinterface.rst
@@ -657,7 +657,7 @@ otherwise permission to complete the operation will be denied.)
We will also add an attachment to the bug.
>>> bug_attachment = bug_four.addAttachment(
- ... bug_four.owner, b"Attachment", "No comment", "test.txt"
+ ... bug_four.owner, b"Attachment", "No comment", "test.txt", url=None
... )
>>> submit_commands(bug_four, "private yes")
diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py
index b33159e..dbdedf5 100644
--- a/lib/lp/scripts/tests/test_garbo.py
+++ b/lib/lp/scripts/tests/test_garbo.py
@@ -1209,13 +1209,15 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
switch_dbuser("testadmin")
bug = self.factory.makeBug()
attachment = self.factory.makeBugAttachment(bug=bug)
+ # Attachments with URLs are never deleted.
+ self.factory.makeBugAttachment(bug=bug, url="https://launchpad.net")
transaction.commit()
# Bug attachments that have a LibraryFileContent record are
# not deleted.
self.assertIsNot(attachment.libraryfile.content, None)
self.runDaily()
- self.assertEqual(bug.attachments.count(), 1)
+ self.assertEqual(bug.attachments.count(), 2)
# But once we delete the LfC record, the attachment is deleted
# in the next daily garbo run.
@@ -1224,7 +1226,7 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
transaction.commit()
self.runDaily()
switch_dbuser("testadmin")
- self.assertEqual(bug.attachments.count(), 0)
+ self.assertEqual(bug.attachments.count(), 1)
def test_TimeLimitedTokenPruner(self):
# Ensure there are no tokens
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 7ecb210..d69badb 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -2565,6 +2565,7 @@ class LaunchpadObjectFactory(ObjectFactory):
filename=None,
content_type=None,
description=None,
+ url=None,
is_patch=_DEFAULT,
):
"""Create and return a new bug attachment.
@@ -2581,23 +2582,31 @@ class LaunchpadObjectFactory(ObjectFactory):
string will be used.
:param content_type: The MIME-type of this file.
:param description: The description of the attachment.
+ :param url: External URL of the attachment (a string or None)
:param is_patch: If true, this attachment is a patch.
:return: An `IBugAttachment`.
"""
+ if url:
+ if data or filename:
+ raise ValueError(
+ "Either `url` or `data` / `filename` " "can be provided."
+ )
+ else:
+ if data is None:
+ data = self.getUniqueBytes()
+ if filename is None:
+ filename = self.getUniqueString()
+
if bug is None:
bug = self.makeBug()
elif isinstance(bug, (int, str)):
bug = getUtility(IBugSet).getByNameOrID(str(bug))
if owner is None:
owner = self.makePerson()
- if data is None:
- data = self.getUniqueBytes()
if description is None:
description = self.getUniqueString()
if comment is None:
comment = self.getUniqueString()
- if filename is None:
- filename = self.getUniqueString()
# If the default value of is_patch when creating a new
# BugAttachment should ever change, we don't want to interfere
# with that. So, we only override it if our caller explicitly
@@ -2610,6 +2619,7 @@ class LaunchpadObjectFactory(ObjectFactory):
data,
comment,
filename,
+ url,
content_type=content_type,
description=description,
**other_params,
Follow ups