launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28701
[Merge] ~cjwatson/launchpad:black-bugs into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:black-bugs into launchpad:master.
Commit message:
lp.bugs: Apply black
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/425872
--
The attached diff has been truncated due to its size.
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:black-bugs into launchpad:master.
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 5030b87..accdfac 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -64,3 +64,5 @@ c606443bdb2f342593c9a7c9437cb70c01f85f29
01c7f7112b20dab5c48373339d530a39f0dc859b
# apply black to lp.blueprints
6178b1869f90f9bf1eb9ed050154888b523c53c5
+# apply black to lp.bugs
+2edd6d52841e03ba11ad431e40828f2f0b6a7e17
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dbcf289..a1c7bb0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -46,6 +46,7 @@ repos:
|archivepublisher
|archiveuploader
|blueprints
+ |bugs
)/
- repo: https://github.com/PyCQA/isort
rev: 5.9.2
@@ -68,6 +69,7 @@ repos:
|archivepublisher
|archiveuploader
|blueprints
+ |bugs
)/
- id: isort
alias: isort-black
@@ -80,6 +82,7 @@ repos:
|archivepublisher
|archiveuploader
|blueprints
+ |bugs
)/
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
diff --git a/lib/lp/bugs/adapters/bug.py b/lib/lp/bugs/adapters/bug.py
index 831f312..ce91bd9 100644
--- a/lib/lp/bugs/adapters/bug.py
+++ b/lib/lp/bugs/adapters/bug.py
@@ -4,10 +4,10 @@
"""Resources having to do with Launchpad bugs."""
__all__ = [
- 'bugcomment_to_entry',
- 'bugtask_to_privacy',
- 'convert_to_information_type',
- ]
+ "bugcomment_to_entry",
+ "bugtask_to_privacy",
+ "convert_to_information_type",
+]
from lazr.restful.interfaces import IEntry
from zope.component import getMultiAdapter
@@ -22,7 +22,8 @@ def bugcomment_to_entry(comment, version):
real IMessage instances but IBugComment.
"""
return getMultiAdapter(
- (comment.bugtask.bug.messages[comment.index], version), IEntry)
+ (comment.bugtask.bug.messages[comment.index], version), IEntry
+ )
def bugtask_to_privacy(bugtask):
diff --git a/lib/lp/bugs/adapters/bugchange.py b/lib/lp/bugs/adapters/bugchange.py
index 714353b..c7babdc 100644
--- a/lib/lp/bugs/adapters/bugchange.py
+++ b/lib/lp/bugs/adapters/bugchange.py
@@ -4,89 +4,85 @@
"""Implementations for bug changes."""
__all__ = [
- 'ATTACHMENT_ADDED',
- 'ATTACHMENT_REMOVED',
- 'BRANCH_LINKED',
- 'BRANCH_UNLINKED',
- 'BranchLinkedToBug',
- 'BranchUnlinkedFromBug',
- 'BUG_WATCH_ADDED',
- 'BUG_WATCH_REMOVED',
- 'BugAttachmentChange',
- 'BugConvertedToQuestion',
- 'BugDescriptionChange',
- 'BugDuplicateChange',
- 'BugInformationTypeChange',
- 'BugLocked',
- 'BugLockReasonSet',
- 'BugTagsChange',
- 'BugTaskAdded',
- 'BugTaskAssigneeChange',
- 'BugTaskBugWatchChange',
- 'BugTaskDeleted',
- 'BugTaskImportanceChange',
- 'BugTaskMilestoneChange',
- 'BugTaskStatusChange',
- 'BugTaskTargetChange',
- 'BugTitleChange',
- 'BugUnlocked',
- 'BugWatchAdded',
- 'BugWatchRemoved',
- 'CHANGED_DUPLICATE_MARKER',
- 'CVE_LINKED',
- 'CVE_UNLINKED',
- 'CveLinkedToBug',
- 'CveUnlinkedFromBug',
- 'get_bug_change_class',
- 'get_bug_changes',
- 'MARKED_AS_DUPLICATE',
- 'MERGE_PROPOSAL_LINKED',
- 'MERGE_PROPOSAL_UNLINKED',
- 'MergeProposalLinkedToBug',
- 'MergeProposalUnlinkedFromBug',
- 'REMOVED_DUPLICATE_MARKER',
- 'REMOVED_SUBSCRIBER',
- 'SeriesNominated',
- 'UnsubscribedFromBug',
- ]
+ "ATTACHMENT_ADDED",
+ "ATTACHMENT_REMOVED",
+ "BRANCH_LINKED",
+ "BRANCH_UNLINKED",
+ "BranchLinkedToBug",
+ "BranchUnlinkedFromBug",
+ "BUG_WATCH_ADDED",
+ "BUG_WATCH_REMOVED",
+ "BugAttachmentChange",
+ "BugConvertedToQuestion",
+ "BugDescriptionChange",
+ "BugDuplicateChange",
+ "BugInformationTypeChange",
+ "BugLocked",
+ "BugLockReasonSet",
+ "BugTagsChange",
+ "BugTaskAdded",
+ "BugTaskAssigneeChange",
+ "BugTaskBugWatchChange",
+ "BugTaskDeleted",
+ "BugTaskImportanceChange",
+ "BugTaskMilestoneChange",
+ "BugTaskStatusChange",
+ "BugTaskTargetChange",
+ "BugTitleChange",
+ "BugUnlocked",
+ "BugWatchAdded",
+ "BugWatchRemoved",
+ "CHANGED_DUPLICATE_MARKER",
+ "CVE_LINKED",
+ "CVE_UNLINKED",
+ "CveLinkedToBug",
+ "CveUnlinkedFromBug",
+ "get_bug_change_class",
+ "get_bug_changes",
+ "MARKED_AS_DUPLICATE",
+ "MERGE_PROPOSAL_LINKED",
+ "MERGE_PROPOSAL_UNLINKED",
+ "MergeProposalLinkedToBug",
+ "MergeProposalUnlinkedFromBug",
+ "REMOVED_DUPLICATE_MARKER",
+ "REMOVED_SUBSCRIBER",
+ "SeriesNominated",
+ "UnsubscribedFromBug",
+]
from textwrap import dedent
from zope.interface import implementer
from zope.security.proxy import isinstance as zope_isinstance
-from lp.bugs.enums import (
- BugLockStatus,
- BugNotificationLevel,
- )
+from lp.bugs.enums import BugLockStatus, BugNotificationLevel
from lp.bugs.interfaces.bugchange import IBugChange
from lp.bugs.interfaces.bugtask import (
- IBugTask,
RESOLVED_BUGTASK_STATUSES,
UNRESOLVED_BUGTASK_STATUSES,
- )
+ 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
# the output from these change objects into the attribute that actually
# changed. It is fragile, but a reasonable incremental step.
ATTACHMENT_ADDED = "attachment added"
ATTACHMENT_REMOVED = "attachment removed"
-BRANCH_LINKED = 'branch linked'
-BRANCH_UNLINKED = 'branch unlinked'
-BUG_WATCH_ADDED = 'bug watch added'
-BUG_WATCH_REMOVED = 'bug watch removed'
-CHANGED_DUPLICATE_MARKER = 'changed duplicate marker'
-CVE_LINKED = 'cve linked'
-CVE_UNLINKED = 'cve unlinked'
-MARKED_AS_DUPLICATE = 'marked as duplicate'
-MERGE_PROPOSAL_LINKED = 'merge proposal linked'
-MERGE_PROPOSAL_UNLINKED = 'merge proposal unlinked'
-REMOVED_DUPLICATE_MARKER = 'removed duplicate marker'
-REMOVED_SUBSCRIBER = 'removed subscriber'
+BRANCH_LINKED = "branch linked"
+BRANCH_UNLINKED = "branch unlinked"
+BUG_WATCH_ADDED = "bug watch added"
+BUG_WATCH_REMOVED = "bug watch removed"
+CHANGED_DUPLICATE_MARKER = "changed duplicate marker"
+CVE_LINKED = "cve linked"
+CVE_UNLINKED = "cve unlinked"
+MARKED_AS_DUPLICATE = "marked as duplicate"
+MERGE_PROPOSAL_LINKED = "merge proposal linked"
+MERGE_PROPOSAL_UNLINKED = "merge proposal unlinked"
+REMOVED_DUPLICATE_MARKER = "removed duplicate marker"
+REMOVED_SUBSCRIBER = "removed subscriber"
class NoBugChangeFoundError(Exception):
@@ -106,7 +102,8 @@ def get_bug_change_class(obj, field_name):
except KeyError:
raise NoBugChangeFoundError(
"Unable to find a suitable BugChange for field '%s' on object "
- "%s" % (field_name, obj))
+ "%s" % (field_name, obj)
+ )
def get_bug_changes(bug_delta):
@@ -114,15 +111,25 @@ def get_bug_changes(bug_delta):
# The order of the field names in this list is important; this is
# the order in which changes will appear both in the bug activity
# log and in notification emails.
- bug_change_field_names = ['duplicateof', 'title', 'description',
- 'information_type', 'tags', 'attachment']
+ bug_change_field_names = [
+ "duplicateof",
+ "title",
+ "description",
+ "information_type",
+ "tags",
+ "attachment",
+ ]
for field_name in bug_change_field_names:
field_delta = getattr(bug_delta, field_name)
if field_delta is not None:
bug_change_class = get_bug_change_class(bug_delta.bug, field_name)
yield bug_change_class(
- when=None, person=bug_delta.user, what_changed=field_name,
- old_value=field_delta['old'], new_value=field_delta['new'])
+ when=None,
+ person=bug_delta.user,
+ what_changed=field_name,
+ old_value=field_delta["old"],
+ new_value=field_delta["new"],
+ )
if bug_delta.bugtask_deltas is not None:
bugtask_deltas = bug_delta.bugtask_deltas
@@ -133,21 +140,30 @@ def get_bug_changes(bug_delta):
# The order here is important; see bug_change_field_names.
bugtask_change_field_names = [
- 'target', 'importance', 'status', 'milestone', 'bugwatch',
- 'assignee', 'importance_explanation', 'status_explanation',
- ]
+ "target",
+ "importance",
+ "status",
+ "milestone",
+ "bugwatch",
+ "assignee",
+ "importance_explanation",
+ "status_explanation",
+ ]
for bugtask_delta in bugtask_deltas:
for field_name in bugtask_change_field_names:
field_delta = getattr(bugtask_delta, field_name)
if field_delta is not None:
bug_change_class = get_bug_change_class(
- bugtask_delta.bugtask, field_name)
+ bugtask_delta.bugtask, field_name
+ )
yield bug_change_class(
bug_task=bugtask_delta.bugtask,
- when=None, person=bug_delta.user,
+ when=None,
+ person=bug_delta.user,
what_changed=field_name,
- old_value=field_delta['old'],
- new_value=field_delta['new'])
+ old_value=field_delta["old"],
+ new_value=field_delta["new"],
+ )
@implementer(IBugChange)
@@ -182,10 +198,10 @@ class AttributeChange(BugChangeBase):
def getBugActivity(self):
"""Return the BugActivity data for the textual change."""
return {
- 'newvalue': self.new_value,
- 'oldvalue': self.old_value,
- 'whatchanged': self.what_changed,
- }
+ "newvalue": self.new_value,
+ "oldvalue": self.old_value,
+ "whatchanged": self.what_changed,
+ }
class UnsubscribedFromBug(BugChangeBase):
@@ -194,20 +210,20 @@ class UnsubscribedFromBug(BugChangeBase):
def __init__(self, when, person, unsubscribed_user, **kwargs):
super().__init__(when, person)
self.unsubscribed_user = unsubscribed_user
- self.send_notification = kwargs.get('send_notification', False)
- self.notification_text = kwargs.get('notification_text')
+ self.send_notification = kwargs.get("send_notification", False)
+ self.notification_text = kwargs.get("notification_text")
def getBugActivity(self):
"""See `IBugChange`."""
return dict(
- whatchanged='%s %s' % (
- REMOVED_SUBSCRIBER,
- self.unsubscribed_user.displayname))
+ whatchanged="%s %s"
+ % (REMOVED_SUBSCRIBER, self.unsubscribed_user.displayname)
+ )
def getBugNotification(self):
"""See `IBugChange`."""
if self.send_notification and self.notification_text:
- return {'text': '** %s' % self.notification_text}
+ return {"text": "** %s" % self.notification_text}
else:
return None
@@ -222,16 +238,17 @@ class BugConvertedToQuestion(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
return dict(
- whatchanged='converted to question',
- newvalue=str(self.question.id))
+ whatchanged="converted to question", newvalue=str(self.question.id)
+ )
def getBugNotification(self):
"""See `IBugChange`."""
return {
- 'text': (
- '** Converted to question:\n'
- ' %s' % canonical_url(self.question)),
- }
+ "text": (
+ "** Converted to question:\n"
+ " %s" % canonical_url(self.question)
+ ),
+ }
class BugTaskAdded(BugChangeBase):
@@ -246,30 +263,31 @@ class BugTaskAdded(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
return dict(
- whatchanged='bug task added',
- newvalue=self.bug_task.bugtargetname)
+ whatchanged="bug task added", newvalue=self.bug_task.bugtargetname
+ )
def getBugNotification(self):
"""See `IBugChange`."""
lines = []
if self.bug_task.bugwatch:
- lines.append("** Also affects: %s via" % (
- self.bug_task.bugtargetname))
+ lines.append(
+ "** Also affects: %s via" % (self.bug_task.bugtargetname)
+ )
lines.append(" %s" % self.bug_task.bugwatch.url)
else:
- lines.append("** Also affects: %s" % (
- self.bug_task.bugtargetname))
- lines.append("%13s: %s" % (
- "Importance", self.bug_task.importance.title))
+ lines.append("** Also affects: %s" % (self.bug_task.bugtargetname))
+ lines.append(
+ "%13s: %s" % ("Importance", self.bug_task.importance.title)
+ )
if self.bug_task.assignee:
assignee = self.bug_task.assignee
- lines.append("%13s: %s" % (
- "Assignee", assignee.unique_displayname))
- lines.append("%13s: %s" % (
- "Status", self.bug_task.status.title))
+ lines.append(
+ "%13s: %s" % ("Assignee", assignee.unique_displayname)
+ )
+ lines.append("%13s: %s" % ("Status", self.bug_task.status.title))
return {
- 'text': '\n'.join(lines),
- }
+ "text": "\n".join(lines),
+ }
class BugTaskDeleted(BugChangeBase):
@@ -283,16 +301,13 @@ class BugTaskDeleted(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
- return dict(
- whatchanged='bug task deleted',
- oldvalue=self.targetname)
+ return dict(whatchanged="bug task deleted", oldvalue=self.targetname)
def getBugNotification(self):
"""See `IBugChange`."""
return {
- 'text': (
- "** No longer affects: %s" % self.targetname),
- }
+ "text": ("** No longer affects: %s" % self.targetname),
+ }
class SeriesNominated(BugChangeBase):
@@ -305,8 +320,9 @@ class SeriesNominated(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
return dict(
- whatchanged='nominated for series',
- newvalue=self.series.bugtargetname)
+ whatchanged="nominated for series",
+ newvalue=self.series.bugtargetname,
+ )
def getBugNotification(self):
"""See `IBugChange`."""
@@ -322,19 +338,21 @@ class BugWatchAdded(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
- return dict(
- whatchanged=BUG_WATCH_ADDED,
- newvalue=self.bug_watch.url)
+ return dict(whatchanged=BUG_WATCH_ADDED, newvalue=self.bug_watch.url)
def getBugNotification(self):
"""See `IBugChange`."""
return {
- 'text': (
+ "text": (
"** Bug watch added: %s #%s\n"
- " %s" % (
- self.bug_watch.bugtracker.title, self.bug_watch.remotebug,
- self.bug_watch.url)),
- }
+ " %s"
+ % (
+ self.bug_watch.bugtracker.title,
+ self.bug_watch.remotebug,
+ self.bug_watch.url,
+ )
+ ),
+ }
class BugWatchRemoved(BugChangeBase):
@@ -346,19 +364,21 @@ class BugWatchRemoved(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
- return dict(
- whatchanged=BUG_WATCH_REMOVED,
- oldvalue=self.bug_watch.url)
+ return dict(whatchanged=BUG_WATCH_REMOVED, oldvalue=self.bug_watch.url)
def getBugNotification(self):
"""See `IBugChange`."""
return {
- 'text': (
+ "text": (
"** Bug watch removed: %s #%s\n"
- " %s" % (
- self.bug_watch.bugtracker.title, self.bug_watch.remotebug,
- self.bug_watch.url)),
- }
+ " %s"
+ % (
+ self.bug_watch.bugtracker.title,
+ self.bug_watch.remotebug,
+ self.bug_watch.url,
+ )
+ ),
+ }
class BranchLinkedToBug(BugChangeBase):
@@ -374,14 +394,14 @@ class BranchLinkedToBug(BugChangeBase):
if self.branch.private:
return None
return dict(
- whatchanged=BRANCH_LINKED,
- newvalue=self.branch.bzr_identity)
+ whatchanged=BRANCH_LINKED, newvalue=self.branch.bzr_identity
+ )
def getBugNotification(self):
"""See `IBugChange`."""
if self.branch.private or self.bug.is_complete:
return None
- return {'text': '** Branch linked: %s' % self.branch.bzr_identity}
+ return {"text": "** Branch linked: %s" % self.branch.bzr_identity}
class BranchUnlinkedFromBug(BugChangeBase):
@@ -397,14 +417,14 @@ class BranchUnlinkedFromBug(BugChangeBase):
if self.branch.private:
return None
return dict(
- whatchanged=BRANCH_UNLINKED,
- oldvalue=self.branch.bzr_identity)
+ whatchanged=BRANCH_UNLINKED, oldvalue=self.branch.bzr_identity
+ )
def getBugNotification(self):
"""See `IBugChange`."""
if self.branch.private or self.bug.is_complete:
return None
- return {'text': '** Branch unlinked: %s' % self.branch.bzr_identity}
+ return {"text": "** Branch unlinked: %s" % self.branch.bzr_identity}
class MergeProposalLinkedToBug(BugChangeBase):
@@ -421,17 +441,19 @@ class MergeProposalLinkedToBug(BugChangeBase):
return None
return dict(
whatchanged=MERGE_PROPOSAL_LINKED,
- newvalue=canonical_url(self.merge_proposal))
+ newvalue=canonical_url(self.merge_proposal),
+ )
def getBugNotification(self):
"""See `IBugChange`."""
if self.merge_proposal.private or self.bug.is_complete:
return None
return {
- 'text': (
- '** Merge proposal linked:\n'
- ' %s' % canonical_url(self.merge_proposal)),
- }
+ "text": (
+ "** Merge proposal linked:\n"
+ " %s" % canonical_url(self.merge_proposal)
+ ),
+ }
class MergeProposalUnlinkedFromBug(BugChangeBase):
@@ -448,17 +470,19 @@ class MergeProposalUnlinkedFromBug(BugChangeBase):
return None
return dict(
whatchanged=MERGE_PROPOSAL_UNLINKED,
- oldvalue=canonical_url(self.merge_proposal))
+ oldvalue=canonical_url(self.merge_proposal),
+ )
def getBugNotification(self):
"""See `IBugChange`."""
if self.merge_proposal.private or self.bug.is_complete:
return None
return {
- 'text': (
- '** Merge proposal unlinked:\n'
- ' %s' % canonical_url(self.merge_proposal)),
- }
+ "text": (
+ "** Merge proposal unlinked:\n"
+ " %s" % canonical_url(self.merge_proposal)
+ ),
+ }
class BugDescriptionChange(AttributeChange):
@@ -466,23 +490,26 @@ class BugDescriptionChange(AttributeChange):
def getBugNotification(self):
from lp.services.mail.notification import get_unified_diff
- description_diff = get_unified_diff(
- self.old_value, self.new_value, 72)
- notification_text = (
- "** Description changed:\n\n%s" % description_diff)
- return {'text': notification_text}
+
+ description_diff = get_unified_diff(self.old_value, self.new_value, 72)
+ notification_text = "** Description changed:\n\n%s" % description_diff
+ return {"text": notification_text}
def _is_status_change_lifecycle_change(old_status, new_status):
"""Is a status change a lifecycle change?"""
# Bug is moving from one of unresolved bug statuses (like
# 'in progress') to one of resolved ('fix released').
- bug_is_closed = (old_status in UNRESOLVED_BUGTASK_STATUSES and
- new_status in RESOLVED_BUGTASK_STATUSES)
+ bug_is_closed = (
+ old_status in UNRESOLVED_BUGTASK_STATUSES
+ and new_status in RESOLVED_BUGTASK_STATUSES
+ )
# Bug is moving back from one of resolved bug statuses (reopening).
- bug_is_reopened = (old_status in RESOLVED_BUGTASK_STATUSES and
- new_status in UNRESOLVED_BUGTASK_STATUSES)
+ bug_is_reopened = (
+ old_status in RESOLVED_BUGTASK_STATUSES
+ and new_status in UNRESOLVED_BUGTASK_STATUSES
+ )
return bug_is_closed or bug_is_reopened
@@ -498,17 +525,19 @@ class BugDuplicateChange(AttributeChange):
# Bug was already a duplicate of one bug,
# and we are changing it to be a duplicate of another bug.
lifecycle = _is_status_change_lifecycle_change(
- old_bug.default_bugtask.status,
- new_bug.default_bugtask.status)
+ old_bug.default_bugtask.status, new_bug.default_bugtask.status
+ )
elif new_bug is not None:
# old_bug is None here, so we are just adding a duplicate marker.
- lifecycle = (new_bug.default_bugtask.status in
- RESOLVED_BUGTASK_STATUSES)
+ lifecycle = (
+ new_bug.default_bugtask.status in RESOLVED_BUGTASK_STATUSES
+ )
elif old_bug is not None:
# Unmarking a bug as duplicate. This is lifecycle change
# only if bug has been reopened as a result.
- lifecycle = (old_bug.default_bugtask.status in
- RESOLVED_BUGTASK_STATUSES)
+ lifecycle = (
+ old_bug.default_bugtask.status in RESOLVED_BUGTASK_STATUSES
+ )
else:
pass
@@ -520,42 +549,47 @@ class BugDuplicateChange(AttributeChange):
def getBugActivity(self):
if self.old_value is not None and self.new_value is not None:
return {
- 'whatchanged': CHANGED_DUPLICATE_MARKER,
- 'oldvalue': str(self.old_value.id),
- 'newvalue': str(self.new_value.id),
- }
+ "whatchanged": CHANGED_DUPLICATE_MARKER,
+ "oldvalue": str(self.old_value.id),
+ "newvalue": str(self.new_value.id),
+ }
elif self.old_value is None:
return {
- 'whatchanged': MARKED_AS_DUPLICATE,
- 'newvalue': str(self.new_value.id),
- }
+ "whatchanged": MARKED_AS_DUPLICATE,
+ "newvalue": str(self.new_value.id),
+ }
elif self.new_value is None:
return {
- 'whatchanged': REMOVED_DUPLICATE_MARKER,
- 'oldvalue': str(self.old_value.id),
- }
+ "whatchanged": REMOVED_DUPLICATE_MARKER,
+ "oldvalue": str(self.old_value.id),
+ }
else:
raise AssertionError(
- "There is no change: both the old bug and new bug are None.")
+ "There is no change: both the old bug and new bug are None."
+ )
def getBugNotification(self):
if self.old_value is not None and self.new_value is not None:
if self.old_value.private:
old_value_text = (
"** This bug is no longer a duplicate of private bug "
- "%d" % self.old_value.id)
+ "%d" % self.old_value.id
+ )
else:
old_value_text = (
"** This bug is no longer a duplicate of bug %d\n"
- " %s" % (self.old_value.id, self.old_value.title))
+ " %s" % (self.old_value.id, self.old_value.title)
+ )
if self.new_value.private:
new_value_text = (
"** This bug has been marked a duplicate of private bug "
- "%d" % self.new_value.id)
+ "%d" % self.new_value.id
+ )
else:
new_value_text = (
"** This bug has been marked a duplicate of bug %d\n"
- " %s" % (self.new_value.id, self.new_value.title))
+ " %s" % (self.new_value.id, self.new_value.title)
+ )
text = "\n".join((old_value_text, new_value_text))
@@ -563,27 +597,32 @@ class BugDuplicateChange(AttributeChange):
if self.new_value.private:
text = (
"** This bug has been marked a duplicate of private bug "
- "%d" % self.new_value.id)
+ "%d" % self.new_value.id
+ )
else:
text = (
"** This bug has been marked a duplicate of bug %d\n"
- " %s" % (self.new_value.id, self.new_value.title))
+ " %s" % (self.new_value.id, self.new_value.title)
+ )
elif self.new_value is None:
if self.old_value.private:
text = (
"** This bug is no longer a duplicate of private bug "
- "%d" % self.old_value.id)
+ "%d" % self.old_value.id
+ )
else:
text = (
"** This bug is no longer a duplicate of bug %d\n"
- " %s" % (self.old_value.id, self.old_value.title))
+ " %s" % (self.old_value.id, self.old_value.title)
+ )
else:
raise AssertionError(
- "There is no change: both the old bug and new bug are None.")
+ "There is no change: both the old bug and new bug are None."
+ )
- return {'text': text}
+ return {"text": text}
class BugTitleChange(AttributeChange):
@@ -595,16 +634,19 @@ class BugTitleChange(AttributeChange):
# We return 'summary' instead of 'title' for title changes
# because the bug's title is referred to as its summary in the
# UI.
- activity['whatchanged'] = 'summary'
+ activity["whatchanged"] = "summary"
return activity
def getBugNotification(self):
- notification_text = dedent("""\
+ notification_text = dedent(
+ """\
** Summary changed:
- %s
- + %s""" % (self.old_value, self.new_value))
- return {'text': notification_text}
+ + %s"""
+ % (self.old_value, self.new_value)
+ )
+ return {"text": notification_text}
class BugInformationTypeChange(AttributeChange):
@@ -612,15 +654,16 @@ class BugInformationTypeChange(AttributeChange):
def getBugActivity(self):
return {
- 'newvalue': self.new_value.title,
- 'oldvalue': self.old_value.title,
- 'whatchanged': 'information type'
- }
+ "newvalue": self.new_value.title,
+ "oldvalue": self.old_value.title,
+ "whatchanged": "information type",
+ }
def getBugNotification(self):
return {
- 'text': "** Information type changed from %s to %s" % (
- self.old_value.title, self.new_value.title)}
+ "text": "** Information type changed from %s to %s"
+ % (self.old_value.title, self.new_value.title)
+ }
class BugTagsChange(AttributeChange):
@@ -633,10 +676,10 @@ class BugTagsChange(AttributeChange):
old_value = " ".join(sorted(set(self.old_value)))
return {
- 'newvalue': new_value,
- 'oldvalue': old_value,
- 'whatchanged': self.what_changed,
- }
+ "newvalue": new_value,
+ "oldvalue": old_value,
+ "whatchanged": self.what_changed,
+ }
def getBugNotification(self):
new_tags = set(self.new_value)
@@ -647,18 +690,17 @@ class BugTagsChange(AttributeChange):
messages = []
if len(removed_tags) > 0:
messages.append(
- "** Tags removed: %s" % " ".join(sorted(removed_tags)))
+ "** Tags removed: %s" % " ".join(sorted(removed_tags))
+ )
if len(added_tags) > 0:
- messages.append(
- "** Tags added: %s" % " ".join(sorted(added_tags)))
+ messages.append("** Tags added: %s" % " ".join(sorted(added_tags)))
- return {'text': "\n".join(messages)}
+ 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
+ return ProxiedLibraryFileAlias(attachment.libraryfile, attachment).http_url
class BugAttachmentChange(AttributeChange):
@@ -670,39 +712,45 @@ class BugAttachmentChange(AttributeChange):
old_value = None
new_value = "%s %s" % (
self.new_value.title,
- download_url_of_bugattachment(self.new_value))
+ download_url_of_bugattachment(self.new_value),
+ )
else:
what_changed = ATTACHMENT_REMOVED
old_value = "%s %s" % (
self.old_value.title,
- download_url_of_bugattachment(self.old_value))
+ download_url_of_bugattachment(self.old_value),
+ )
new_value = None
return {
- 'newvalue': new_value,
- 'oldvalue': old_value,
- 'whatchanged': what_changed,
- }
+ "newvalue": new_value,
+ "oldvalue": old_value,
+ "whatchanged": what_changed,
+ }
def getBugNotification(self):
if self.old_value is None:
if self.new_value.is_patch:
- attachment_str = 'Patch'
+ attachment_str = "Patch"
else:
- attachment_str = 'Attachment'
+ attachment_str = "Attachment"
message = '** %s added: "%s"\n %s' % (
- attachment_str, self.new_value.title,
- download_url_of_bugattachment(self.new_value))
+ attachment_str,
+ self.new_value.title,
+ download_url_of_bugattachment(self.new_value),
+ )
else:
if self.old_value.is_patch:
- attachment_str = 'Patch'
+ attachment_str = "Patch"
else:
- attachment_str = 'Attachment'
+ attachment_str = "Attachment"
message = '** %s removed: "%s"\n %s' % (
- attachment_str, self.old_value.title,
- download_url_of_bugattachment(self.old_value))
+ attachment_str,
+ self.old_value.title,
+ download_url_of_bugattachment(self.old_value),
+ )
- return {'text': message}
+ return {"text": message}
class CveLinkedToBug(BugChangeBase):
@@ -714,13 +762,11 @@ class CveLinkedToBug(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
- return dict(
- newvalue=self.cve.sequence,
- whatchanged=CVE_LINKED)
+ return dict(newvalue=self.cve.sequence, whatchanged=CVE_LINKED)
def getBugNotification(self):
"""See `IBugChange`."""
- return {'text': "** CVE added: %s" % self.cve.url}
+ return {"text": "** CVE added: %s" % self.cve.url}
class CveUnlinkedFromBug(BugChangeBase):
@@ -732,20 +778,17 @@ class CveUnlinkedFromBug(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
- return dict(
- oldvalue=self.cve.sequence,
- whatchanged=CVE_UNLINKED)
+ return dict(oldvalue=self.cve.sequence, whatchanged=CVE_UNLINKED)
def getBugNotification(self):
"""See `IBugChange`."""
- return {'text': "** CVE removed: %s" % self.cve.url}
+ return {"text": "** CVE removed: %s" % self.cve.url}
class BugLocked(BugChangeBase):
"""Used to represent the locking of a bug."""
- def __init__(self, when, person, old_status,
- new_status, reason, **kwargs):
+ def __init__(self, when, person, old_status, new_status, reason, **kwargs):
super().__init__(when, person)
self.old_status = old_status
self.new_status = new_status
@@ -754,12 +797,12 @@ class BugLocked(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange'."""
activity = dict(
- whatchanged='lock status',
+ whatchanged="lock status",
oldvalue=str(self.old_status),
newvalue=str(self.new_status),
)
if self.reason:
- activity['message'] = self.reason
+ activity["message"] = self.reason
return activity
@@ -769,9 +812,8 @@ class BugLocked(BugChangeBase):
if self.reason:
text = "{}: {}".format(text, self.reason)
- return {
- 'text': text
- }
+ return {"text": text}
+
class BugUnlocked(BugChangeBase):
"""Used to represent the unlocking of a bug."""
@@ -783,16 +825,14 @@ class BugUnlocked(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange'."""
return dict(
- whatchanged='lock status',
+ whatchanged="lock status",
newvalue=str(BugLockStatus.UNLOCKED),
oldvalue=str(self.old_status),
)
def getBugNotification(self):
"""See `IBugChange`."""
- return {
- 'text': "** Bug unlocked"
- }
+ return {"text": "** Bug unlocked"}
class BugLockReasonSet(BugChangeBase):
@@ -806,9 +846,9 @@ class BugLockReasonSet(BugChangeBase):
def getBugActivity(self):
"""See `IBugChange`."""
return dict(
- whatchanged='lock reason',
- oldvalue=self.old_reason if self.old_reason else 'unset',
- newvalue=self.new_reason if self.new_reason else 'unset',
+ whatchanged="lock reason",
+ oldvalue=self.old_reason if self.old_reason else "unset",
+ newvalue=self.new_reason if self.new_reason else "unset",
)
def getBugNotification(self):
@@ -816,11 +856,9 @@ class BugLockReasonSet(BugChangeBase):
if self.new_reason is None:
text = "Bug lock reason unset"
else:
- text = '** Bug lock reason changed: {}'.format(self.new_reason)
+ text = "** Bug lock reason changed: {}".format(self.new_reason)
- return {
- 'text': text
- }
+ return {"text": text}
class BugTaskAttributeChange(AttributeChange):
@@ -835,8 +873,9 @@ class BugTaskAttributeChange(AttributeChange):
sending notifications.
"""
- def __init__(self, bug_task, when, person, what_changed, old_value,
- new_value):
+ def __init__(
+ self, bug_task, when, person, what_changed, old_value, new_value
+ ):
super().__init__(when, person, what_changed, old_value, new_value)
self.bug_task = bug_task
@@ -844,13 +883,15 @@ class BugTaskAttributeChange(AttributeChange):
self.display_old_value = None
else:
self.display_old_value = getattr(
- self.old_value, self.display_attribute)
+ self.old_value, self.display_attribute
+ )
if self.new_value is None:
self.display_new_value = None
else:
self.display_new_value = getattr(
- self.new_value, self.display_attribute)
+ self.new_value, self.display_attribute
+ )
@property
def display_activity_label(self):
@@ -876,11 +917,11 @@ class BugTaskAttributeChange(AttributeChange):
target so as to make it clear in which task the change was made.
"""
return {
- 'whatchanged': '%s: %s' % (
- self.bug_task.bugtargetname, self.display_activity_label),
- 'oldvalue': self.display_old_value,
- 'newvalue': self.display_new_value,
- }
+ "whatchanged": "%s: %s"
+ % (self.bug_task.bugtargetname, self.display_activity_label),
+ "oldvalue": self.display_old_value,
+ "newvalue": self.display_new_value,
+ }
def getBugNotification(self):
"""Return the bug notification text for this change.
@@ -890,35 +931,38 @@ class BugTaskAttributeChange(AttributeChange):
"""
text = (
"** Changed in: %(bug_target_name)s\n"
- "%(label)13s: %(oldval)s => %(newval)s\n" % {
- 'bug_target_name': self.bug_task.bugtargetname,
- 'label': self.display_notification_label,
- 'oldval': self.display_old_value,
- 'newval': self.display_new_value,
- })
+ "%(label)13s: %(oldval)s => %(newval)s\n"
+ % {
+ "bug_target_name": self.bug_task.bugtargetname,
+ "label": self.display_notification_label,
+ "oldval": self.display_old_value,
+ "newval": self.display_new_value,
+ }
+ )
- return {'text': text.rstrip()}
+ return {"text": text.rstrip()}
class BugTaskImportanceChange(BugTaskAttributeChange):
"""Represents a change in BugTask.importance."""
# Use `importance.title` in activity records and notifications.
- display_attribute = 'title'
+ display_attribute = "title"
class BugTaskStatusChange(BugTaskAttributeChange):
"""Represents a change in BugTask.status."""
# Use `status.title` in activity records and notifications.
- display_attribute = 'title'
+ display_attribute = "title"
@property
def change_level(self):
"""See `IBugChange`."""
# Is bug being closed or reopened?
lifecycle_change = _is_status_change_lifecycle_change(
- self.old_value, self.new_value)
+ self.old_value, self.new_value
+ )
if lifecycle_change:
return BugNotificationLevel.LIFECYCLE
@@ -930,25 +974,26 @@ class BugTaskMilestoneChange(BugTaskAttributeChange):
"""Represents a change in BugTask.milestone."""
# Use `milestone.name` in activity records and notifications.
- display_attribute = 'name'
+ display_attribute = "name"
class BugTaskBugWatchChange(BugTaskAttributeChange):
"""Represents a change in BugTask.bugwatch."""
# Use the term "remote watch" as this is used in the UI.
- display_activity_label = 'remote watch'
- display_notification_label = 'Remote watch'
+ display_activity_label = "remote watch"
+ display_notification_label = "Remote watch"
# Use `bugwatch.title` in activity records and notifications.
- display_attribute = 'title'
+ display_attribute = "title"
class BugTaskAssigneeChange(AttributeChange):
"""Represents a change in BugTask.assignee."""
- def __init__(self, bug_task, when, person,
- what_changed, old_value, new_value):
+ def __init__(
+ self, bug_task, when, person, what_changed, old_value, new_value
+ ):
super().__init__(when, person, what_changed, old_value, new_value)
self.bug_task = bug_task
@@ -962,10 +1007,10 @@ class BugTaskAssigneeChange(AttributeChange):
return assignee.unique_displayname
return {
- 'whatchanged': '%s: assignee' % self.bug_task.bugtargetname,
- 'oldvalue': assignee_for_display(self.old_value),
- 'newvalue': assignee_for_display(self.new_value),
- }
+ "whatchanged": "%s: assignee" % self.bug_task.bugtargetname,
+ "oldvalue": assignee_for_display(self.old_value),
+ "newvalue": assignee_for_display(self.new_value),
+ }
def getBugNotification(self):
"""See `IBugChange`."""
@@ -977,13 +1022,16 @@ class BugTaskAssigneeChange(AttributeChange):
return assignee.unique_displayname
return {
- 'text': (
+ "text": (
"** Changed in: %s\n"
- " Assignee: %s => %s" % (
+ " Assignee: %s => %s"
+ % (
self.bug_task.bugtargetname,
assignee_for_display(self.old_value),
- assignee_for_display(self.new_value))),
- }
+ assignee_for_display(self.new_value),
+ )
+ ),
+ }
class BugTaskTargetChange(AttributeChange):
@@ -991,18 +1039,19 @@ class BugTaskTargetChange(AttributeChange):
change_level = BugNotificationLevel.LIFECYCLE
- def __init__(self, bug_task, when, person,
- what_changed, old_value, new_value):
+ def __init__(
+ self, bug_task, when, person, what_changed, old_value, new_value
+ ):
super().__init__(when, person, what_changed, old_value, new_value)
self.bug_task = bug_task
def getBugActivity(self):
"""See `IBugChange`."""
return {
- 'whatchanged': 'affects',
- 'oldvalue': self.old_value.bugtargetname,
- 'newvalue': self.new_value.bugtargetname,
- }
+ "whatchanged": "affects",
+ "oldvalue": self.old_value.bugtargetname,
+ "newvalue": self.new_value.bugtargetname,
+ }
def getBugNotification(self):
"""See `IBugChange`."""
@@ -1012,15 +1061,17 @@ class BugTaskTargetChange(AttributeChange):
template = "** Package changed: %s => %s"
text = template % (
self.old_value.bugtargetname,
- self.new_value.bugtargetname)
- return {'text': text}
+ self.new_value.bugtargetname,
+ )
+ return {"text": text}
class BugTaskImportanceExplanationChange(AttributeChange):
"""Represents a change in BugTask.importance_explanation."""
- def __init__(self, bug_task, when, person,
- what_changed, old_value, new_value):
+ def __init__(
+ self, bug_task, when, person, what_changed, old_value, new_value
+ ):
# XXX: lgp171188 2022-04-12: This is a sub-class of
# the `AttributeChange` class instead of
# the `BugTaskAttributeChange` class because the latter
@@ -1037,23 +1088,22 @@ class BugTaskImportanceExplanationChange(AttributeChange):
super().__init__(when, person, what_changed, old_value, new_value)
self.bug_task = bug_task
-
def getBugActivity(self):
"""See `IBugChange`."""
return {
- 'whatchanged': '%s: importance explanation' % (
- self.bug_task.bugtargetname,
- ),
- 'oldvalue': self.old_value or 'unset',
- 'newvalue': self.new_value or 'unset',
+ "whatchanged": "%s: importance explanation"
+ % (self.bug_task.bugtargetname,),
+ "oldvalue": self.old_value or "unset",
+ "newvalue": self.new_value or "unset",
}
def getBugNotification(self):
"""See `IBugChange`."""
return {
- 'text': (
+ "text": (
"** Changed in %s\n"
- " Importance explanation: %s => %s" % (
+ " Importance explanation: %s => %s"
+ % (
self.bug_task.bugtargetname,
self.old_value or "unset",
self.new_value or "unset",
@@ -1065,8 +1115,9 @@ class BugTaskImportanceExplanationChange(AttributeChange):
class BugTaskStatusExplanationChange(AttributeChange):
"""Represents a change in BugTask.status_explanation."""
- def __init__(self, bug_task, when, person,
- what_changed, old_value, new_value):
+ def __init__(
+ self, bug_task, when, person, what_changed, old_value, new_value
+ ):
# XXX: lgp171188 2022-04-12: This is a sub-class of
# the `AttributeChange` class instead of
# the `BugTaskAttributeChange` class because the latter
@@ -1086,44 +1137,44 @@ class BugTaskStatusExplanationChange(AttributeChange):
def getBugActivity(self):
"""See `IBugChange`."""
return {
- 'whatchanged': '%s: status explanation' % (
- self.bug_task.bugtargetname,
- ),
- 'oldvalue': self.old_value or 'unset',
- 'newvalue': self.new_value or 'unset',
+ "whatchanged": "%s: status explanation"
+ % (self.bug_task.bugtargetname,),
+ "oldvalue": self.old_value or "unset",
+ "newvalue": self.new_value or "unset",
}
def getBugNotification(self):
"""See `IBugChange`."""
return {
- 'text': (
- '** Changed in %s\n'
- ' Status explanation: %s => %s' % (
+ "text": (
+ "** Changed in %s\n"
+ " Status explanation: %s => %s"
+ % (
self.bug_task.bugtargetname,
- self.old_value or 'unset',
- self.new_value or 'unset',
+ self.old_value or "unset",
+ self.new_value or "unset",
)
)
}
BUG_CHANGE_LOOKUP = {
- 'description': BugDescriptionChange,
- 'information_type': BugInformationTypeChange,
- 'tags': BugTagsChange,
- 'title': BugTitleChange,
- 'attachment': BugAttachmentChange,
- 'duplicateof': BugDuplicateChange,
- }
+ "description": BugDescriptionChange,
+ "information_type": BugInformationTypeChange,
+ "tags": BugTagsChange,
+ "title": BugTitleChange,
+ "attachment": BugAttachmentChange,
+ "duplicateof": BugDuplicateChange,
+}
BUGTASK_CHANGE_LOOKUP = {
- 'importance': BugTaskImportanceChange,
- 'status': BugTaskStatusChange,
- 'target': BugTaskTargetChange,
- 'milestone': BugTaskMilestoneChange,
- 'bugwatch': BugTaskBugWatchChange,
- 'assignee': BugTaskAssigneeChange,
- 'importance_explanation': BugTaskImportanceExplanationChange,
- 'status_explanation': BugTaskStatusExplanationChange,
- }
+ "importance": BugTaskImportanceChange,
+ "status": BugTaskStatusChange,
+ "target": BugTaskTargetChange,
+ "milestone": BugTaskMilestoneChange,
+ "bugwatch": BugTaskBugWatchChange,
+ "assignee": BugTaskAssigneeChange,
+ "importance_explanation": BugTaskImportanceExplanationChange,
+ "status_explanation": BugTaskStatusExplanationChange,
+}
diff --git a/lib/lp/bugs/adapters/bugdelta.py b/lib/lp/bugs/adapters/bugdelta.py
index 8d8268c..f0f31a0 100644
--- a/lib/lp/bugs/adapters/bugdelta.py
+++ b/lib/lp/bugs/adapters/bugdelta.py
@@ -12,13 +12,27 @@ from lp.bugs.interfaces.bug import IBugDelta
class BugDelta:
"""See `IBugDelta`."""
- def __init__(self, bug, bugurl, user,
- title=None, description=None, name=None,
- private=None, security_related=None, information_type=None,
- duplicateof=None, external_reference=None, bugwatch=None,
- cve=None, attachment=None, tags=None,
- added_bugtasks=None, bugtask_deltas=None,
- bug_before_modification=None):
+ def __init__(
+ self,
+ bug,
+ bugurl,
+ user,
+ title=None,
+ description=None,
+ name=None,
+ private=None,
+ security_related=None,
+ information_type=None,
+ duplicateof=None,
+ external_reference=None,
+ bugwatch=None,
+ cve=None,
+ attachment=None,
+ tags=None,
+ added_bugtasks=None,
+ bugtask_deltas=None,
+ bug_before_modification=None,
+ ):
self.bug = bug
self.bug_before_modification = bug_before_modification
self.bugurl = bugurl
diff --git a/lib/lp/bugs/adapters/bugsubscriptionfilter.py b/lib/lp/bugs/adapters/bugsubscriptionfilter.py
index ba6f3c4..17b6238 100644
--- a/lib/lp/bugs/adapters/bugsubscriptionfilter.py
+++ b/lib/lp/bugs/adapters/bugsubscriptionfilter.py
@@ -4,9 +4,9 @@
"""Adapt IStructuralSubscription to other types."""
__all__ = [
- 'bugsubscriptionfilter_to_distribution',
- 'bugsubscriptionfilter_to_product',
- ]
+ "bugsubscriptionfilter_to_distribution",
+ "bugsubscriptionfilter_to_product",
+]
def bugsubscriptionfilter_to_distribution(bug_subscription_filter):
diff --git a/lib/lp/bugs/adapters/tests/test_bugchange.py b/lib/lp/bugs/adapters/tests/test_bugchange.py
index e2af8df..6a8c3b9 100644
--- a/lib/lp/bugs/adapters/tests/test_bugchange.py
+++ b/lib/lp/bugs/adapters/tests/test_bugchange.py
@@ -8,7 +8,7 @@ from lp.bugs.adapters.bugchange import (
BugDescriptionChange,
get_bug_change_class,
get_bug_changes,
- )
+)
from lp.bugs.adapters.bugdelta import BugDelta
from lp.bugs.enums import BugNotificationLevel
from lp.bugs.interfaces.bugtask import BugTaskStatus
@@ -34,9 +34,11 @@ class BugChangeTestCase(TestCaseWithFactory):
expected = BUG_CHANGE_LOOKUP[field_name]
received = get_bug_change_class(bug, field_name)
self.assertEqual(
- expected, received,
+ expected,
+ received,
"Expected %s from get_bug_change_class() for field name %s. "
- "Got %s." % (expected, field_name, received))
+ "Got %s." % (expected, field_name, received),
+ )
class BugChangeLevelTestCase(TestCaseWithFactory):
@@ -53,19 +55,18 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
if user is None:
user = self.user
return BugDelta(
- bug=self.bug,
- bugurl=canonical_url(self.bug),
- user=user,
- **kwargs)
+ bug=self.bug, bugurl=canonical_url(self.bug), user=user, **kwargs
+ )
def test_change_level_metadata_description(self):
# Changing a bug description is considered to have change_level
# of BugNotificationLevel.METADATA.
bug_delta = self.createDelta(
description={
- 'new': 'new description',
- 'old': self.bug.description,
- })
+ "new": "new description",
+ "old": self.bug.description,
+ }
+ )
change = list(get_bug_changes(bug_delta))[0]
self.assertTrue(isinstance(change, BugDescriptionChange))
@@ -77,11 +78,11 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
bugtask_delta = BugTaskDelta(
bugtask=self.bugtask,
status={
- 'old': BugTaskStatus.NEW,
- 'new': BugTaskStatus.FIXRELEASED,
- })
- bug_delta = self.createDelta(
- bugtask_deltas=bugtask_delta)
+ "old": BugTaskStatus.NEW,
+ "new": BugTaskStatus.FIXRELEASED,
+ },
+ )
+ bug_delta = self.createDelta(bugtask_deltas=bugtask_delta)
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.LIFECYCLE, change.change_level)
@@ -92,11 +93,11 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
bugtask_delta = BugTaskDelta(
bugtask=self.bugtask,
status={
- 'old': BugTaskStatus.FIXRELEASED,
- 'new': BugTaskStatus.TRIAGED,
- })
- bug_delta = self.createDelta(
- bugtask_deltas=bugtask_delta)
+ "old": BugTaskStatus.FIXRELEASED,
+ "new": BugTaskStatus.TRIAGED,
+ },
+ )
+ bug_delta = self.createDelta(bugtask_deltas=bugtask_delta)
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.LIFECYCLE, change.change_level)
@@ -107,11 +108,11 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
bugtask_delta = BugTaskDelta(
bugtask=self.bugtask,
status={
- 'old': BugTaskStatus.TRIAGED,
- 'new': BugTaskStatus.FIXCOMMITTED,
- })
- bug_delta = self.createDelta(
- bugtask_deltas=bugtask_delta)
+ "old": BugTaskStatus.TRIAGED,
+ "new": BugTaskStatus.FIXCOMMITTED,
+ },
+ )
+ bug_delta = self.createDelta(bugtask_deltas=bugtask_delta)
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.METADATA, change.change_level)
@@ -122,11 +123,11 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
bugtask_delta = BugTaskDelta(
bugtask=self.bugtask,
status={
- 'old': BugTaskStatus.OPINION,
- 'new': BugTaskStatus.WONTFIX,
- })
- bug_delta = self.createDelta(
- bugtask_deltas=bugtask_delta)
+ "old": BugTaskStatus.OPINION,
+ "new": BugTaskStatus.WONTFIX,
+ },
+ )
+ bug_delta = self.createDelta(bugtask_deltas=bugtask_delta)
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.METADATA, change.change_level)
@@ -136,13 +137,15 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
# simple BugNotificationLevel.METADATA change.
duplicate_of = self.factory.makeBug()
duplicate_of.default_bugtask.transitionToStatus(
- BugTaskStatus.NEW, self.user)
+ BugTaskStatus.NEW, self.user
+ )
bug_delta = self.createDelta(
user=self.bug.owner,
duplicateof={
- 'old': None,
- 'new': duplicate_of,
- })
+ "old": None,
+ "new": duplicate_of,
+ },
+ )
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.METADATA, change.change_level)
@@ -152,13 +155,15 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
# a BugNotificationLevel.LIFECYCLE change.
duplicate_of = self.factory.makeBug()
duplicate_of.default_bugtask.transitionToStatus(
- BugTaskStatus.FIXRELEASED, self.user)
+ BugTaskStatus.FIXRELEASED, self.user
+ )
bug_delta = self.createDelta(
user=self.bug.owner,
duplicateof={
- 'old': None,
- 'new': duplicate_of,
- })
+ "old": None,
+ "new": duplicate_of,
+ },
+ )
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.LIFECYCLE, change.change_level)
@@ -168,13 +173,15 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
# simple BugNotificationLevel.METADATA change.
duplicate_of = self.factory.makeBug()
duplicate_of.default_bugtask.transitionToStatus(
- BugTaskStatus.NEW, self.user)
+ BugTaskStatus.NEW, self.user
+ )
bug_delta = self.createDelta(
user=self.bug.owner,
duplicateof={
- 'new': None,
- 'old': duplicate_of,
- })
+ "new": None,
+ "old": duplicate_of,
+ },
+ )
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.METADATA, change.change_level)
@@ -184,12 +191,11 @@ class BugChangeLevelTestCase(TestCaseWithFactory):
# a BugNotificationLevel.LIFECYCLE change.
duplicate_of = self.factory.makeBug()
duplicate_of.default_bugtask.transitionToStatus(
- BugTaskStatus.FIXRELEASED, self.user)
+ BugTaskStatus.FIXRELEASED, self.user
+ )
bug_delta = self.createDelta(
- user=self.bug.owner,
- duplicateof={
- 'new': None,
- 'old': duplicate_of})
+ user=self.bug.owner, duplicateof={"new": None, "old": duplicate_of}
+ )
change = list(get_bug_changes(bug_delta))[0]
self.assertEqual(BugNotificationLevel.LIFECYCLE, change.change_level)
diff --git a/lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py b/lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py
index 3a4f5fc..b17527f 100644
--- a/lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py
+++ b/lib/lp/bugs/adapters/tests/test_bugsubscriptionfilter.py
@@ -6,7 +6,7 @@
from lp.bugs.adapters.bugsubscriptionfilter import (
bugsubscriptionfilter_to_distribution,
bugsubscriptionfilter_to_product,
- )
+)
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.product import IProduct
from lp.testing import TestCaseWithFactory
@@ -20,35 +20,43 @@ class BugSubscriptionFilterTestCase(TestCaseWithFactory):
def test_bugsubscriptionfilter_to_product_with_product(self):
product = self.factory.makeProduct()
subscription_filter = self.factory.makeBugSubscriptionFilter(
- target=product)
+ target=product
+ )
self.assertEqual(
- product, bugsubscriptionfilter_to_product(subscription_filter))
+ product, bugsubscriptionfilter_to_product(subscription_filter)
+ )
self.assertEqual(product, IProduct(subscription_filter))
def test_bugsubscriptionfilter_to_product_with_productseries(self):
product = self.factory.makeProduct()
series = product.development_focus
subscription_filter = self.factory.makeBugSubscriptionFilter(
- target=series)
+ target=series
+ )
self.assertEqual(
- product, bugsubscriptionfilter_to_product(subscription_filter))
+ product, bugsubscriptionfilter_to_product(subscription_filter)
+ )
self.assertEqual(product, IProduct(subscription_filter))
def test_bugsubscriptionfilter_to_distribution_with_distribution(self):
distribution = self.factory.makeDistribution()
subscription_filter = self.factory.makeBugSubscriptionFilter(
- distribution)
+ distribution
+ )
self.assertEqual(
distribution,
- bugsubscriptionfilter_to_distribution(subscription_filter))
+ bugsubscriptionfilter_to_distribution(subscription_filter),
+ )
self.assertEqual(distribution, IDistribution(subscription_filter))
def test_bugsubscriptionfilter_to_distroseries_with_distribution(self):
distribution = self.factory.makeDistribution()
series = self.factory.makeDistroSeries(distribution=distribution)
subscription_filter = self.factory.makeBugSubscriptionFilter(
- target=series)
+ target=series
+ )
self.assertEqual(
distribution,
- bugsubscriptionfilter_to_distribution(subscription_filter))
+ bugsubscriptionfilter_to_distribution(subscription_filter),
+ )
self.assertEqual(distribution, IDistribution(subscription_filter))
diff --git a/lib/lp/bugs/adapters/treelookup.py b/lib/lp/bugs/adapters/treelookup.py
index eebb7e2..796fe94 100644
--- a/lib/lp/bugs/adapters/treelookup.py
+++ b/lib/lp/bugs/adapters/treelookup.py
@@ -31,9 +31,9 @@ on the eye, makes debugging easier, and means they can be customised.
"""
__all__ = [
- 'LookupBranch',
- 'LookupTree',
- ]
+ "LookupBranch",
+ "LookupTree",
+]
import copy
import string
@@ -98,18 +98,19 @@ class LookupBranch:
This is mainly intended as an aid to development.
"""
- format = 'branch(%s => %%s)'
+ format = "branch(%s => %%s)"
if self.is_default:
- format = format % '*'
+ format = format % "*"
else:
- format = format % ', '.join(
- self._describe_key(key) for key in self.keys)
+ format = format % ", ".join(
+ self._describe_key(key) for key in self.keys
+ )
if self.is_leaf:
return format % self._describe_result(self.result)
else:
return format % self.result.describe(level)
- _describe_key_chars = set(string.ascii_letters + string.digits + '-_+=*')
+ _describe_key_chars = set(string.ascii_letters + string.digits + "-_+=*")
def _describe_key(self, key):
"""Return a pretty representation of a simple key.
@@ -133,9 +134,10 @@ class LookupBranch:
def __repr__(self):
"""A machine-readable representation of this branch."""
- return '%s(%s)' % (
+ return "%s(%s)" % (
self.__class__.__name__,
- ', '.join(repr(item) for item in (self.keys + (self.result,))))
+ ", ".join(repr(item) for item in (self.keys + (self.result,))),
+ )
class LookupTree:
@@ -182,8 +184,8 @@ class LookupTree:
continue
branch = copy.copy(branch)
branch.keys = tuple(
- key for key in branch.keys
- if key not in prune)
+ key for key in branch.keys if key not in prune
+ )
pruned_branches.append(branch)
seen_keys.update(branch.keys)
@@ -202,9 +204,9 @@ class LookupTree:
default = False
for branch in self.branches:
if not isinstance(branch, LookupBranch):
- raise TypeError('Not a LookupBranch: %r' % (branch,))
+ raise TypeError("Not a LookupBranch: %r" % (branch,))
if default:
- raise TypeError('Default branch must be last.')
+ raise TypeError("Default branch must be last.")
default = branch.is_default
def find(self, key, *more):
@@ -271,15 +273,18 @@ class LookupTree:
This is mainly intended as an aid to development.
"""
- indent = ' ' * level
- format = indent + '%s'
- return 'tree(\n%s\n%s)' % (
- '\n'.join(format % branch.describe(level + 1)
- for branch in self.branches),
- indent)
+ indent = " " * level
+ format = indent + "%s"
+ return "tree(\n%s\n%s)" % (
+ "\n".join(
+ format % branch.describe(level + 1) for branch in self.branches
+ ),
+ indent,
+ )
def __repr__(self):
"""A machine-readable representation of this tree."""
- return '%s(%s)' % (
+ return "%s(%s)" % (
self.__class__.__name__,
- ', '.join(repr(branch) for branch in self.branches))
+ ", ".join(repr(branch) for branch in self.branches),
+ )
diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py
index 1759a8e..e3f3361 100644
--- a/lib/lp/bugs/browser/bug.py
+++ b/lib/lp/bugs/browser/bug.py
@@ -4,70 +4,54 @@
"""IBug related view classes."""
__all__ = [
- 'BugActivity',
- 'BugContextMenu',
- 'BugEditView',
- 'BugInformationTypePortletView',
- 'BugLockStatusEditView',
- 'BugMarkAsAffectingUserView',
- 'BugMarkAsDuplicateView',
- 'BugNavigation',
- 'BugSecrecyEditView',
- 'BugSetNavigation',
- 'BugSubscriptionPortletDetails',
- 'BugTextView',
- 'BugURL',
- 'BugView',
- 'BugViewMixin',
- 'BugWithoutContextView',
- 'DeprecatedAssignedBugsView',
- 'MaloneView',
- ]
+ "BugActivity",
+ "BugContextMenu",
+ "BugEditView",
+ "BugInformationTypePortletView",
+ "BugLockStatusEditView",
+ "BugMarkAsAffectingUserView",
+ "BugMarkAsDuplicateView",
+ "BugNavigation",
+ "BugSecrecyEditView",
+ "BugSetNavigation",
+ "BugSubscriptionPortletDetails",
+ "BugTextView",
+ "BugURL",
+ "BugView",
+ "BugViewMixin",
+ "BugWithoutContextView",
+ "DeprecatedAssignedBugsView",
+ "MaloneView",
+]
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
import json
import re
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
-from lazr.enum import (
- EnumeratedType,
- Item,
- )
+from lazr.enum import EnumeratedType, Item
from lazr.lifecycle.event import ObjectModifiedEvent
from lazr.lifecycle.snapshot import Snapshot
-from lazr.restful import (
- EntryResource,
- ResourceJSONEncoder,
- )
+from lazr.restful import EntryResource, ResourceJSONEncoder
from lazr.restful.interface import copy_field
from lazr.restful.interfaces import IJSONRequestCache
from zope import formlib
-from zope.component import (
- getMultiAdapter,
- getUtility,
- )
+from zope.component import getMultiAdapter, getUtility
from zope.event import notify
from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import TextWidget
-from zope.interface import (
- implementer,
- Interface,
- providedBy,
- )
+from zope.interface import Interface, implementer, providedBy
from zope.publisher.defaultview import getDefaultViewName
-from zope.schema import (
- Bool,
- Choice,
- )
+from zope.schema import Bool, Choice
from zope.security.proxy import removeSecurityProxy
from lp import _
from lp.app.browser.informationtype import InformationTypePortletMixin
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
- )
+ action,
+)
from lp.app.enums import PRIVATE_INFORMATION_TYPES
from lp.app.errors import NotFoundError
from lp.app.interfaces.services import IService
@@ -77,34 +61,25 @@ from lp.app.widgets.product import GhostCheckBoxWidget
from lp.app.widgets.project import ProjectScopeWidget
from lp.bugs.browser.bugsubscription import BugPortletSubscribersWithDetails
from lp.bugs.browser.widgets.bug import BugTagsWidget
-from lp.bugs.enums import (
- BugLockStatus,
- BugNotificationLevel,
- )
-from lp.bugs.interfaces.bug import (
- IBug,
- IBugSet,
- )
+from lp.bugs.enums import BugLockStatus, BugNotificationLevel
+from lp.bugs.interfaces.bug import IBug, IBugSet
from lp.bugs.interfaces.bugattachment import (
BugAttachmentType,
IBugAttachmentSet,
- )
+)
from lp.bugs.interfaces.bugnomination import IBugNominationSet
-from lp.bugs.interfaces.bugtask import (
- BugTaskStatus,
- IBugTask,
- )
+from lp.bugs.interfaces.bugtask import BugTaskStatus, IBugTask
from lp.bugs.interfaces.bugtasksearch import (
BugTaskSearchParams,
IFrontPageBugTaskSearch,
- )
+)
from lp.bugs.interfaces.bugwatch import IBugWatchSet
from lp.bugs.interfaces.cve import ICveSet
from lp.bugs.mail.bugnotificationbuilder import format_rfc2822_date
from lp.bugs.model.personsubscriptioninfo import PersonSubscriptions
from lp.bugs.model.structuralsubscription import (
get_structural_subscriptions_for_bug,
- )
+)
from lp.registry.interfaces.person import IPersonSet
from lp.services.compat import message_as_bytes
from lp.services.features import getFeatureFlag
@@ -112,34 +87,29 @@ 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,
- )
+from lp.services.searchbuilder import any, not_equals
from lp.services.webapp import (
- canonical_url,
ContextMenu,
- enabled_with_permission,
LaunchpadView,
Link,
Navigation,
+ canonical_url,
+ enabled_with_permission,
stepthrough,
structured,
- )
+)
from lp.services.webapp.authorization import (
check_permission,
precache_permission_for_objects,
- )
-from lp.services.webapp.interfaces import (
- ICanonicalUrlData,
- ILaunchBag,
- )
+)
+from lp.services.webapp.interfaces import ICanonicalUrlData, ILaunchBag
from lp.services.webapp.publisher import RedirectionView
from lp.services.webapp.snapshot import notify_modified
class BugNavigation(Navigation):
"""Navigation for the `IBug`."""
+
# It would be easier, since there is no per-bug sequence for a BugWatch
# and we have to leak the BugWatch.id anyway, to hang bugwatches off a
# global /bugwatchs/nnnn
@@ -153,21 +123,21 @@ class BugNavigation(Navigation):
usedfor = IBug
- @stepthrough('+watch')
+ @stepthrough("+watch")
def traverse_watches(self, name):
"""Retrieve a BugWatch by name."""
if name.isdigit():
# in future this should look up by (bug.id, watch.seqnum)
return getUtility(IBugWatchSet).get(int(name))
- @stepthrough('+subscription')
+ @stepthrough("+subscription")
def traverse_subscriptions(self, person_name):
"""Retrieve a BugSubscription by person name."""
for subscription in self.context.subscriptions:
if subscription.person.name == person_name:
return subscription
- @stepthrough('attachments')
+ @stepthrough("attachments")
def traverse_attachments(self, name):
"""Retrieve a BugAttachment by ID.
@@ -177,10 +147,10 @@ class BugNavigation(Navigation):
attachment = getUtility(IBugAttachmentSet)[name]
if attachment is not None and attachment.bug == self.context:
return self.redirectSubTree(
- canonical_url(attachment, request=self.request),
- status=301)
+ canonical_url(attachment, request=self.request), status=301
+ )
- @stepthrough('+attachment')
+ @stepthrough("+attachment")
def traverse_attachment(self, name):
"""Retrieve a BugAttachment by ID.
@@ -191,7 +161,7 @@ class BugNavigation(Navigation):
if attachment is not None and attachment.bug == self.context:
return attachment
- @stepthrough('nominations')
+ @stepthrough("nominations")
def traverse_nominations(self, nomination_id):
"""Traverse to a nomination by id."""
if nomination_id.isdigit():
@@ -203,9 +173,10 @@ class BugNavigation(Navigation):
class BugSetNavigation(Navigation):
"""Navigation for the IBugSet."""
+
usedfor = IBugSet
- @stepthrough('+text')
+ @stepthrough("+text")
def text(self, name):
"""Retrieve a bug by name."""
try:
@@ -216,13 +187,29 @@ class BugSetNavigation(Navigation):
class BugContextMenu(ContextMenu):
"""Context menu of actions that can be performed upon a Bug."""
+
usedfor = IBug
links = [
- 'editdescription', 'markduplicate', 'visibility', 'addupstream',
- 'adddistro', 'subscription', 'addsubscriber', 'editsubscriptions',
- 'addcomment', 'nominate', 'addbranch', 'linktocve', 'unlinkcve',
- 'createquestion', 'mute_subscription', 'removequestion',
- 'activitylog', 'affectsmetoo', 'change_lock_status']
+ "editdescription",
+ "markduplicate",
+ "visibility",
+ "addupstream",
+ "adddistro",
+ "subscription",
+ "addsubscriber",
+ "editsubscriptions",
+ "addcomment",
+ "nominate",
+ "addbranch",
+ "linktocve",
+ "unlinkcve",
+ "createquestion",
+ "mute_subscription",
+ "removequestion",
+ "activitylog",
+ "affectsmetoo",
+ "change_lock_status",
+ ]
def __init__(self, context):
# Always force the context to be the current bugtask, so that we don't
@@ -230,166 +217,187 @@ class BugContextMenu(ContextMenu):
ContextMenu.__init__(self, getUtility(ILaunchBag).bugtask)
@cachedproperty
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def editdescription(self):
"""Return the 'Edit description/tags' Link."""
- text = 'Update description / tags'
- return Link('+edit', text, icon='edit')
+ text = "Update description / tags"
+ return Link("+edit", text, icon="edit")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def visibility(self):
"""Return the 'Set privacy/security' Link."""
- text = 'Change privacy/security'
- return Link('+secrecy', text)
+ text = "Change privacy/security"
+ return Link("+secrecy", text)
- @enabled_with_permission('launchpad.Moderate')
+ @enabled_with_permission("launchpad.Moderate")
def change_lock_status(self):
"""Return the 'Change lock status' Link."""
- text = 'Change lock status'
- return Link('+lock-status', text, icon='edit')
+ text = "Change lock status"
+ return Link("+lock-status", text, icon="edit")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def markduplicate(self):
"""Return the 'Mark as duplicate' Link."""
- return Link('+duplicate', 'Mark as duplicate')
+ return Link("+duplicate", "Mark as duplicate")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def addupstream(self):
"""Return the 'Also affects project' Link."""
- text = 'Also affects project'
- return Link('+choose-affected-product', text, icon='add')
+ text = "Also affects project"
+ return Link("+choose-affected-product", text, icon="add")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def adddistro(self):
"""Return the 'Also affects distribution/package' Link."""
- text = 'Also affects distribution/package'
- return Link('+distrotask', text, icon='add')
+ text = "Also affects distribution/package"
+ return Link("+distrotask", text, icon="add")
def subscription(self):
"""Return the 'Subscribe/Unsubscribe' Link."""
user = getUtility(ILaunchBag).user
if user is None:
- text = 'Subscribe/Unsubscribe'
- icon = 'edit'
+ text = "Subscribe/Unsubscribe"
+ icon = "edit"
elif user is not None and (
- self.context.bug.isSubscribed(user) or
- self.context.bug.isSubscribedToDupes(user)):
+ self.context.bug.isSubscribed(user)
+ or self.context.bug.isSubscribedToDupes(user)
+ ):
if self.context.bug.isMuted(user):
- text = 'Subscribe'
- icon = 'add'
+ text = "Subscribe"
+ icon = "add"
else:
- text = 'Edit subscription'
- icon = 'edit'
+ text = "Edit subscription"
+ icon = "edit"
else:
- text = 'Subscribe'
- icon = 'add'
- return Link('+subscribe', text, icon=icon, summary=(
- 'When you are subscribed, Launchpad will email you each time '
- 'this bug changes'))
+ text = "Subscribe"
+ icon = "add"
+ return Link(
+ "+subscribe",
+ text,
+ icon=icon,
+ summary=(
+ "When you are subscribed, Launchpad will email you each time "
+ "this bug changes"
+ ),
+ )
def addsubscriber(self):
"""Return the 'Subscribe someone else' Link."""
- text = 'Subscribe someone else'
+ text = "Subscribe someone else"
return Link(
- '+addsubscriber', text, icon='add', summary=(
- 'Launchpad will email that person whenever this bugs '
- 'changes'))
+ "+addsubscriber",
+ text,
+ icon="add",
+ summary=(
+ "Launchpad will email that person whenever this bugs "
+ "changes"
+ ),
+ )
def editsubscriptions(self):
"""Return the 'Edit subscriptions' Link."""
- text = 'Edit bug mail'
+ text = "Edit bug mail"
return Link(
- '+subscriptions', text, icon='edit', summary=(
- 'View and change your subscriptions to this bug'))
+ "+subscriptions",
+ text,
+ icon="edit",
+ summary=("View and change your subscriptions to this bug"),
+ )
def mute_subscription(self):
"""Return the 'Mute subscription' Link."""
user = getUtility(ILaunchBag).user
if self.context.bug.isMuted(user):
text = "Unmute bug mail"
- icon = 'unmute'
+ icon = "unmute"
summary = (
- 'Unmute this bug so that you will receive emails about it.')
+ "Unmute this bug so that you will receive emails about it."
+ )
else:
text = "Mute bug mail"
- icon = 'mute'
+ icon = "mute"
summary = (
- 'Mute this bug so that you will not receive emails about it.')
+ "Mute this bug so that you will not receive emails about it."
+ )
- return Link('+mute', text, icon=icon, summary=summary)
+ return Link("+mute", text, icon=icon, summary=summary)
def nominate(self):
"""Return the 'Target/Nominate for series' Link."""
launchbag = getUtility(ILaunchBag)
target = launchbag.product or launchbag.distribution
if check_permission("launchpad.Driver", target) or (
- getFeatureFlag('bugs.nominations.bug_supervisors_can_target')
- and check_permission("launchpad.BugSupervisor", target)):
+ getFeatureFlag("bugs.nominations.bug_supervisors_can_target")
+ and check_permission("launchpad.BugSupervisor", target)
+ ):
text = "Target to series"
- return Link('+nominate', text, icon='milestone')
- elif (check_permission("launchpad.BugSupervisor", target) or
- self.user is None):
- text = 'Nominate for series'
- return Link('+nominate', text, icon='milestone')
+ return Link("+nominate", text, icon="milestone")
+ elif (
+ check_permission("launchpad.BugSupervisor", target)
+ or self.user is None
+ ):
+ text = "Nominate for series"
+ return Link("+nominate", text, icon="milestone")
else:
- return Link('+nominate', '', enabled=False, icon='milestone')
+ return Link("+nominate", "", enabled=False, icon="milestone")
- @enabled_with_permission('launchpad.Append')
+ @enabled_with_permission("launchpad.Append")
def addcomment(self):
"""Return the 'Comment or attach file' Link."""
- text = 'Add attachment or patch'
- return Link('+addcomment', text, icon='add')
+ text = "Add attachment or patch"
+ return Link("+addcomment", text, icon="add")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def addbranch(self):
"""Return the 'Add branch' Link."""
- text = 'Link a related branch'
- return Link('+addbranch', text, icon='add')
+ text = "Link a related branch"
+ return Link("+addbranch", text, icon="add")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def linktocve(self):
"""Return the 'Link to CVE' Link."""
text = structured(
- 'Link to '
+ "Link to "
'<abbr title="Common Vulnerabilities and Exposures Index">'
- 'CVE'
- '</abbr>')
- return Link('+linkcve', text, icon='add')
+ "CVE"
+ "</abbr>"
+ )
+ return Link("+linkcve", text, icon="add")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def unlinkcve(self):
"""Return 'Remove CVE link' Link."""
enabled = self.context.bug.has_cves
- text = 'Remove CVE link'
- return Link('+unlinkcve', text, icon='remove', enabled=enabled)
+ text = "Remove CVE link"
+ return Link("+unlinkcve", text, icon="remove", enabled=enabled)
@property
def _bug_question(self):
return self.context.bug.getQuestionCreatedFromBug()
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def createquestion(self):
"""Create a question from this bug."""
- text = 'Convert to a question'
+ text = "Convert to a question"
enabled = self._bug_question is None
- return Link('+create-question', text, enabled=enabled, icon='add')
+ return Link("+create-question", text, enabled=enabled, icon="add")
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def removequestion(self):
"""Remove the created question from this bug."""
- text = 'Convert back to a bug'
+ text = "Convert back to a bug"
enabled = self._bug_question is not None
- return Link('+remove-question', text, enabled=enabled, icon='remove')
+ return Link("+remove-question", text, enabled=enabled, icon="remove")
def activitylog(self):
"""Return the 'Activity log' Link."""
- text = 'See full activity log'
- return Link('+activity', text)
+ text = "See full activity log"
+ return Link("+activity", text)
def affectsmetoo(self):
"""Return the 'This bug affects me too' link."""
enabled = getUtility(ILaunchBag).user is not None
- return Link('+affectsmetoo', 'change', enabled=enabled)
+ return Link("+affectsmetoo", "change", enabled=enabled)
class MaloneView(LaunchpadFormView):
@@ -398,25 +406,25 @@ class MaloneView(LaunchpadFormView):
custom_widget_searchtext = CustomWidgetFactory(TextWidget, displayWidth=50)
custom_widget_scope = ProjectScopeWidget
schema = IFrontPageBugTaskSearch
- field_names = ['searchtext', 'scope']
+ field_names = ["searchtext", "scope"]
# Test: standalone/xx-slash-malone-slash-bugs.rst
error_message = None
- page_title = 'Launchpad Bugs'
+ page_title = "Launchpad Bugs"
@property
def target_css_class(self):
"""The CSS class for used in the target widget."""
if self.target_error:
- return 'error'
+ return "error"
else:
return None
@property
def target_error(self):
"""The error message for the target widget."""
- return self.getFieldError('scope')
+ return self.getFieldError("scope")
def initialize(self):
"""Initialize the view to handle the request."""
@@ -424,7 +432,7 @@ class MaloneView(LaunchpadFormView):
bug_id = self.request.form.get("id")
if bug_id:
self._redirectToBug(bug_id)
- elif self.widgets['scope'].hasInput():
+ elif self.widgets["scope"].hasInput():
self._validate(action=None, data={})
def _redirectToBug(self, bug_id):
@@ -446,17 +454,22 @@ class MaloneView(LaunchpadFormView):
def most_recently_fixed_bugs(self):
"""Return the five most recently fixed bugs."""
params = BugTaskSearchParams(
- self.user, status=BugTaskStatus.FIXRELEASED,
- date_closed=not_equals(None), orderby='-date_closed')
+ self.user,
+ status=BugTaskStatus.FIXRELEASED,
+ date_closed=not_equals(None),
+ orderby="-date_closed",
+ )
return getUtility(IBugSet).getDistinctBugsForBugTasks(
- self.context.searchTasks(params), self.user, limit=5)
+ self.context.searchTasks(params), self.user, limit=5
+ )
@property
def most_recently_reported_bugs(self):
"""Return the five most recently reported bugs."""
- params = BugTaskSearchParams(self.user, orderby='-datecreated')
+ params = BugTaskSearchParams(self.user, orderby="-datecreated")
return getUtility(IBugSet).getDistinctBugsForBugTasks(
- self.context.searchTasks(params), self.user, limit=5)
+ self.context.searchTasks(params), self.user, limit=5
+ )
def getCveBugLinkCount(self):
"""Return the number of links between bugs and CVEs there are."""
@@ -472,7 +485,8 @@ class BugViewMixin:
if self.context.duplicateof is not None:
naked_duplicate = removeSecurityProxy(self.context.duplicateof)
active = getattr(
- naked_duplicate.default_bugtask.target, 'active', True)
+ naked_duplicate.default_bugtask.target, "active", True
+ )
return active
@cachedproperty
@@ -499,14 +513,14 @@ class BugViewMixin:
For example, "subscribed-false dup-subscribed-true".
"""
if subscribed_person in self.duplicate_subscribers:
- dup_class = 'dup-subscribed-true'
+ dup_class = "dup-subscribed-true"
else:
- dup_class = 'dup-subscribed-false'
+ dup_class = "dup-subscribed-false"
if subscribed_person in self.direct_subscribers:
- return 'subscribed-true %s' % dup_class
+ return "subscribed-true %s" % dup_class
else:
- return 'subscribed-false %s' % dup_class
+ return "subscribed-false %s" % dup_class
@cachedproperty
def _bug_attachments(self):
@@ -515,25 +529,26 @@ class BugViewMixin:
# if you are looking to consolidate things.
result = {
BugAttachmentType.PATCH: [],
- 'other': [],
- }
+ "other": [],
+ }
for attachment in self.context.attachments_unpopulated:
info = {
- 'attachment': attachment,
- 'file': ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment),
- }
+ "attachment": attachment,
+ "file": ProxiedLibraryFileAlias(
+ attachment.libraryfile, attachment
+ ),
+ }
if attachment.type == BugAttachmentType.PATCH:
key = attachment.type
else:
- key = 'other'
+ key = "other"
result[key].append(info)
return result
@property
def regular_attachments(self):
"""The list of bug attachments that are not patches."""
- return self._bug_attachments['other']
+ return self._bug_attachments["other"]
@property
def patches(self):
@@ -553,8 +568,9 @@ class BugViewMixin:
return self.context.getSpecifications(self.user)
-class BugInformationTypePortletView(InformationTypePortletMixin,
- LaunchpadView):
+class BugInformationTypePortletView(
+ InformationTypePortletMixin, LaunchpadView
+):
"""View class for the information type portlet."""
@@ -594,24 +610,26 @@ class BugView(LaunchpadView, BugViewMixin):
dupes_in_current_context = {
bugtask.bug: bugtask
for bugtask in current_task.target.searchTasks(
- BugTaskSearchParams(self.user, bug=any(*duplicate_bugs)))}
+ BugTaskSearchParams(self.user, bug=any(*duplicate_bugs))
+ )
+ }
dupes = []
for bug in duplicate_bugs:
# Don't disclose even the ID of a private bug. The link will
# just 404.
- if not check_permission('launchpad.View', bug):
+ if not check_permission("launchpad.View", bug):
continue
dupe = {
- 'title': bug.title,
- 'id': bug.id,
- 'bug': bug,
- }
+ "title": bug.title,
+ "id": bug.id,
+ "bug": bug,
+ }
# If the dupe has the same context as the one we're in, link
# to that bug task directly.
if bug in dupes_in_current_context:
- dupe['url'] = canonical_url(dupes_in_current_context[bug])
+ dupe["url"] = canonical_url(dupes_in_current_context[bug])
else:
- dupe['url'] = canonical_url(bug)
+ dupe["url"] = canonical_url(bug)
dupes.append(dupe)
return dupes
@@ -619,18 +637,22 @@ class BugView(LaunchpadView, BugViewMixin):
def proxiedUrlForLibraryFile(self, attachment):
"""Return the proxied download URL for a Librarian file."""
return ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment).http_url
+ attachment.libraryfile, attachment
+ ).http_url
class BugActivity(BugView):
- page_title = 'Activity log'
+ page_title = "Activity log"
@cachedproperty
def activity(self):
activity = list(IBug(self.context).activity)
- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- [a.person_id for a in activity], need_validity=True))
+ list(
+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ [a.person_id for a in activity], need_validity=True
+ )
+ )
return activity
@@ -650,13 +672,14 @@ class BugSubscriptionPortletDetails:
self.muted = False
if user is not None:
has_structural_subscriptions = not (
- get_structural_subscriptions_for_bug(bug, user).is_empty())
+ get_structural_subscriptions_for_bug(bug, user).is_empty()
+ )
self.muted = bug.isMuted(user)
psi = PersonSubscriptions(user, bug)
if psi.direct.personal:
self.direct_notifications = True
direct = psi.direct.personal[0]
- cache['subscription'] = direct.subscription
+ cache["subscription"] = direct.subscription
level = direct.subscription.bug_notification_level
if level == BugNotificationLevel.COMMENTS:
self.direct_all_notifications = True
@@ -666,55 +689,66 @@ class BugSubscriptionPortletDetails:
assert level == BugNotificationLevel.LIFECYCLE
self.direct_lifecycle_notifications = True
self.other_subscription_notifications = bool(
- has_structural_subscriptions or
- psi.from_duplicate.count or
- psi.as_owner.count or
- psi.as_assignee.count or
- psi.direct.as_team_member or
- psi.direct.as_team_admin)
- cache['other_subscription_notifications'] = bool(
- self.other_subscription_notifications)
+ has_structural_subscriptions
+ or psi.from_duplicate.count
+ or psi.as_owner.count
+ or psi.as_assignee.count
+ or psi.direct.as_team_member
+ or psi.direct.as_team_admin
+ )
+ cache["other_subscription_notifications"] = bool(
+ self.other_subscription_notifications
+ )
self.only_other_subscription_notifications = (
- self.other_subscription_notifications and
- not self.direct_notifications)
+ self.other_subscription_notifications
+ and not self.direct_notifications
+ )
self.any_subscription_notifications = (
- self.other_subscription_notifications or
- self.direct_notifications)
+ self.other_subscription_notifications
+ or self.direct_notifications
+ )
self.user_should_see_mute_link = (
- self.any_subscription_notifications or self.muted)
+ self.any_subscription_notifications or self.muted
+ )
-class BugSubscriptionPortletView(LaunchpadView,
- BugSubscriptionPortletDetails, BugViewMixin):
+class BugSubscriptionPortletView(
+ LaunchpadView, BugSubscriptionPortletDetails, BugViewMixin
+):
"""View class for the subscription portlet."""
# We want these strings to be available for the template and for the
# JavaScript.
notifications_text = {
- 'not_only_other_subscription': _('You are'),
- 'only_other_subscription': _(
- 'You have subscriptions that may cause you to receive '
- 'notifications, but you are'),
- 'direct_all': _('subscribed to all notifications for this bug.'),
- 'direct_metadata': _(
- 'subscribed to all notifications except comments for this bug.'),
- 'direct_lifecycle': _(
- 'subscribed to notifications when this bug is closed or '
- 'reopened.'),
- 'not_direct': _(
- "not directly subscribed to this bug's notifications."),
- 'muted': _(
- 'Your personal email notifications from this bug are muted.'),
- }
+ "not_only_other_subscription": _("You are"),
+ "only_other_subscription": _(
+ "You have subscriptions that may cause you to receive "
+ "notifications, but you are"
+ ),
+ "direct_all": _("subscribed to all notifications for this bug."),
+ "direct_metadata": _(
+ "subscribed to all notifications except comments for this bug."
+ ),
+ "direct_lifecycle": _(
+ "subscribed to notifications when this bug is closed or "
+ "reopened."
+ ),
+ "not_direct": _(
+ "not directly subscribed to this bug's notifications."
+ ),
+ "muted": _(
+ "Your personal email notifications from this bug are muted."
+ ),
+ }
def initialize(self):
"""Initialize the view to handle the request."""
LaunchpadView.initialize(self)
cache = IJSONRequestCache(self.request).objects
self.extractBugSubscriptionDetails(self.user, self.context, cache)
- cache['bug_is_private'] = self.context.private
+ cache["bug_is_private"] = self.context.private
if self.user:
- cache['notifications_text'] = self.notifications_text
+ cache["notifications_text"] = self.notifications_text
class BugWithoutContextView(RedirectionView):
@@ -728,16 +762,18 @@ class BugWithoutContextView(RedirectionView):
redirected_context = context.default_bugtask
viewname = getDefaultViewName(redirected_context, request)
cache_view = getMultiAdapter(
- (redirected_context, request), name=viewname)
+ (redirected_context, request), name=viewname
+ )
super().__init__(
- canonical_url(redirected_context), request, cache_view=cache_view)
+ canonical_url(redirected_context), request, cache_view=cache_view
+ )
class BugEditViewBase(LaunchpadEditFormView):
"""Base class for all bug edit pages."""
schema = IBug
- page_title = 'Edit'
+ page_title = "Edit"
def setUpWidgets(self):
"""Set up the widgets using the bug as the context."""
@@ -746,7 +782,8 @@ class BugEditViewBase(LaunchpadEditFormView):
def updateBugFromData(self, data):
"""Update the bug using the values in the data dictionary."""
LaunchpadEditFormView.updateContextFromData(
- self, data, context=self.context.bug)
+ self, data, context=self.context.bug
+ )
@property
def next_url(self):
@@ -759,18 +796,18 @@ class BugEditViewBase(LaunchpadEditFormView):
class BugEditView(BugEditViewBase):
"""The view for the edit bug page."""
- field_names = ['title', 'description', 'tags']
+ field_names = ["title", "description", "tags"]
custom_widget_title = CustomWidgetFactory(TextWidget, displayWidth=30)
custom_widget_tags = BugTagsWidget
@property
def label(self):
"""The form label."""
- return 'Edit details for bug #%d' % self.context.bug.id
+ return "Edit details for bug #%d" % self.context.bug.id
page_title = label
- @action('Change', name='change')
+ @action("Change", name="change")
def change_action(self, action, data):
"""Update the bug with submitted changes."""
self.updateBugFromData(data)
@@ -782,25 +819,24 @@ class BugLockStatusEditView(LaunchpadEditFormView):
class schema(Interface):
# Duplicating the fields is necessary because these fields are
# read-only in `IBug`.
- lock_status = copy_field(IBug['lock_status'], readonly=False)
- lock_reason = copy_field(IBug['lock_reason'], readonly=False)
+ lock_status = copy_field(IBug["lock_status"], readonly=False)
+ lock_reason = copy_field(IBug["lock_reason"], readonly=False)
@property
def adapters(self):
"""See `LaunchpadFormView`."""
return {self.schema: self.context.bug}
- field_names = ['lock_status', 'lock_reason']
+ field_names = ["lock_status", "lock_reason"]
custom_widget_lock_status = LaunchpadRadioWidgetWithDescription
custom_widget_lock_reason = CustomWidgetFactory(
- TextWidget,
- displayWidth=30
+ TextWidget, displayWidth=30
)
def initialize(self):
super().initialize()
- lock_status_widget = self.widgets['lock_status']
+ lock_status_widget = self.widgets["lock_status"]
lock_status_widget._displayItemForMissingValue = False
@property
@@ -812,26 +848,26 @@ class BugLockStatusEditView(LaunchpadEditFormView):
page_title = label
- @action('Change', name='change')
+ @action("Change", name="change")
def change_action(self, action, data):
"""Update the bug lock status and reason with submitted changes."""
bug = self.context.bug
- if bug.lock_status != data['lock_status']:
- locked_states = (
- BugLockStatus.COMMENT_ONLY,
- )
- if data['lock_status'] in locked_states:
+ if bug.lock_status != data["lock_status"]:
+ locked_states = (BugLockStatus.COMMENT_ONLY,)
+ if data["lock_status"] in locked_states:
bug.lock(
- status=data['lock_status'],
- reason=data['lock_reason'],
- who=self.user
+ status=data["lock_status"],
+ reason=data["lock_reason"],
+ who=self.user,
)
else:
bug.unlock(who=self.user)
- elif (bug.lock_status != BugLockStatus.UNLOCKED and
- bug.lock_reason != data['lock_reason']):
- bug.setLockReason(data['lock_reason'], who=self.user)
+ elif (
+ bug.lock_status != BugLockStatus.UNLOCKED
+ and bug.lock_reason != data["lock_reason"]
+ ):
+ bug.setLockReason(data["lock_reason"], who=self.user)
@property
def next_url(self):
@@ -846,7 +882,7 @@ class BugLockStatusEditView(LaunchpadEditFormView):
class BugMarkAsDuplicateView(BugEditViewBase):
"""Page for marking a bug as a duplicate."""
- field_names = ['duplicateof']
+ field_names = ["duplicateof"]
label = "Mark bug report as a duplicate"
page_title = label
@@ -855,15 +891,16 @@ class BugMarkAsDuplicateView(BugEditViewBase):
super().setUpFields()
duplicateof_field = DuplicateBug(
- __name__='duplicateof', title=_('Duplicate Of'), required=True)
+ __name__="duplicateof", title=_("Duplicate Of"), required=True
+ )
- self.form_fields = self.form_fields.omit('duplicateof')
+ self.form_fields = self.form_fields.omit("duplicateof")
self.form_fields = formlib.form.Fields(duplicateof_field)
@property
def initial_values(self):
"""See `LaunchpadFormView.`"""
- return {'duplicateof': self.context.bug.duplicateof}
+ return {"duplicateof": self.context.bug.duplicateof}
@property
def next_url(self):
@@ -873,20 +910,23 @@ class BugMarkAsDuplicateView(BugEditViewBase):
return None
def _validate(self, action, data):
- if action.name != 'remove':
+ if action.name != "remove":
return super()._validate(action, data)
return []
- @action('Set Duplicate', name='change',
- failure=LaunchpadFormView.ajax_failure_handler)
+ @action(
+ "Set Duplicate",
+ name="change",
+ failure=LaunchpadFormView.ajax_failure_handler,
+ )
def change_action(self, action, data):
"""Update the bug."""
data = dict(data)
# We handle duplicate changes by hand instead of leaving it to
# the usual machinery because we must use bug.markAsDuplicate().
bug = self.context.bug
- with notify_modified(bug, ['duplicateof']):
- duplicateof = data.pop('duplicateof')
+ with notify_modified(bug, ["duplicateof"]):
+ duplicateof = data.pop("duplicateof")
bug.markAsDuplicate(duplicateof)
# Apply other changes.
self.updateBugFromData(data)
@@ -895,12 +935,13 @@ class BugMarkAsDuplicateView(BugEditViewBase):
def shouldShowRemoveButton(self, action):
return self.context.bug.duplicateof is not None
- @action('Remove Duplicate', name='remove',
- condition=shouldShowRemoveButton)
+ @action(
+ "Remove Duplicate", name="remove", condition=shouldShowRemoveButton
+ )
def remove_action(self, action, data):
"""Update the bug."""
bug = self.context.bug
- with notify_modified(bug, ['duplicateof']):
+ with notify_modified(bug, ["duplicateof"]):
bug.markAsDuplicate(None)
return self._duplicate_action_result()
@@ -910,8 +951,8 @@ class BugMarkAsDuplicateView(BugEditViewBase):
launchbag = getUtility(ILaunchBag)
launchbag.add(bug.default_bugtask)
view = getMultiAdapter(
- (bug, self.request),
- name='+bugtasks-and-nominations-table')
+ (bug, self.request), name="+bugtasks-and-nominations-table"
+ )
view.initialize()
return view.render()
return None
@@ -922,11 +963,11 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
@property
def label(self):
- return 'Bug #%i - Set information type' % self.context.bug.id
+ return "Bug #%i - Set information type" % self.context.bug.id
page_title = label
- field_names = ['information_type', 'validate_change']
+ field_names = ["information_type", "validate_change"]
custom_widget_information_type = LaunchpadRadioWidgetWithDescription
custom_widget_validate_change = GhostCheckBoxWidget
@@ -938,13 +979,17 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
class information_type_schema(Interface):
information_type_field = copy_field(
- IBug['information_type'], readonly=False,
- vocabulary=InformationTypeVocabulary(types=info_types))
+ IBug["information_type"],
+ readonly=False,
+ vocabulary=InformationTypeVocabulary(types=info_types),
+ )
# A hidden field used to determine if the new information type
# should be validated to ensure the bug does not become invisible
# after the change.
validate_change = Bool(
- title="Validate change", required=False, default=False)
+ title="Validate change", required=False, default=False
+ )
+
return information_type_schema
@property
@@ -959,65 +1004,78 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
@property
def initial_values(self):
"""See `LaunchpadFormView.`"""
- return {'information_type': self.context.bug.information_type}
+ return {"information_type": self.context.bug.information_type}
- @action('Change', name='change',
- failure=LaunchpadFormView.ajax_failure_handler)
+ @action(
+ "Change", name="change", failure=LaunchpadFormView.ajax_failure_handler
+ )
def change_action(self, action, data):
"""Update the bug."""
data = dict(data)
bug = self.context.bug
- information_type = data.pop('information_type')
- changed_fields = ['information_type']
+ information_type = data.pop("information_type")
+ changed_fields = ["information_type"]
# When the user first submits the form, validate change is True and
# so we check that the bug does not become invisible. If the user
# confirms they really want to make the change, validate change is
# False and we process the change as normal.
if self.request.is_ajax:
- validate_change = data.get('validate_change', False)
- if (validate_change and
- information_type in PRIVATE_INFORMATION_TYPES and
- self._bug_will_be_invisible(information_type)):
+ validate_change = data.get("validate_change", False)
+ if (
+ validate_change
+ and information_type in PRIVATE_INFORMATION_TYPES
+ and self._bug_will_be_invisible(information_type)
+ ):
self.request.response.setStatus(400, "Bug Visibility")
- return ''
+ return ""
user_will_be_subscribed = (
- information_type in PRIVATE_INFORMATION_TYPES and
- bug.getSubscribersForPerson(self.user).is_empty())
+ information_type in PRIVATE_INFORMATION_TYPES
+ and bug.getSubscribersForPerson(self.user).is_empty()
+ )
bug_before_modification = Snapshot(bug, providing=providedBy(bug))
- changed = bug.transitionToInformationType(
- information_type, self.user)
+ changed = bug.transitionToInformationType(information_type, self.user)
if changed:
self._handlePrivacyChanged(user_will_be_subscribed)
notify(
ObjectModifiedEvent(
- bug, bug_before_modification, changed_fields,
- user=self.user))
+ bug,
+ bug_before_modification,
+ changed_fields,
+ user=self.user,
+ )
+ )
if self.request.is_ajax:
# Avoid circular imports
from lp.bugs.browser.bugtask import (
can_add_package_task_to_bug,
can_add_project_task_to_bug,
- )
+ )
+
if changed:
result_data = self._getSubscriptionDetails()
- result_data['can_add_project_task'] = (
- can_add_project_task_to_bug(bug))
- result_data['can_add_package_task'] = (
- can_add_package_task_to_bug(bug))
+ result_data[
+ "can_add_project_task"
+ ] = can_add_project_task_to_bug(bug)
+ result_data[
+ "can_add_package_task"
+ ] = can_add_package_task_to_bug(bug)
self.request.response.setHeader(
- 'content-type', 'application/json')
+ "content-type", "application/json"
+ )
return json.dumps(
- result_data, cls=ResourceJSONEncoder,
- media_type=EntryResource.JSON_TYPE)
+ result_data,
+ cls=ResourceJSONEncoder,
+ media_type=EntryResource.JSON_TYPE,
+ )
else:
- return ''
+ return ""
def _bug_will_be_invisible(self, information_type):
# Return true if this bug will be totally invisible if it were to be
# change to the specified information type.
pillars = self.context.bug.affected_pillars
- service = getUtility(IService, 'sharing')
+ service = getUtility(IService, "sharing")
for pillar in pillars:
grant_counts = service.getAccessPolicyGrantCounts(pillar)
for count_info in grant_counts:
@@ -1037,11 +1095,12 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
else:
bug = self.context
subscribers_portlet = BugPortletSubscribersWithDetails(
- bug, self.request)
+ bug, self.request
+ )
subscription_data = subscribers_portlet.subscriber_data
result_data = dict(
- cache_data=cache,
- subscription_data=subscription_data)
+ cache_data=cache, subscription_data=subscription_data
+ )
return result_data
def _handlePrivacyChanged(self, user_will_be_subscribed):
@@ -1054,18 +1113,20 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
"""
if user_will_be_subscribed:
notification_text = (
- "Since you marked this bug as private you have "
- "automatically been subscribed to it. "
- "If you don't want to receive email about "
- "this bug you can <a href=\"%s\">mute your "
- "subscription</a> or <a href=\"%s\">"
- "unsubscribe</a>." % (
- canonical_url(
- self.context, view_name='+mute'),
- canonical_url(
- self.context, view_name='+subscribe')))
+ "Since you marked this bug as private you have "
+ "automatically been subscribed to it. "
+ "If you don't want to receive email about "
+ 'this bug you can <a href="%s">mute your '
+ 'subscription</a> or <a href="%s">'
+ "unsubscribe</a>."
+ % (
+ canonical_url(self.context, view_name="+mute"),
+ canonical_url(self.context, view_name="+subscribe"),
+ )
+ )
self.request.response.addInfoNotification(
- structured(notification_text))
+ structured(notification_text)
+ )
class DeprecatedAssignedBugsView(RedirectionView):
@@ -1084,7 +1145,8 @@ class DeprecatedAssignedBugsView(RedirectionView):
def __call__(self):
self.target = canonical_url(
- getUtility(ILaunchBag).user, view_name='+assignedbugs')
+ getUtility(ILaunchBag).user, view_name="+assignedbugs"
+ )
super().__call__()
@property
@@ -1092,7 +1154,7 @@ class DeprecatedAssignedBugsView(RedirectionView):
return self._context
-normalize_mime_type = re.compile(r'\s+')
+normalize_mime_type = re.compile(r"\s+")
class BugTextView(LaunchpadView):
@@ -1109,7 +1171,8 @@ class BugTextView(LaunchpadView):
authorised_people.append(task.assignee)
authorised_people.extend(self.subscribers)
precache_permission_for_objects(
- self.request, 'launchpad.LimitedView', authorised_people)
+ self.request, "launchpad.LimitedView", authorised_people
+ )
@cachedproperty
def bugtasks(self):
@@ -1119,8 +1182,11 @@ class BugTextView(LaunchpadView):
@cachedproperty
def subscribers(self):
"""Cache subscribers and avoid hitting the DB twice."""
- return [sub.person for sub in self.context.subscriptions
- if self.user or not sub.person.private]
+ return [
+ sub.person
+ for sub in self.context.subscriptions
+ if self.user or not sub.person.private
+ ]
def bug_text(self):
@@ -1128,99 +1194,112 @@ class BugTextView(LaunchpadView):
bug = self.context
text = []
- text.append('bug: %d' % bug.id)
- text.append('title: %s' % bug.title)
- text.append('date-reported: %s' %
- format_rfc2822_date(bug.datecreated))
- text.append('date-updated: %s' %
- format_rfc2822_date(bug.date_last_updated))
- text.append('reporter: %s' % bug.owner.unique_displayname)
+ text.append("bug: %d" % bug.id)
+ text.append("title: %s" % bug.title)
+ text.append("date-reported: %s" % format_rfc2822_date(bug.datecreated))
+ text.append(
+ "date-updated: %s" % format_rfc2822_date(bug.date_last_updated)
+ )
+ text.append("reporter: %s" % bug.owner.unique_displayname)
if bug.duplicateof:
- text.append('duplicate-of: %d' % bug.duplicateof.id)
+ text.append("duplicate-of: %d" % bug.duplicateof.id)
else:
- text.append('duplicate-of: ')
+ text.append("duplicate-of: ")
if bug.duplicates:
- dupes = ' '.join(str(dupe.id) for dupe in bug.duplicates)
- text.append('duplicates: %s' % dupes)
+ dupes = " ".join(str(dupe.id) for dupe in bug.duplicates)
+ text.append("duplicates: %s" % dupes)
else:
- text.append('duplicates: ')
+ text.append("duplicates: ")
if bug.private:
# XXX kiko 2007-10-31: this could include date_made_private and
# who_made_private but Bjorn doesn't let me.
- text.append('private: yes')
+ text.append("private: yes")
if bug.security_related:
- text.append('security: yes')
+ text.append("security: yes")
patches = []
- text.append('attachments: ')
+ text.append("attachments: ")
for attachment in bug.attachments_unpopulated:
if attachment.type != BugAttachmentType.PATCH:
- text.append(' %s' % self.attachment_text(attachment))
+ text.append(" %s" % self.attachment_text(attachment))
else:
patches.append(attachment)
- text.append('patches: ')
+ text.append("patches: ")
for attachment in patches:
- text.append(' %s' % self.attachment_text(attachment))
+ text.append(" %s" % self.attachment_text(attachment))
- text.append('tags: %s' % ' '.join(bug.tags))
+ text.append("tags: %s" % " ".join(bug.tags))
- text.append('subscribers: ')
+ text.append("subscribers: ")
for subscriber in self.subscribers:
- text.append(' %s' % subscriber.unique_displayname)
+ text.append(" %s" % subscriber.unique_displayname)
- return ''.join(line + '\n' for line in text)
+ return "".join(line + "\n" for line in text)
def bugtask_text(self, task):
"""Return a BugTask for text display."""
text = []
- text.append('task: %s' % task.bugtargetname)
- text.append('status: %s' % task.status.title)
- text.append('date-created: %s' %
- format_rfc2822_date(task.datecreated))
-
- for status in ["left_new", "confirmed", "triaged", "assigned",
- "inprogress", "closed", "incomplete",
- "fix_committed", "fix_released", "left_closed"]:
+ text.append("task: %s" % task.bugtargetname)
+ text.append("status: %s" % task.status.title)
+ text.append("date-created: %s" % format_rfc2822_date(task.datecreated))
+
+ for status in [
+ "left_new",
+ "confirmed",
+ "triaged",
+ "assigned",
+ "inprogress",
+ "closed",
+ "incomplete",
+ "fix_committed",
+ "fix_released",
+ "left_closed",
+ ]:
date = getattr(task, "date_%s" % status)
if date:
- text.append("date-%s: %s" % (
- status.replace('_', '-'), format_rfc2822_date(date)))
+ text.append(
+ "date-%s: %s"
+ % (status.replace("_", "-"), format_rfc2822_date(date))
+ )
- text.append('reporter: %s' % task.owner.unique_displayname)
+ text.append("reporter: %s" % task.owner.unique_displayname)
if task.bugwatch:
- text.append('watch: %s' % task.bugwatch.url)
+ text.append("watch: %s" % task.bugwatch.url)
- text.append('importance: %s' % task.importance.title)
+ text.append("importance: %s" % task.importance.title)
component = task.getPackageComponent()
if component:
- text.append('component: %s' % component.name)
+ text.append("component: %s" % component.name)
- if (task.assignee
- and check_permission('launchpad.LimitedView', task.assignee)):
- text.append('assignee: %s' % task.assignee.unique_displayname)
+ if task.assignee and check_permission(
+ "launchpad.LimitedView", task.assignee
+ ):
+ text.append("assignee: %s" % task.assignee.unique_displayname)
else:
- text.append('assignee: ')
+ text.append("assignee: ")
if task.milestone:
- text.append('milestone: %s' % task.milestone.name)
+ text.append("milestone: %s" % task.milestone.name)
else:
- text.append('milestone: ')
+ text.append("milestone: ")
- return ''.join(line + '\n' for line in text)
+ return "".join(line + "\n" for line in text)
def attachment_text(self, attachment):
"""Return a text representation of a bug attachment."""
mime_type = normalize_mime_type.sub(
- ' ', attachment.libraryfile.mimetype)
+ " ", attachment.libraryfile.mimetype
+ )
download_url = ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment).http_url
+ attachment.libraryfile, attachment
+ ).http_url
return "%s %s" % (download_url, mime_type)
def comment_text(self):
@@ -1229,16 +1308,15 @@ class BugTextView(LaunchpadView):
def build_message(text):
mailwrapper = MailWrapper(width=72)
text = mailwrapper.format(text)
- message = MIMEText(text.encode('utf-8'),
- 'plain', 'utf-8')
+ message = MIMEText(text.encode("utf-8"), "plain", "utf-8")
# This is redundant and makes the template noisy
- del message['MIME-Version']
+ del message["MIME-Version"]
return message
from lp.bugs.browser.bugtask import (
get_comments_for_bugtask,
get_visible_comments,
- )
+ )
# XXX: kiko 2007-10-31: for some reason, get_comments_for_bugtask
# takes a task, not a bug. For now live with it.
@@ -1252,16 +1330,16 @@ class BugTextView(LaunchpadView):
for comment in comments:
message = build_message(comment.text_for_display)
- message['Author'] = comment.owner.unique_displayname
- message['Date'] = format_rfc2822_date(comment.datecreated)
- message['Message-Id'] = comment.rfc822msgid
+ message["Author"] = comment.owner.unique_displayname
+ message["Date"] = format_rfc2822_date(comment.datecreated)
+ message["Message-Id"] = comment.rfc822msgid
comment_mime.attach(message)
- return message_as_bytes(comment_mime).decode('utf-8')
+ return message_as_bytes(comment_mime).decode("utf-8")
def render(self):
"""Return a text representation of the bug."""
- self.request.response.setHeader('Content-type', 'text/plain')
+ self.request.response.setHeader("Content-type", "text/plain")
texts = [self.bug_text()]
texts.extend(self.bugtask_text(task) for task in self.bugtasks)
texts.append(self.comment_text())
@@ -1273,7 +1351,7 @@ class BugURL:
"""Bug URL creation rules."""
inside = None
- rootsite = 'bugs'
+ rootsite = "bugs"
def __init__(self, context):
self.context = context
@@ -1287,24 +1365,29 @@ class BugURL:
class BugAffectingUserChoice(EnumeratedType):
"""The choices for a bug affecting a user."""
- YES = Item("""
+ YES = Item(
+ """
Yes
This bug affects me.
- """)
+ """
+ )
- NO = Item("""
+ NO = Item(
+ """
No
This bug doesn't affect me.
- """)
+ """
+ )
class BugMarkAsAffectingUserForm(Interface):
"""Form schema for marking the bug as affecting the user."""
+
affects = Choice(
- title=_('Does this bug affect you?'),
- vocabulary=BugAffectingUserChoice)
+ title=_("Does this bug affect you?"), vocabulary=BugAffectingUserChoice
+ )
class BugMarkAsAffectingUserView(LaunchpadFormView):
@@ -1312,7 +1395,7 @@ class BugMarkAsAffectingUserView(LaunchpadFormView):
schema = BugMarkAsAffectingUserForm
- field_names = ['affects']
+ field_names = ["affects"]
label = "Does this bug affect you?"
page_title = label
@@ -1327,11 +1410,12 @@ class BugMarkAsAffectingUserView(LaunchpadFormView):
else:
affects = BugAffectingUserChoice.NO
- return {'affects': affects}
+ return {"affects": affects}
- @action('Change', name='change')
+ @action("Change", name="change")
def change_action(self, action, data):
"""Mark the bug according to the selection."""
self.context.bug.markUserAffected(
- self.user, data['affects'] == BugAffectingUserChoice.YES)
+ self.user, data["affects"] == BugAffectingUserChoice.YES
+ )
self.request.response.redirect(canonical_url(self.context.bug))
diff --git a/lib/lp/bugs/browser/bugalsoaffects.py b/lib/lp/bugs/browser/bugalsoaffects.py
index 7372c56..5525100 100644
--- a/lib/lp/bugs/browser/bugalsoaffects.py
+++ b/lib/lp/bugs/browser/bugalsoaffects.py
@@ -2,17 +2,14 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
- 'BugAlsoAffectsProductMetaView',
- 'BugAlsoAffectsDistroMetaView',
- 'BugAlsoAffectsProductWithProductCreationView'
- ]
+ "BugAlsoAffectsProductMetaView",
+ "BugAlsoAffectsDistroMetaView",
+ "BugAlsoAffectsProductWithProductCreationView",
+]
from textwrap import dedent
-from lazr.enum import (
- EnumeratedType,
- Item,
- )
+from lazr.enum import EnumeratedType, Item
from lazr.lifecycle.event import ObjectCreatedEvent
from zope.browserpage import ViewPageTemplateFile
from zope.component import getUtility
@@ -22,20 +19,11 @@ from zope.formlib.interfaces import MissingInputError
from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import DropdownWidget
from zope.schema import Choice
-from zope.schema.vocabulary import (
- SimpleTerm,
- SimpleVocabulary,
- )
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
from lp import _
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
-from lp.app.browser.multistep import (
- MultiStepView,
- StepView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
+from lp.app.browser.multistep import MultiStepView, StepView
from lp.app.enums import ServiceUsage
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.validators.email import email_validator
@@ -44,7 +32,7 @@ from lp.app.widgets.popup import SearchForUpstreamPopupWidget
from lp.app.widgets.textwidgets import StrippedTextWidget
from lp.bugs.browser.widgets.bugtask import (
BugTaskAlsoAffectsSourcePackageNameWidget,
- )
+)
from lp.bugs.interfaces.bug import IBug
from lp.bugs.interfaces.bugtask import (
BugTaskImportance,
@@ -53,31 +41,19 @@ from lp.bugs.interfaces.bugtask import (
IAddBugTaskWithProductCreationForm,
IllegalTarget,
valid_remote_bug_url,
- )
-from lp.bugs.interfaces.bugtracker import (
- BugTrackerType,
- IBugTrackerSet,
- )
+)
+from lp.bugs.interfaces.bugtracker import BugTrackerType, IBugTrackerSet
from lp.bugs.interfaces.bugwatch import (
IBugWatchSet,
NoBugTrackerFound,
UnrecognizedBugTrackerURL,
- )
-from lp.bugs.model.bugtask import (
- validate_new_target,
- validate_target,
- )
+)
+from lp.bugs.model.bugtask import validate_new_target, validate_target
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
- )
-from lp.registry.interfaces.packaging import (
- IPackagingUtil,
- PackagingType,
- )
-from lp.registry.interfaces.product import (
- IProductSet,
- License,
- )
+)
+from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
+from lp.registry.interfaces.product import IProductSet, License
from lp.registry.model.product import Product
from lp.services.features import getFeatureFlag
from lp.services.fields import StrippedTextLine
@@ -88,7 +64,7 @@ from lp.services.webapp.interfaces import ILaunchBag
class BugAlsoAffectsProductMetaView(MultiStepView):
- page_title = 'Record as affecting another project'
+ page_title = "Record as affecting another project"
@property
def first_step(self):
@@ -96,7 +72,7 @@ class BugAlsoAffectsProductMetaView(MultiStepView):
class BugAlsoAffectsDistroMetaView(MultiStepView):
- page_title = 'Record as affecting another distribution/package'
+ page_title = "Record as affecting another distribution/package"
@property
def first_step(self):
@@ -104,17 +80,17 @@ class BugAlsoAffectsDistroMetaView(MultiStepView):
class AlsoAffectsStep(StepView):
- __launchpad_facetname__ = 'bugs'
+ __launchpad_facetname__ = "bugs"
schema = IAddBugTaskForm
class LinkPackgingMixin:
-
@property
def can_link_package(self):
bugtask = self.context
is_package_bugtask = IDistributionSourcePackage.providedBy(
- bugtask.target)
+ bugtask.target
+ )
return is_package_bugtask and bugtask.target.upstream_product is None
@@ -122,7 +98,8 @@ class ChooseProductStep(LinkPackgingMixin, AlsoAffectsStep):
"""View for choosing a product that is affected by a given bug."""
template = ViewPageTemplateFile(
- '../templates/bugtask-choose-affected-product.pt')
+ "../templates/bugtask-choose-affected-product.pt"
+ )
custom_widget_product = SearchForUpstreamPopupWidget
label = "Record as affecting another project"
@@ -131,15 +108,18 @@ class ChooseProductStep(LinkPackgingMixin, AlsoAffectsStep):
@property
def _field_names(self):
"""The fields needed to choose an existing project."""
- names = ['product']
+ names = ["product"]
if self.can_link_package:
- names.append('add_packaging')
+ names.append("add_packaging")
return names
def initialize(self):
super().initialize()
- if (self.widgets['product'].hasInput() or
- not IDistributionSourcePackage.providedBy(self.context.target)):
+ if self.widgets[
+ "product"
+ ].hasInput() or not IDistributionSourcePackage.providedBy(
+ self.context.target
+ ):
return
self.maybeAddNotificationOrTeleport()
@@ -164,20 +144,20 @@ class ChooseProductStep(LinkPackgingMixin, AlsoAffectsStep):
# We can infer the upstream and there's no bugtask for it,
# so we can go straight to the page asking for the remote
# bug URL.
- self.request.form['field.product'] = upstream.name
- self.request.form['field.add_packaging'] = 'off'
+ self.request.form["field.product"] = upstream.name
+ self.request.form["field.add_packaging"] = "off"
self.next_step = ProductBugTaskCreationStep
return
def validateStep(self, data):
- if data.get('product'):
+ if data.get("product"):
try:
- validate_new_target(self.context.bug, data.get('product'))
+ validate_new_target(self.context.bug, data.get("product"))
except IllegalTarget as e:
- self.setFieldError('product', e.args[0])
+ self.setFieldError("product", e.args[0])
return
- entered_product = self.request.form.get(self.widgets['product'].name)
+ entered_product = self.request.form.get(self.widgets["product"].name)
if not entered_product:
return
@@ -185,10 +165,11 @@ class ChooseProductStep(LinkPackgingMixin, AlsoAffectsStep):
# Tell the user to search for it using the popup widget as it'll allow
# the user to register a new product if the one they are looking for
# is not yet registered.
- widget_link_id = self.widgets['product'].show_widget_id
+ widget_link_id = self.widgets["product"].show_widget_id
self.setFieldError(
- 'product',
- structured("""
+ "product",
+ structured(
+ """
There is no project in Launchpad named "%s". Please
<a href="/projects"
onclick="LPJS.use('event').Event.simulate(
@@ -196,17 +177,20 @@ class ChooseProductStep(LinkPackgingMixin, AlsoAffectsStep):
return false;"
>search for it</a> as it may be
registered with a different name.""",
- entered_product, widget_link_id))
+ entered_product,
+ widget_link_id,
+ ),
+ )
def main_action(self, data):
"""Perform the 'Continue' action."""
# Inject the selected product into the form and set the next_step to
# be used by our multistep controller.
- self.request.form['field.product'] = data['product'].name
- if data.get('add_packaging', False):
- self.request.form['field.add_packaging'] = 'on'
+ self.request.form["field.product"] = data["product"].name
+ if data.get("add_packaging", False):
+ self.request.form["field.add_packaging"] = "on"
else:
- self.request.form['field.add_packaging'] = 'off'
+ self.request.form["field.add_packaging"] = "off"
self.next_step = ProductBugTaskCreationStep
@@ -223,10 +207,11 @@ class BugTaskCreationStep(AlsoAffectsStep):
"""
custom_widget_bug_url = CustomWidgetFactory(
- StrippedTextWidget, displayWidth=62)
+ StrippedTextWidget, displayWidth=62
+ )
- initial_focus_widget = 'bug_url'
- step_name = 'specify_remote_bug_url'
+ initial_focus_widget = "bug_url"
+ step_name = "specify_remote_bug_url"
target_field_names = ()
# This is necessary so that other views which dispatch work to this one
@@ -236,15 +221,16 @@ class BugTaskCreationStep(AlsoAffectsStep):
def __init__(self, context, request):
super().__init__(context, request)
self.notifications = []
- self._field_names = ['bug_url'] + list(self.target_field_names)
+ self._field_names = ["bug_url"] + list(self.target_field_names)
def setUpWidgets(self):
super().setUpWidgets()
self.target_widgets = [
self.widgets[field_name]
for field_name in self.field_names
- if field_name in self.target_field_names]
- self.bugwatch_widgets = [self.widgets['bug_url']]
+ if field_name in self.target_field_names
+ ]
+ self.bugwatch_widgets = [self.widgets["bug_url"]]
def getTarget(self, data=None):
"""Return the fix target.
@@ -261,7 +247,7 @@ class BugTaskCreationStep(AlsoAffectsStep):
that URL we create a bug watch and link it to the newly created bug
task.
"""
- bug_url = data.get('bug_url', '')
+ bug_url = data.get("bug_url", "")
target = self.getTarget(data)
extracted_bug = None
@@ -269,63 +255,75 @@ class BugTaskCreationStep(AlsoAffectsStep):
if bug_url:
try:
extracted_bugtracker, extracted_bug = getUtility(
- IBugWatchSet).extractBugTrackerAndBug(bug_url)
+ IBugWatchSet
+ ).extractBugTrackerAndBug(bug_url)
except NoBugTrackerFound:
# Delegate to another view which will ask the user if they
# wants to create the bugtracker now.
- if 'product' in self.target_field_names:
+ if "product" in self.target_field_names:
self.next_step = UpstreamBugTrackerCreationStep
else:
- assert 'distribution' in self.target_field_names
+ assert "distribution" in self.target_field_names
self.next_step = DistroBugTrackerCreationStep
return
- if data.get('product') is not None:
- task_target = data['product']
+ if data.get("product") is not None:
+ task_target = data["product"]
else:
- task_target = data['distribution']
- if data.get('sourcepackagename') is not None:
- task_target = data['sourcepackagename']
+ task_target = data["distribution"]
+ if data.get("sourcepackagename") is not None:
+ task_target = data["sourcepackagename"]
# The new target has already been validated so don't do it again.
self.task_added = self.context.bug.addTask(
- getUtility(ILaunchBag).user, task_target, validate_target=False)
+ getUtility(ILaunchBag).user, task_target, validate_target=False
+ )
task_added = self.task_added
if extracted_bug is not None:
- assert extracted_bugtracker is not None, (
- "validate() should have ensured that bugtracker is not None.")
+ assert (
+ extracted_bugtracker is not None
+ ), "validate() should have ensured that bugtracker is not None."
# Display a notification, if another bug is already linked
# to the same external bug.
other_bugs_already_watching = [
- bug for bug in extracted_bugtracker.getBugsWatching(
- extracted_bug)
- if bug != self.context.bug]
+ bug
+ for bug in extracted_bugtracker.getBugsWatching(extracted_bug)
+ if bug != self.context.bug
+ ]
# Simply add one notification per bug to simplify the
# implementation; most of the time it will be only one bug.
for other_bug in other_bugs_already_watching:
self.request.response.addInfoNotification(
structured(
- '<a href="%(bug_url)s">Bug #%(bug_id)s</a> also links'
- ' to the added bug watch'
- ' (%(bugtracker_name)s #%(remote_bug)s).',
- bug_url=canonical_url(other_bug),
- bug_id=str(other_bug.id),
- bugtracker_name=extracted_bugtracker.name,
- remote_bug=extracted_bug))
+ '<a href="%(bug_url)s">Bug #%(bug_id)s</a> also links'
+ " to the added bug watch"
+ " (%(bugtracker_name)s #%(remote_bug)s).",
+ bug_url=canonical_url(other_bug),
+ bug_id=str(other_bug.id),
+ bugtracker_name=extracted_bugtracker.name,
+ remote_bug=extracted_bug,
+ )
+ )
# Make sure that we don't add duplicate bug watches.
bug_watch = task_added.bug.getBugWatch(
- extracted_bugtracker, extracted_bug)
+ extracted_bugtracker, extracted_bug
+ )
if bug_watch is None:
bug_watch = task_added.bug.addWatch(
- extracted_bugtracker, extracted_bug, self.user)
+ extracted_bugtracker, extracted_bug, self.user
+ )
if target.bug_tracking_usage != ServiceUsage.LAUNCHPAD:
task_added.bugwatch = bug_watch
- if (target.bug_tracking_usage != ServiceUsage.LAUNCHPAD
+ if (
+ target.bug_tracking_usage != ServiceUsage.LAUNCHPAD
and task_added.bugwatch is not None
- and (task_added.bugwatch.bugtracker.bugtrackertype !=
- BugTrackerType.EMAILADDRESS)):
+ and (
+ task_added.bugwatch.bugtracker.bugtrackertype
+ != BugTrackerType.EMAILADDRESS
+ )
+ ):
# A remote bug task gets its status from a bug watch, so
# we want its status/importance to be UNKNOWN when
# created. Status updates cannot be fetched from Email
@@ -333,10 +331,10 @@ class BugTaskCreationStep(AlsoAffectsStep):
# importance to be updated manually, so we do not reset
# the status and importance here.
bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
- task_added.transitionToStatus(
- BugTaskStatus.UNKNOWN, bug_importer)
+ task_added.transitionToStatus(BugTaskStatus.UNKNOWN, bug_importer)
task_added.transitionToImportance(
- BugTaskImportance.UNKNOWN, bug_importer)
+ BugTaskImportance.UNKNOWN, bug_importer
+ )
notify(ObjectCreatedEvent(task_added))
self.next_url = canonical_url(task_added)
@@ -345,49 +343,54 @@ class BugTaskCreationStep(AlsoAffectsStep):
class IAddDistroBugTaskForm(IAddBugTaskForm):
sourcepackagename = Choice(
- title=_("Source Package Name"), required=False,
- description=_("The source package in which the bug occurs. "
- "Leave blank if you are not sure."),
- vocabulary='DistributionSourcePackage')
+ title=_("Source Package Name"),
+ required=False,
+ description=_(
+ "The source package in which the bug occurs. "
+ "Leave blank if you are not sure."
+ ),
+ vocabulary="DistributionSourcePackage",
+ )
class DistroBugTaskCreationStep(BugTaskCreationStep):
- """Specialized BugTaskCreationStep for reporting a bug in a distribution.
- """
+ """Specialized BugTaskCreationStep for reporting a bug in a distro."""
@property
def schema(self):
- if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
+ if bool(getFeatureFlag("disclosure.dsp_picker.enabled")):
return IAddDistroBugTaskForm
else:
return IAddBugTaskForm
custom_widget_sourcepackagename = BugTaskAlsoAffectsSourcePackageNameWidget
- template = ViewPageTemplateFile('../templates/bugtask-requestfix.pt')
+ template = ViewPageTemplateFile("../templates/bugtask-requestfix.pt")
label = "Also affects distribution/package"
- target_field_names = ('distribution', 'sourcepackagename')
+ target_field_names = ("distribution", "sourcepackagename")
@property
def initial_values(self):
"""Return the initial values for the view's fields."""
- return {'distribution': getUtility(ILaunchpadCelebrities).ubuntu}
+ return {"distribution": getUtility(ILaunchpadCelebrities).ubuntu}
def getTarget(self, data=None):
if data is not None:
- return data.get('distribution')
+ return data.get("distribution")
else:
- return self.widgets['distribution'].getInputValue()
+ return self.widgets["distribution"].getInputValue()
def main_action(self, data):
"""Create the new bug task, confirming if necessary."""
- bug_url = data.get('bug_url', '')
+ bug_url = data.get("bug_url", "")
target = self.getTarget(data)
- if (not bug_url and
- not self.request.get('ignore_missing_remote_bug') and
- target.bug_tracking_usage != ServiceUsage.LAUNCHPAD):
+ if (
+ not bug_url
+ and not self.request.get("ignore_missing_remote_bug")
+ and target.bug_tracking_usage != ServiceUsage.LAUNCHPAD
+ ):
# We have no URL for the remote bug and the target does not use
# Launchpad for bug tracking, so we warn the user this is not
# optimal and ask for their confirmation.
@@ -399,14 +402,21 @@ class DistroBugTaskCreationStep(BugTaskCreationStep):
'<input type="hidden" name="%s" value="1" />'
'<input style="font-size: smaller" type="submit"'
' value="Add Anyway" name="ignore_missing_remote_bug" />',
- self.continue_action.__name__)
- self.notifications.append(structured(
- dedent("""
+ self.continue_action.__name__,
+ )
+ self.notifications.append(
+ structured(
+ dedent(
+ """
%s doesn't use Launchpad as its bug tracker. Without a bug
URL to watch, the %s status will not update automatically.
- %s"""),
- target.displayname, target.displayname,
- confirm_button).escapedtext)
+ %s"""
+ ),
+ target.displayname,
+ target.displayname,
+ confirm_button,
+ ).escapedtext
+ )
return None
# Create the task.
return super().main_action(data)
@@ -419,31 +429,36 @@ class DistroBugTaskCreationStep(BugTaskCreationStep):
3. it's possible to create a new task for the given package/distro.
"""
target = self.getTarget(data)
- bug_url = data.get('bug_url')
+ bug_url = data.get("bug_url")
if bug_url and target.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
self.addError(
"Bug watches can not be added for %s, as it uses Launchpad"
" as its official bug tracker. Alternatives are to add a"
" watch for another project, or a comment containing a"
- " URL to the related bug report." % target.displayname)
+ " URL to the related bug report." % target.displayname
+ )
- distribution = data.get('distribution')
- sourcepackagename = data.get('sourcepackagename')
+ distribution = data.get("distribution")
+ sourcepackagename = data.get("sourcepackagename")
entered_package = self.request.form.get(
- self.widgets['sourcepackagename'].name)
+ self.widgets["sourcepackagename"].name
+ )
if sourcepackagename is None and entered_package:
# The entered package doesn't exist.
if distribution.has_published_binaries:
- binary_tracking = ''
+ binary_tracking = ""
else:
binary_tracking = structured(
- ' Launchpad does not track binary package names '
- 'in %s.', distribution.displayname)
+ " Launchpad does not track binary package names " "in %s.",
+ distribution.displayname,
+ )
error = structured(
'There is no package in %s named "%s".%s',
- distribution.displayname, entered_package,
- binary_tracking)
- self.setFieldError('sourcepackagename', error)
+ distribution.displayname,
+ entered_package,
+ binary_tracking,
+ )
+ self.setFieldError("sourcepackagename", error)
elif not IDistributionSourcePackage.providedBy(sourcepackagename):
try:
target = distribution
@@ -452,27 +467,30 @@ class DistroBugTaskCreationStep(BugTaskCreationStep):
# The validity of the source package has already been checked
# by the bug target widget.
validate_new_target(
- self.context.bug, target, check_source_package=False)
+ self.context.bug, target, check_source_package=False
+ )
if sourcepackagename:
- data['sourcepackagename'] = target
+ data["sourcepackagename"] = target
except IllegalTarget as e:
if sourcepackagename:
- self.setFieldError('sourcepackagename', e.args[0])
+ self.setFieldError("sourcepackagename", e.args[0])
else:
- self.setFieldError('distribution', e.args[0])
+ self.setFieldError("distribution", e.args[0])
super().validateStep(data)
def render(self):
for bugtask in IBug(self.context).bugtasks:
- if (IDistributionSourcePackage.providedBy(bugtask.target) and
- (not self.widgets['sourcepackagename'].hasInput())):
+ if IDistributionSourcePackage.providedBy(bugtask.target) and (
+ not self.widgets["sourcepackagename"].hasInput()
+ ):
# Set the rendered value to the raw name string rather than
# the SPN object. The widget won't have its distribution
# set up in order to be able to look up the SPN, and even if
# it did it might not correspond to a valid DSP.
- self.widgets['sourcepackagename'].setRenderedValue(
- bugtask.sourcepackagename.name)
+ self.widgets["sourcepackagename"].setRenderedValue(
+ bugtask.sourcepackagename.name
+ )
break
return super().render()
@@ -484,43 +502,46 @@ class LinkUpstreamHowOptions(EnumeratedType):
Enter the URL in the upstream bug tracker. If it's in a
supported upstream bug tracker, Launchpad can download the
status and display it in the bug report.
- """)
-
-# XXX: GavinPanella 2008-02-13 bug=201793: This will be uncommented in
-# a later branch.
-#
-# EMAIL_UPSTREAM = Item(
-# """I would like to email an upstream bug contact.
-#
-# Launchpad will prepare an example email containing all the
-# pertinent details. You can send it from Launchpad or from your
-# own mail software. If you send it from Launchpad, it'll save
-# the message id and - in the future - will use it to try and
-# follow the resulting conversation, provided it happens on a
-# public mailing list.
-# """)
+ """
+ )
+
+ # XXX: GavinPanella 2008-02-13 bug=201793: This will be uncommented in
+ # a later branch.
+ #
+ # EMAIL_UPSTREAM = Item(
+ # """I would like to email an upstream bug contact.
+ #
+ # Launchpad will prepare an example email containing all the
+ # pertinent details. You can send it from Launchpad or from your
+ # own mail software. If you send it from Launchpad, it'll save
+ # the message id and - in the future - will use it to try and
+ # follow the resulting conversation, provided it happens on a
+ # public mailing list.
+ # """)
EMAIL_UPSTREAM_DONE = Item(
"""I have already emailed an upstream bug contact:
Launchpad will record that.
- """)
+ """
+ )
-# XXX: GavinPanella 2008-02-13 bug=201793: This additional description
-# for EMAIL_UPSTREAM_DONE should be appended when EMAIL_UPSTREAM is
-# made available.
-#
-# "Next time, try using Launchpad to send the message upstream
-# too. That way it may be able to follow the conversation that
-# results from your bug report. This is especially true for public
-# mailing lists."
+ # XXX: GavinPanella 2008-02-13 bug=201793: This additional description
+ # for EMAIL_UPSTREAM_DONE should be appended when EMAIL_UPSTREAM is
+ # made available.
+ #
+ # "Next time, try using Launchpad to send the message upstream
+ # too. That way it may be able to follow the conversation that
+ # results from your bug report. This is especially true for public
+ # mailing lists."
UNLINKED_UPSTREAM = Item(
"""I want to add this upstream project to the bug report, but someone\
must find or report this bug in the upstream bug tracker.
Launchpad will record that.
- """)
+ """
+ )
class IAddBugTaskWithUpstreamLinkForm(IAddBugTaskForm):
@@ -535,44 +556,60 @@ class IAddBugTaskWithUpstreamLinkForm(IAddBugTaskForm):
check is left to the view, in part so that better error messages
can be provided.
"""
+
# link_upstream_how must have required=False, since
# ProductBugTaskCreationStep doesn't always display a form input for it.
link_upstream_how = Choice(
- title=_('How'), required=False,
+ title=_("How"),
+ required=False,
vocabulary=LinkUpstreamHowOptions,
default=LinkUpstreamHowOptions.LINK_UPSTREAM,
- description=_("How to link to an upstream bug."))
+ description=_("How to link to an upstream bug."),
+ )
bug_url = StrippedTextLine(
- title=_('Bug URL'), required=False, constraint=valid_remote_bug_url,
- description=_("The URL of this bug in the remote bug tracker."))
+ title=_("Bug URL"),
+ required=False,
+ constraint=valid_remote_bug_url,
+ description=_("The URL of this bug in the remote bug tracker."),
+ )
upstream_email_address_done = StrippedTextLine(
- title=_('Email Address'), required=False, constraint=email_validator,
- description=_("The upstream email address that this bug has been "
- "forwarded to."))
+ title=_("Email Address"),
+ required=False,
+ constraint=email_validator,
+ description=_(
+ "The upstream email address that this bug has been "
+ "forwarded to."
+ ),
+ )
class ProductBugTaskCreationStep(BugTaskCreationStep):
"""Specialized BugTaskCreationStep for reporting a bug in an upstream."""
template = ViewPageTemplateFile(
- '../templates/bugtask-requestfix-upstream.pt')
+ "../templates/bugtask-requestfix-upstream.pt"
+ )
label = "Confirm project"
- target_field_names = ('product', 'add_packaging')
- main_action_label = 'Add to Bug Report'
+ target_field_names = ("product", "add_packaging")
+ main_action_label = "Add to Bug Report"
schema = IAddBugTaskWithUpstreamLinkForm
custom_widget_link_upstream_how = CustomWidgetFactory(
- LaunchpadRadioWidget, _displayItemForMissingValue=False)
+ LaunchpadRadioWidget, _displayItemForMissingValue=False
+ )
custom_widget_bug_url = CustomWidgetFactory(
- StrippedTextWidget, displayWidth=42)
+ StrippedTextWidget, displayWidth=42
+ )
custom_widget_upstream_email_address_done = CustomWidgetFactory(
- StrippedTextWidget, displayWidth=42)
+ StrippedTextWidget, displayWidth=42
+ )
@property
def field_names(self):
- return ['link_upstream_how', 'upstream_email_address_done'] + (
- super().field_names)
+ return ["link_upstream_how", "upstream_email_address_done"] + (
+ super().field_names
+ )
def validate_widgets(self, data, names=None):
# The form is essentially just a radio group, with zero or one
@@ -591,13 +628,14 @@ class ProductBugTaskCreationStep(BugTaskCreationStep):
# A mapping from radio buttons to their related text widgets.
link_upstream_options = {
- LinkUpstreamHowOptions.LINK_UPSTREAM:
- 'bug_url',
- LinkUpstreamHowOptions.EMAIL_UPSTREAM_DONE:
- 'upstream_email_address_done'}
+ LinkUpstreamHowOptions.LINK_UPSTREAM: "bug_url",
+ LinkUpstreamHowOptions.EMAIL_UPSTREAM_DONE: (
+ "upstream_email_address_done"
+ ),
+ }
# Examine the radio group if it has valid input.
- link_upstream_how = self.widgets['link_upstream_how']
+ link_upstream_how = self.widgets["link_upstream_how"]
if link_upstream_how.hasValidInput():
link_upstream_how = link_upstream_how.getInputValue()
@@ -611,8 +649,7 @@ class ProductBugTaskCreationStep(BugTaskCreationStep):
# fields in the schema are set to required=False
# to make the radio+text-widget mechanism work.
if not self.widgets[name].getInputValue():
- self.setFieldError(
- name, 'Required input is missing.')
+ self.setFieldError(name, "Required input is missing.")
else:
# Don't validate these widgets when we don't yet know how
@@ -623,9 +660,9 @@ class ProductBugTaskCreationStep(BugTaskCreationStep):
def getTarget(self, data=None):
if data is not None:
- return data.get('product')
+ return data.get("product")
else:
- return self.widgets['product'].getInputValue()
+ return self.widgets["product"].getInputValue()
@cachedproperty
def link_upstream_how_items(self):
@@ -636,7 +673,7 @@ class ProductBugTaskCreationStep(BugTaskCreationStep):
widgets. We need to dig down a bit and place the individually
rendered radio buttons into our custom layout.
"""
- widget = self.widgets['link_upstream_how']
+ widget = self.widgets["link_upstream_how"]
try:
current_value = widget.getInputValue()
except MissingInputError:
@@ -648,33 +685,37 @@ class ProductBugTaskCreationStep(BugTaskCreationStep):
# link_upstream_how has _displayItemForMissingValue=False
# so that renderItems() doesn't return an extra radio button which
# prevents it from matching widget.vocabulary's ordering.
- return {entry.token: items[i]
- for i, entry in enumerate(widget.vocabulary)}
+ return {
+ entry.token: items[i] for i, entry in enumerate(widget.vocabulary)
+ }
def main_action(self, data):
- link_upstream_how = data.get('link_upstream_how')
+ link_upstream_how = data.get("link_upstream_how")
if link_upstream_how == LinkUpstreamHowOptions.UNLINKED_UPSTREAM:
# Erase bug_url because we don't want to create a bug
# watch against a specific URL.
- if 'bug_url' in data:
- del data['bug_url']
+ if "bug_url" in data:
+ del data["bug_url"]
elif link_upstream_how == LinkUpstreamHowOptions.EMAIL_UPSTREAM_DONE:
# Ensure there's a bug tracker for this email address.
- bug_url = 'mailto:' + data['upstream_email_address_done']
+ bug_url = "mailto:" + data["upstream_email_address_done"]
getUtility(IBugTrackerSet).ensureBugTracker(
- bug_url, self.user, BugTrackerType.EMAILADDRESS)
- data['bug_url'] = bug_url
- if data.get('add_packaging', False):
+ bug_url, self.user, BugTrackerType.EMAILADDRESS
+ )
+ data["bug_url"] = bug_url
+ if data.get("add_packaging", False):
# Create a packaging link so that Launchpad will suggest the
# upstream project to the user.
series = self.context.target.distribution.currentseries
if series:
getUtility(IPackagingUtil).createPackaging(
- productseries=data['product'].development_focus,
+ productseries=data["product"].development_focus,
sourcepackagename=self.context.target.sourcepackagename,
- distroseries=series, packaging=PackagingType.PRIME,
- owner=self.user)
+ distroseries=series,
+ packaging=PackagingType.PRIME,
+ owner=self.user,
+ )
return super().main_action(data)
@property
@@ -693,9 +734,12 @@ class ProductBugTaskCreationStep(BugTaskCreationStep):
bug = self.context.bug
title = bug.title
description = "Originally reported at:\n %s\n\n%s" % (
- canonical_url(bug), bug.description)
+ canonical_url(bug),
+ bug.description,
+ )
return target.bugtracker.getBugFilingAndSearchLinks(
- target.remote_product, title, description)
+ target.remote_product, title, description
+ )
class BugTrackerCreationStep(AlsoAffectsStep):
@@ -707,51 +751,60 @@ class BugTrackerCreationStep(AlsoAffectsStep):
"""
custom_widget_bug_url = CustomWidgetFactory(
- StrippedTextWidget, displayWidth=62)
+ StrippedTextWidget, displayWidth=62
+ )
step_name = "bugtracker_creation"
- main_action_label = 'Register Bug Tracker and Add to Bug Report'
+ main_action_label = "Register Bug Tracker and Add to Bug Report"
_next_step = None
def main_action(self, data):
- assert self._next_step is not None, (
- "_next_step must be specified in subclasses.")
- bug_url = data.get('bug_url').strip()
+ assert (
+ self._next_step is not None
+ ), "_next_step must be specified in subclasses."
+ bug_url = data.get("bug_url").strip()
try:
getUtility(IBugWatchSet).extractBugTrackerAndBug(bug_url)
except NoBugTrackerFound as error:
getUtility(IBugTrackerSet).ensureBugTracker(
- error.base_url, self.user, error.bugtracker_type)
+ error.base_url, self.user, error.bugtracker_type
+ )
self.next_step = self._next_step
class DistroBugTrackerCreationStep(BugTrackerCreationStep):
_next_step = DistroBugTaskCreationStep
- _field_names = ['distribution', 'sourcepackagename', 'bug_url']
+ _field_names = ["distribution", "sourcepackagename", "bug_url"]
custom_widget_distribution = CustomWidgetFactory(
- DropdownWidget, visible=False)
+ DropdownWidget, visible=False
+ )
custom_widget_sourcepackagename = CustomWidgetFactory(
- DropdownWidget, visible=False)
+ DropdownWidget, visible=False
+ )
label = "Also affects distribution/package"
template = ViewPageTemplateFile(
- '../templates/bugtask-confirm-bugtracker-creation.pt')
+ "../templates/bugtask-confirm-bugtracker-creation.pt"
+ )
class UpstreamBugTrackerCreationStep(BugTrackerCreationStep):
schema = IAddBugTaskWithUpstreamLinkForm
_next_step = ProductBugTaskCreationStep
- _field_names = ['product', 'bug_url', 'link_upstream_how']
+ _field_names = ["product", "bug_url", "link_upstream_how"]
custom_widget_product = CustomWidgetFactory(DropdownWidget, visible=False)
custom_widget_link_upstream_how = CustomWidgetFactory(
- LaunchpadRadioWidget, visible=False)
+ LaunchpadRadioWidget, visible=False
+ )
label = "Confirm project"
template = ViewPageTemplateFile(
- '../templates/bugtask-confirm-bugtracker-creation.pt')
+ "../templates/bugtask-confirm-bugtracker-creation.pt"
+ )
-class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
- LaunchpadFormView):
+class BugAlsoAffectsProductWithProductCreationView(
+ LinkPackgingMixin, LaunchpadFormView
+):
"""Register a product and indicate this bug affects it.
If there's no bugtracker with the given URL registered in Launchpad, then
@@ -761,7 +814,8 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
label = "Register project affected by this bug"
schema = IAddBugTaskWithProductCreationForm
custom_widget_bug_url = CustomWidgetFactory(
- StrippedTextWidget, displayWidth=62)
+ StrippedTextWidget, displayWidth=62
+ )
custom_widget_existing_product = LaunchpadRadioWidget
existing_products = None
MAX_PRODUCTS_TO_DISPLAY = 10
@@ -770,9 +824,9 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
@property
def field_names(self):
"""The fields needed to choose an existing project."""
- names = ['bug_url', 'display_name', 'name', 'summary']
+ names = ["bug_url", "display_name", "name", "summary"]
if self.can_link_package:
- names.append('add_packaging')
+ names.append("add_packaging")
return names
def _loadProductsUsingBugTracker(self):
@@ -783,7 +837,7 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
If there are too many products using that bugtracker then we'll store
only the first ones that somehow match the name given.
"""
- bug_url = self.request.form.get('field.bug_url')
+ bug_url = self.request.form.get("field.bug_url")
if not bug_url:
return
@@ -802,12 +856,16 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
# Use a local import as we don't want removeSecurityProxy used
# anywhere else.
from zope.security.proxy import removeSecurityProxy
+
name_matches = removeSecurityProxy(
- getUtility(IProductSet).search(self.user,
- self.request.form.get('field.name')))
+ getUtility(IProductSet).search(
+ self.user, self.request.form.get("field.name")
+ )
+ )
products = name_matches.find(Product.bugtracker == bugtracker.id)
self.existing_products = list(
- products[:self.MAX_PRODUCTS_TO_DISPLAY])
+ products[: self.MAX_PRODUCTS_TO_DISPLAY]
+ )
else:
# The bugtracker is registered in Launchpad but there are no
# products using it at the moment.
@@ -829,37 +887,44 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
for product in self.existing_products:
terms.append(SimpleTerm(product, product.name, product.title))
existing_product = form.FormField(
- Choice(__name__='existing_product',
- title=_("Existing project"), required=True,
- vocabulary=SimpleVocabulary(terms)))
+ Choice(
+ __name__="existing_product",
+ title=_("Existing project"),
+ required=True,
+ vocabulary=SimpleVocabulary(terms),
+ )
+ )
self.form_fields += form.Fields(existing_product)
- if 'field.existing_product' not in self.request.form:
+ if "field.existing_product" not in self.request.form:
# This is the first time the form is being submitted, so the
# request doesn't contain a value for the existing_product
# widget and thus we'll end up rendering an error message around
# said widget unless we sneak a value for it in our request.
- self.request.form['field.existing_product'] = terms[0].token
+ self.request.form["field.existing_product"] = terms[0].token
def validate_existing_product(self, action, data):
"""Check if the chosen project is not already affected by this bug."""
self._validate(action, data)
- project = data.get('existing_product')
+ project = data.get("existing_product")
try:
validate_target(self.context.bug, project)
except IllegalTarget as e:
- self.setFieldError('existing_product', e.args[0])
+ self.setFieldError("existing_product", e.args[0])
- @action('Use Existing Project', name='use_existing_product',
- validator=validate_existing_product)
+ @action(
+ "Use Existing Project",
+ name="use_existing_product",
+ validator=validate_existing_product,
+ )
def use_existing_product_action(self, action, data):
"""Record the chosen project as being affected by this bug.
Also creates a bugwatch for the given remote bug.
"""
- data['product'] = data['existing_product']
+ data["product"] = data["existing_product"]
self._createBugTaskAndWatch(data)
- @action('Continue', name='continue')
+ @action("Continue", name="continue")
def continue_action(self, action, data):
"""Create a new product and a bugtask for this bug on that product.
@@ -867,7 +932,7 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
other products registered in Launchpad, then we show these products to
the user and ask if they don't want to create the task in one of them.
"""
- if self.existing_products and not self.request.form.get('create_new'):
+ if self.existing_products and not self.request.form.get("create_new"):
# Present the projects using that bugtracker to the user as
# possible options to report the bug on. If there are too many
# projects using that bugtracker then show only the ones that
@@ -876,11 +941,14 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
# Products created through this view have DONT_KNOW licensing.
product = getUtility(IProductSet).createProduct(
owner=self.user,
- name=data['name'],
- display_name=data['display_name'], title=data['display_name'],
- summary=data['summary'], licenses=self.licenses,
- registrant=self.user)
- data['product'] = product
+ name=data["name"],
+ display_name=data["display_name"],
+ title=data["display_name"],
+ summary=data["summary"],
+ licenses=self.licenses,
+ registrant=self.user,
+ )
+ data["product"] = product
self._createBugTaskAndWatch(data, set_bugtracker=True)
# Now that the product is configured set the owner to be the registry
# experts team.
@@ -910,5 +978,5 @@ class BugAlsoAffectsProductWithProductCreationView(LinkPackgingMixin,
view.main_action(data)
if set_bugtracker:
- data['product'].bugtracker = view.task_added.bugwatch.bugtracker
+ data["product"].bugtracker = view.task_added.bugwatch.bugtracker
self.next_url = canonical_url(view.task_added)
diff --git a/lib/lp/bugs/browser/bugattachment.py b/lib/lp/bugs/browser/bugattachment.py
index 324c4e7..7d45e45 100644
--- a/lib/lp/bugs/browser/bugattachment.py
+++ b/lib/lp/bugs/browser/bugattachment.py
@@ -4,25 +4,19 @@
"""Bug attachment views."""
__all__ = [
- 'BugAttachmentContentCheck',
- 'BugAttachmentFileNavigation',
- 'BugAttachmentSetNavigation',
- 'BugAttachmentEditView',
- 'BugAttachmentURL',
- ]
+ "BugAttachmentContentCheck",
+ "BugAttachmentFileNavigation",
+ "BugAttachmentSetNavigation",
+ "BugAttachmentEditView",
+ "BugAttachmentURL",
+]
from lazr.restful.utils import smartquote
-from zope.component import (
- getMultiAdapter,
- getUtility,
- )
+from zope.component import getMultiAdapter, getUtility
from zope.contenttype import guess_content_type
from zope.interface import implementer
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
from lp.app.widgets.itemswidgets import LaunchpadBooleanRadioWidget
from lp.bugs.interfaces.bugattachment import (
BugAttachmentType,
@@ -30,58 +24,52 @@ from lp.bugs.interfaces.bugattachment import (
IBugAttachmentEditForm,
IBugAttachmentIsPatchConfirmationForm,
IBugAttachmentSet,
- )
+)
from lp.services.librarian.browser import (
FileNavigationMixin,
ProxiedLibraryFileAlias,
- )
+)
from lp.services.librarian.interfaces import ILibraryFileAliasWithParent
-from lp.services.webapp import (
- canonical_url,
- GetitemNavigation,
- Navigation,
- )
+from lp.services.webapp import GetitemNavigation, Navigation, canonical_url
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.escaping import structured
-from lp.services.webapp.interfaces import (
- ICanonicalUrlData,
- ILaunchBag,
- )
+from lp.services.webapp.interfaces import ICanonicalUrlData, ILaunchBag
class BugAttachmentContentCheck:
- """A mixin class that checks the consistency of patch flag and file type.
- """
+ """Mixin that checks the consistency of patch flag and file type."""
def guessContentType(self, filename, file_content):
"""Guess the content type a file with the given name and content."""
guessed_type, encoding = guess_content_type(
- name=filename, body=file_content)
+ name=filename, body=file_content
+ )
# Zope's guess_content_type() doesn't consider all the factors
# we want considered. So after we get its answer, we probe a
# little further. But we still don't look at the encoding nor
# the file content, because we'd like to avoid reimplementing
# 'patch'. See bug #538219 for more.
- if (guessed_type == 'text/plain'
- and (filename.endswith('.diff')
- or filename.endswith('.debdiff')
- or filename.endswith('.patch'))):
- guessed_type = 'text/x-diff'
+ if guessed_type == "text/plain" and (
+ filename.endswith(".diff")
+ or filename.endswith(".debdiff")
+ or filename.endswith(".patch")
+ ):
+ guessed_type = "text/x-diff"
return guessed_type
def attachmentTypeConsistentWithContentType(
- self, patch_flag_set, filename, file_content):
- """Return True iff patch_flag is consistent with filename and content.
- """
+ self, patch_flag_set, filename, file_content
+ ):
+ """True iff patch_flag is consistent with filename and content."""
guessed_type = self.guessContentType(filename, file_content)
# An XOR of "is the patch flag selected?" with "is the
# guessed type not a diff?" tells us if the type selected
# by the user matches the guessed type.
- return (patch_flag_set ^ (guessed_type != 'text/x-diff'))
+ return patch_flag_set ^ (guessed_type != "text/x-diff")
def nextUrlForInconsistentPatchFlags(self, attachment):
"""The next_url value used for an inconistent patch flag."""
- return canonical_url(attachment) + '/+confirm-is-patch'
+ return canonical_url(attachment) + "/+confirm-is-patch"
class BugAttachmentSetNavigation(GetitemNavigation):
@@ -96,7 +84,7 @@ class BugAttachmentSetNavigation(GetitemNavigation):
class BugAttachmentURL:
"""Bug URL creation rules."""
- rootsite = 'bugs'
+ rootsite = "bugs"
def __init__(self, context):
self.context = context
@@ -120,12 +108,13 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
"""Edit a bug attachment."""
schema = IBugAttachmentEditForm
- field_names = ['title', 'patch', 'contenttype']
+ field_names = ["title", "patch", "contenttype"]
def __init__(self, context, request):
LaunchpadFormView.__init__(self, context, request)
- self.next_url = self.cancel_url = (
- canonical_url(ICanonicalUrlData(context).inside))
+ self.next_url = self.cancel_url = canonical_url(
+ ICanonicalUrlData(context).inside
+ )
@property
def initial_values(self):
@@ -133,14 +122,15 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
return dict(
title=attachment.title,
patch=attachment.type == BugAttachmentType.PATCH,
- contenttype=attachment.libraryfile.mimetype)
+ contenttype=attachment.libraryfile.mimetype,
+ )
def canEditAttachment(self, action):
- return check_permission('launchpad.Edit', self.context)
+ return check_permission("launchpad.Edit", self.context)
- @action('Change', name='change', condition=canEditAttachment)
+ @action("Change", name="change", condition=canEditAttachment)
def change_action(self, action, data):
- if data['patch']:
+ if data["patch"]:
new_type = BugAttachmentType.PATCH
else:
new_type = BugAttachmentType.UNSPECIFIED
@@ -155,37 +145,46 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
# choice of the patch flag.
new_type_consistent_with_guessed_type = (
self.attachmentTypeConsistentWithContentType(
- new_type == BugAttachmentType.PATCH, filename,
- file_content))
+ new_type == BugAttachmentType.PATCH, filename, file_content
+ )
+ )
if new_type_consistent_with_guessed_type:
self.context.type = new_type
else:
self.next_url = self.nextUrlForInconsistentPatchFlags(
- self.context)
+ self.context
+ )
- if data['title'] != self.context.title:
- self.context.title = data['title']
+ if data["title"] != self.context.title:
+ self.context.title = data["title"]
- if self.context.libraryfile.mimetype != data['contenttype']:
+ if self.context.libraryfile.mimetype != data["contenttype"]:
lfa_with_parent = getMultiAdapter(
(self.context.libraryfile, self.context),
- ILibraryFileAliasWithParent)
- lfa_with_parent.mimetype = data['contenttype']
+ ILibraryFileAliasWithParent,
+ )
+ lfa_with_parent.mimetype = data["contenttype"]
- @action('Delete Attachment', name='delete', condition=canEditAttachment)
+ @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, name=self.context.title))
+ 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,
+ name=self.context.title,
+ )
+ )
self.context.removeFromBug(user=self.user)
@property
def label(self):
return smartquote('Edit attachment "%s"') % self.context.title
- page_title = 'Edit attachment'
+ page_title = "Edit attachment"
class BugAttachmentPatchConfirmationView(LaunchpadFormView):
@@ -203,27 +202,29 @@ class BugAttachmentPatchConfirmationView(LaunchpadFormView):
def __init__(self, context, request):
LaunchpadFormView.__init__(self, context, request)
- self.next_url = self.cancel_url = (
- canonical_url(ICanonicalUrlData(context).inside))
+ self.next_url = self.cancel_url = canonical_url(
+ ICanonicalUrlData(context).inside
+ )
def initialize(self):
super().initialize()
- self.widgets['patch'].setRenderedValue(self.is_patch)
+ self.widgets["patch"].setRenderedValue(self.is_patch)
@property
def label(self):
return smartquote('Confirm attachment type of "%s"') % (
- self.context.title)
+ self.context.title
+ )
- page_title = 'Confirm attachment type'
+ page_title = "Confirm attachment type"
- @action('Change', name='change')
+ @action("Change", name="change")
def change_action(self, action, data):
current_patch_setting = self.context.type == BugAttachmentType.PATCH
- if data['patch'] != current_patch_setting:
- if data['patch']:
+ if data["patch"] != current_patch_setting:
+ if data["patch"]:
self.context.type = BugAttachmentType.PATCH
- #xxxxxxxxxx adjust content type!
+ # xxxxxxxxxx adjust content type!
# xxx use mixin, together with BugAttachmnetEditView
else:
self.context.type = BugAttachmentType.UNSPECIFIED
diff --git a/lib/lp/bugs/browser/bugbranch.py b/lib/lp/bugs/browser/bugbranch.py
index e7ea7ab..a2a650d 100644
--- a/lib/lp/bugs/browser/bugbranch.py
+++ b/lib/lp/bugs/browser/bugbranch.py
@@ -4,58 +4,49 @@
"""Browser view classes for BugBranch-related objects."""
__all__ = [
- 'BranchLinkToBugView',
- 'BugBranchAddView',
- 'BugBranchDeleteView',
- 'BugBranchView',
- ]
+ "BranchLinkToBugView",
+ "BugBranchAddView",
+ "BugBranchDeleteView",
+ "BugBranchView",
+]
from lazr.restful.interfaces import IWebServiceClientRequest
-from zope.component import (
- adapter,
- getMultiAdapter,
- )
-from zope.interface import (
- implementer,
- Interface,
- )
+from zope.component import adapter, getMultiAdapter
+from zope.interface import Interface, implementer
from lp import _
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
- )
+ action,
+)
from lp.bugs.interfaces.bugbranch import IBugBranch
from lp.code.browser.branchmergeproposal import (
latest_proposals_for_each_branch,
- )
+)
from lp.code.enums import BranchLifecycleStatus
from lp.services.propertycache import cachedproperty
-from lp.services.webapp import (
- canonical_url,
- LaunchpadView,
- )
+from lp.services.webapp import LaunchpadView, canonical_url
class BugBranchAddView(LaunchpadFormView):
"""Browser view for linking a bug to a branch."""
+
schema = IBugBranch
# In order to have the branch field rendered using the appropriate
# widget, we set the LaunchpadFormView attribute for_input to True
# to get the read only fields rendered as input widgets.
for_input = True
- page_title = 'Add branch'
- field_names = ['branch']
+ page_title = "Add branch"
+ field_names = ["branch"]
- @action(_('Continue'), name='continue')
+ @action(_("Continue"), name="continue")
def continue_action(self, action, data):
- branch = data['branch']
- self.context.bug.linkBranch(
- branch=branch, registrant=self.user)
+ branch = data["branch"]
+ self.context.bug.linkBranch(branch=branch, registrant=self.user)
self.request.response.addNotification(
- "Successfully registered branch %s for this bug." %
- branch.name)
+ "Successfully registered branch %s for this bug." % branch.name
+ )
@property
def next_url(self):
@@ -63,13 +54,14 @@ class BugBranchAddView(LaunchpadFormView):
@property
def label(self):
- return 'Add a branch to bug #%i' % self.context.bug.id
+ return "Add a branch to bug #%i" % self.context.bug.id
cancel_url = next_url
class BugBranchDeleteView(LaunchpadEditFormView):
"""View to update a BugBranch."""
+
schema = IBugBranch
field_names = []
@@ -83,11 +75,11 @@ class BugBranchDeleteView(LaunchpadEditFormView):
cancel_url = next_url
- @action('Remove link', name='delete')
+ @action("Remove link", name="delete")
def delete_action(self, action, data):
self.context.bug.unlinkBranch(self.context.branch, self.user)
- label = 'Remove bug branch link'
+ label = "Remove bug branch link"
class BugBranchView(LaunchpadView):
@@ -103,19 +95,22 @@ class BugBranchView(LaunchpadView):
def show_branch_status(self):
"""Show the branch status if merged and there are no proposals."""
lifecycle_status = self.context.branch.lifecycle_status
- return (len(self.merge_proposals) == 0 and
- lifecycle_status == BranchLifecycleStatus.MERGED)
+ return (
+ len(self.merge_proposals) == 0
+ and lifecycle_status == BranchLifecycleStatus.MERGED
+ )
class BranchLinkToBugView(LaunchpadFormView):
"""The view to create bug-branch links."""
+
schema = IBugBranch
# In order to have the bug field rendered using the appropriate
# widget, we set the LaunchpadFormView attribute for_input to True
# to get the read only fields rendered as input widgets.
for_input = True
- field_names = ['bug']
+ field_names = ["bug"]
@property
def label(self):
@@ -123,7 +118,7 @@ class BranchLinkToBugView(LaunchpadFormView):
@property
def page_title(self):
- return 'Link branch %s to a bug report' % self.context.displayname
+ return "Link branch %s to a bug report" % self.context.displayname
@property
def next_url(self):
@@ -131,9 +126,9 @@ class BranchLinkToBugView(LaunchpadFormView):
cancel_url = next_url
- @action(_('Continue'), name='continue')
+ @action(_("Continue"), name="continue")
def continue_action(self, action, data):
- bug = data['bug']
+ bug = data["bug"]
bug.linkBranch(branch=self.context, registrant=self.user)
@@ -147,5 +142,6 @@ class BugBranchXHTMLRepresentation:
def __call__(self):
"""Render `BugBranch` as XHTML using the webservice."""
branch_view = getMultiAdapter(
- (self.branch, self.request), name="+bug-branch")
+ (self.branch, self.request), name="+bug-branch"
+ )
return branch_view()
diff --git a/lib/lp/bugs/browser/bugcomment.py b/lib/lp/bugs/browser/bugcomment.py
index 5d2d087..b8650ca 100644
--- a/lib/lp/bugs/browser/bugcomment.py
+++ b/lib/lp/bugs/browser/bugcomment.py
@@ -4,34 +4,24 @@
"""Bug comment browser view classes."""
__all__ = [
- 'BugComment',
- 'BugCommentBoxExpandedReplyView',
- 'BugCommentBoxView',
- 'BugCommentBreadcrumb',
- 'BugCommentView',
- 'BugCommentXHTMLRepresentation',
- 'build_comments_from_chunks',
- 'group_comments_with_activity',
- ]
+ "BugComment",
+ "BugCommentBoxExpandedReplyView",
+ "BugCommentBoxView",
+ "BugCommentBreadcrumb",
+ "BugCommentView",
+ "BugCommentXHTMLRepresentation",
+ "build_comments_from_chunks",
+ "group_comments_with_activity",
+]
from datetime import timedelta
-from itertools import (
- chain,
- groupby,
- )
+from itertools import chain, groupby
from operator import itemgetter
from lazr.delegates import delegate_to
from lazr.restful.interfaces import IWebServiceClientRequest
-from zope.component import (
- adapter,
- getMultiAdapter,
- getUtility,
- )
-from zope.interface import (
- implementer,
- Interface,
- )
+from zope.component import adapter, getMultiAdapter, getUtility
+from zope.interface import Interface, implementer
from zope.security.proxy import removeSecurityProxy
from lp.bugs.interfaces.bugattachment import BugAttachmentType
@@ -41,29 +31,26 @@ 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.propertycache import cachedproperty, get_property_cache
from lp.services.webapp import (
- canonical_url,
LaunchpadView,
Navigation,
+ canonical_url,
stepthrough,
- )
+)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.interfaces import ILaunchBag
-
COMMENT_ACTIVITY_GROUPING_WINDOW = timedelta(minutes=5)
class BugCommentNavigation(Navigation):
"""Navigation for the `IBugComment`."""
+
usedfor = IBugComment
- @stepthrough('revisions')
+ @stepthrough("revisions")
def traverse_revisions(self, revision):
try:
revision = int(revision)
@@ -73,8 +60,13 @@ class BugCommentNavigation(Navigation):
def build_comments_from_chunks(
- bugtask, truncate=False, slice_info=None, show_spam_controls=False,
- user=None, hide_first=False):
+ bugtask,
+ truncate=False,
+ slice_info=None,
+ show_spam_controls=False,
+ user=None,
+ hide_first=False,
+):
"""Build BugComments from MessageChunks.
:param truncate: Perform truncation of large messages.
@@ -85,7 +77,7 @@ def build_comments_from_chunks(
comments = {}
for bugmessage, message, chunk in chunks:
cache = get_property_cache(message)
- if getattr(cache, 'chunks', None) is None:
+ if getattr(cache, "chunks", None) is None:
cache.chunks = []
# Soft-deleted messages will have None chunk here. Skip cache
# filling in this case.
@@ -94,15 +86,19 @@ def build_comments_from_chunks(
bug_comment = comments.get(message.id)
if bug_comment is None:
if bugmessage.index == 0 and hide_first:
- display = 'hide'
+ display = "hide"
elif truncate:
- display = 'truncate'
+ display = "truncate"
else:
- display = 'full'
+ display = "full"
bug_comment = BugComment(
- bugmessage.index, message, bugtask,
- show_spam_controls=show_spam_controls, user=user,
- display=display)
+ bugmessage.index,
+ message,
+ bugtask,
+ show_spam_controls=show_spam_controls,
+ user=user,
+ display=display,
+ )
comments[message.id] = bug_comment
# This code path is currently only used from a BugTask view which
# has already loaded all the bug watches. If we start lazy loading
@@ -111,7 +107,8 @@ def build_comments_from_chunks(
if bugmessage.bugwatch_id is not None:
bug_comment.bugwatch = bugmessage.bugwatch
bug_comment.synchronized = (
- bugmessage.remote_comment_id is not None)
+ bugmessage.remote_comment_id is not None
+ )
return comments
@@ -133,14 +130,26 @@ def group_comments_with_activity(comments, activities):
else:
max_index = 0
comments = (
- (comment.datecreated, comment.index,
- comment.owner, comment_kind, comment)
- for comment in comments)
+ (
+ comment.datecreated,
+ comment.index,
+ comment.owner,
+ comment_kind,
+ comment,
+ )
+ for comment in comments
+ )
activity_kind = "activity"
activity = (
- (activity.datechanged, max_index,
- activity.person, activity_kind, activity)
- for activity in activities)
+ (
+ activity.datechanged,
+ max_index,
+ activity.person,
+ activity_kind,
+ activity,
+ )
+ for activity in activities
+ )
# when an action and a comment happen at the same time, the action comes
# second, when two events are tied the comment index is used to
@@ -166,11 +175,14 @@ def group_comments_with_activity(comments, activities):
for date, _, actor, kind, event in events:
window_ended = (
# A window may contain only one comment.
- (window_comment is not None and kind is comment_kind) or
+ (window_comment is not None and kind is comment_kind)
+ or
# All events must have happened within a given timeframe.
- (window_end is None or date >= window_end) or
+ (window_end is None or date >= window_end)
+ or
# All events within the window must belong to the same actor.
- (window_actor is None or actor != window_actor))
+ (window_actor is None or actor != window_actor)
+ )
if window_ended:
window_comment, window_actor = None, actor
window_index, window_end = window_index + 1, date + window
@@ -181,14 +193,15 @@ def group_comments_with_activity(comments, activities):
event_windows = gen_event_windows(events)
event_windows_grouper = groupby(event_windows, itemgetter(0))
for window_index, window_group in event_windows_grouper:
- window_group = [
- (kind, event) for (index, kind, event) in window_group]
+ window_group = [(kind, event) for (index, kind, event) in window_group]
for kind, event in window_group:
if kind is comment_kind:
window_comment = event
window_comment.activity.extend(
- event for (kind, event) in window_group
- if kind is activity_kind)
+ event
+ for (kind, event) in window_group
+ if kind is activity_kind
+ )
yield window_comment
# There's only one comment per window.
break
@@ -197,7 +210,7 @@ def group_comments_with_activity(comments, activities):
@implementer(IBugComment)
-@delegate_to(IMessage, context='_message')
+@delegate_to(IMessage, context="_message")
class BugComment(MessageComment):
"""Data structure that holds all data pertaining to a bug comment.
@@ -211,9 +224,16 @@ class BugComment(MessageComment):
"""
def __init__(
- self, index, message, bugtask, activity=None,
- show_spam_controls=False, user=None, display='full'):
- if display == 'truncate':
+ self,
+ index,
+ message,
+ bugtask,
+ activity=None,
+ show_spam_controls=False,
+ user=None,
+ display="full",
+ ):
+ if display == "truncate":
comment_limit = config.malone.max_comment_size
else:
comment_limit = None
@@ -237,12 +257,15 @@ class BugComment(MessageComment):
# We use a feature flag to control users deleting their own comments.
user_owns_comment = user is not None and user == self.owner
self.show_spam_controls = show_spam_controls or user_owns_comment
- self.hide_text = (display == 'hide')
+ self.hide_text = display == "hide"
@cachedproperty
def bugattachments(self):
- return [attachment for attachment in self._message.bugattachments if
- attachment.type != BugAttachmentType.PATCH]
+ return [
+ attachment
+ for attachment in self._message.bugattachments
+ if attachment.type != BugAttachmentType.PATCH
+ ]
@property
def show_for_admin(self):
@@ -259,7 +282,7 @@ class BugComment(MessageComment):
@cachedproperty
def text_for_display(self):
if self.hide_text:
- return ''
+ return ""
else:
return super().text_for_display
@@ -273,8 +296,12 @@ class BugComment(MessageComment):
return False
if self.title != other.title:
return False
- if (self.bugattachments or self.patches or other.bugattachments or
- other.patches):
+ if (
+ self.bugattachments
+ or self.patches
+ or other.bugattachments
+ or other.patches
+ ):
# We shouldn't collapse comments which have attachments;
# there's really no possible identity in that case.
return False
@@ -283,23 +310,24 @@ class BugComment(MessageComment):
def isEmpty(self):
"""Return True if text_for_display is empty."""
- return (len(self.text_for_display) == 0 and
- len(self.bugattachments) == 0 and len(self.patches) == 0)
+ return (
+ len(self.text_for_display) == 0
+ and len(self.bugattachments) == 0
+ and len(self.patches) == 0
+ )
@property
def add_comment_url(self):
- return canonical_url(self.bugtask, view_name='+addcomment')
+ return canonical_url(self.bugtask, view_name="+addcomment")
@property
def download_url(self):
- return canonical_url(self, view_name='+download')
+ return canonical_url(self, view_name="+download")
@property
def show_activity(self):
"""Return True if the activity should be shown for this comment."""
- return bool(
- len(self.activity) > 0 or
- self.bugwatch)
+ return bool(len(self.activity) > 0 or self.bugwatch)
class BugCommentView(LaunchpadView):
@@ -326,8 +354,10 @@ class BugCommentView(LaunchpadView):
return self.comment.show_spam_controls
def page_title(self):
- return 'Comment %d for bug %d' % (
- self.comment.index, self.context.bug.id)
+ return "Comment %d for bug %d" % (
+ self.comment.index,
+ self.context.bug.id,
+ )
@property
def page_description(self):
@@ -339,10 +369,11 @@ class BugCommentBoxViewMixin:
@property
def show_spam_controls(self):
- if hasattr(self.context, 'show_spam_controls'):
+ if hasattr(self.context, "show_spam_controls"):
return self.context.show_spam_controls
- elif (hasattr(self, 'comment') and
- hasattr(self.comment, 'show_spam_controls')):
+ elif hasattr(self, "comment") and hasattr(
+ self.comment, "show_spam_controls"
+ ):
return self.comment.show_spam_controls
else:
return False
@@ -350,7 +381,8 @@ class BugCommentBoxViewMixin:
def proxiedUrlOfLibraryFileAlias(self, attachment):
"""Return the proxied URL for the Librarian file of the attachment."""
return ProxiedLibraryFileAlias(
- attachment.libraryfile, attachment).http_url
+ attachment.libraryfile, attachment
+ ).http_url
class BugCommentBoxView(LaunchpadView, BugCommentBoxViewMixin):
@@ -360,7 +392,7 @@ class BugCommentBoxView(LaunchpadView, BugCommentBoxViewMixin):
@property
def can_edit(self):
- return check_permission('launchpad.Edit', self.context)
+ return check_permission("launchpad.Edit", self.context)
class BugCommentBoxExpandedReplyView(LaunchpadView, BugCommentBoxViewMixin):
@@ -383,7 +415,8 @@ class BugCommentXHTMLRepresentation:
def __call__(self):
"""Render `BugComment` as XHTML using the webservice."""
comment_view = getMultiAdapter(
- (self.comment, self.request), name="+box")
+ (self.comment, self.request), name="+box"
+ )
return comment_view()
diff --git a/lib/lp/bugs/browser/buglinktarget.py b/lib/lp/bugs/browser/buglinktarget.py
index f3e2655..9dda500 100644
--- a/lib/lp/bugs/browser/buglinktarget.py
+++ b/lib/lp/bugs/browser/buglinktarget.py
@@ -4,10 +4,10 @@
"""Views for IBugLinkTarget."""
__all__ = [
- 'BugLinkView',
- 'BugLinksListingView',
- 'BugsUnlinkView',
- ]
+ "BugLinkView",
+ "BugLinksListingView",
+ "BugsUnlinkView",
+]
from collections import defaultdict
@@ -19,23 +19,14 @@ from zope.interface import providedBy
from zope.security.interfaces import Unauthorized
from lp import _
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
from lp.bugs.browser.buglisting import BugListingBatchNavigator
-from lp.bugs.interfaces.buglink import (
- IBugLinkForm,
- IUnlinkBugsForm,
- )
+from lp.bugs.interfaces.buglink import IBugLinkForm, IUnlinkBugsForm
from lp.bugs.interfaces.bugtask import IBugTaskSet
from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
from lp.services.config import config
-from lp.services.propertycache import (
- cachedproperty,
- get_property_cache,
- )
+from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.searchbuilder import any
from lp.services.webapp import canonical_url
from lp.services.webapp.authorization import check_permission
@@ -45,41 +36,46 @@ from lp.services.webapp.publisher import LaunchpadView
class BugLinkView(LaunchpadFormView):
"""This view is used to link bugs to any IBugLinkTarget."""
- label = _('Link a bug report')
+ label = _("Link a bug report")
schema = IBugLinkForm
page_title = label
- focused_element_id = 'bug'
+ focused_element_id = "bug"
@property
def cancel_url(self):
"""See `LaunchpadFormview`."""
return canonical_url(self.context)
- @action(_('Link'))
+ @action(_("Link"))
def linkBug(self, action, data):
"""Link to the requested bug. Publish an ObjectModifiedEvent and
display a notification.
"""
response = self.request.response
target_unmodified = Snapshot(
- self.context, providing=providedBy(self.context))
- bug = data['bug']
+ self.context, providing=providedBy(self.context)
+ )
+ bug = data["bug"]
try:
self.context.linkBug(bug, user=self.user)
except Unauthorized:
# XXX flacoste 2006-08-23 bug=57470: This should use proper _().
self.setFieldError(
- 'bug',
- 'You are not allowed to link to private bug #%d.' % bug.id)
+ "bug",
+ "You are not allowed to link to private bug #%d." % bug.id,
+ )
return
- bug_props = {'bugid': bug.id, 'title': bug.title}
+ bug_props = {"bugid": bug.id, "title": bug.title}
response.addNotification(
- _('Added link to bug #$bugid: '
- '\N{left double quotation mark}$title'
- '\N{right double quotation mark}.', mapping=bug_props))
- notify(ObjectModifiedEvent(
- self.context, target_unmodified, ['bugs']))
+ _(
+ "Added link to bug #$bugid: "
+ "\N{left double quotation mark}$title"
+ "\N{right double quotation mark}.",
+ mapping=bug_props,
+ )
+ )
+ notify(ObjectModifiedEvent(self.context, target_unmodified, ["bugs"]))
self.next_url = canonical_url(self.context)
@@ -107,29 +103,39 @@ class BugLinksListingView(LaunchpadView):
tags = bugtask_set.getBugTaskTags(bugtasks)
people = bugtask_set.getBugTaskPeople(bugtasks)
links = []
- columns_to_show = ["id", "summary", "bugtargetdisplayname",
- "importance", "status"]
+ columns_to_show = [
+ "id",
+ "summary",
+ "bugtargetdisplayname",
+ "importance",
+ "status",
+ ]
for bug, tasks in bugs.items():
- navigator = BugListingBatchNavigator(tasks, self.request,
+ navigator = BugListingBatchNavigator(
+ tasks,
+ self.request,
columns_to_show=columns_to_show,
- size=config.malone.buglist_batch_size)
+ size=config.malone.buglist_batch_size,
+ )
get_property_cache(navigator).bug_badge_properties = badges
get_property_cache(navigator).tags_for_batch = tags
get_property_cache(navigator).bugtask_people = people
- links.append({
- 'bug': bug,
- 'title': bug.title,
- 'can_view_bug': True,
- 'tasks': tasks,
- 'batch_navigator': navigator,
- })
+ links.append(
+ {
+ "bug": bug,
+ "title": bug.title,
+ "can_view_bug": True,
+ "tasks": tasks,
+ "batch_navigator": navigator,
+ }
+ )
return links
class BugsUnlinkView(LaunchpadFormView):
"""This view is used to remove bug links from any IBugLinkTarget."""
- label = _('Remove links to bug reports')
+ label = _("Remove links to bug reports")
schema = IUnlinkBugsForm
custom_widget_bugs = LabeledMultiCheckBoxWidget
page_title = label
@@ -139,27 +145,35 @@ class BugsUnlinkView(LaunchpadFormView):
"""See `LaunchpadFormview`."""
return canonical_url(self.context)
- @action(_('Remove'))
+ @action(_("Remove"))
def unlinkBugs(self, action, data):
response = self.request.response
target_unmodified = Snapshot(
- self.context, providing=providedBy(self.context))
- for bug in data['bugs']:
- replacements = {'bugid': bug.id}
+ self.context, providing=providedBy(self.context)
+ )
+ for bug in data["bugs"]:
+ replacements = {"bugid": bug.id}
try:
self.context.unlinkBug(bug, user=self.user)
response.addNotification(
- _('Removed link to bug #$bugid.', mapping=replacements))
+ _("Removed link to bug #$bugid.", mapping=replacements)
+ )
except Unauthorized:
response.addErrorNotification(
- _('Cannot remove link to private bug #$bugid.',
- mapping=replacements))
- notify(ObjectModifiedEvent(self.context, target_unmodified, ['bugs']))
+ _(
+ "Cannot remove link to private bug #$bugid.",
+ mapping=replacements,
+ )
+ )
+ notify(ObjectModifiedEvent(self.context, target_unmodified, ["bugs"]))
self.next_url = canonical_url(self.context)
def bugsWithPermission(self):
"""Return the bugs that the user has permission to remove. This
exclude private bugs to which the user doesn't have any permission.
"""
- return [bug for bug in self.context.bugs
- if check_permission('launchpad.View', bug)]
+ return [
+ bug
+ for bug in self.context.bugs
+ if check_permission("launchpad.View", bug)
+ ]
diff --git a/lib/lp/bugs/browser/buglisting.py b/lib/lp/bugs/browser/buglisting.py
index 4fbb813..2007a51 100644
--- a/lib/lp/bugs/browser/buglisting.py
+++ b/lib/lp/bugs/browser/buglisting.py
@@ -4,48 +4,36 @@
"""IBugTask-related browser views."""
__all__ = [
- 'BugNominationsView',
- 'BugListingBatchNavigator',
- 'BugListingPortletInfoView',
- 'BugListingPortletStatsView',
- 'BugsBugTaskSearchListingView',
- 'BugTargetView',
- 'BugTaskExpirableListingView',
- 'BugTaskListingItem',
- 'BugTaskListingView',
- 'BugTaskSearchListingView',
- 'get_buglisting_search_filter_url',
- 'get_sortorder_from_request',
- 'TextualBugTaskSearchListingView',
- ]
+ "BugNominationsView",
+ "BugListingBatchNavigator",
+ "BugListingPortletInfoView",
+ "BugListingPortletStatsView",
+ "BugsBugTaskSearchListingView",
+ "BugTargetView",
+ "BugTaskExpirableListingView",
+ "BugTaskListingItem",
+ "BugTaskListingView",
+ "BugTaskSearchListingView",
+ "get_buglisting_search_filter_url",
+ "get_sortorder_from_request",
+ "TextualBugTaskSearchListingView",
+]
import os.path
-from urllib.parse import (
- parse_qs,
- parse_qsl,
- quote,
- urlencode,
- )
+from urllib.parse import parse_qs, parse_qsl, quote, urlencode
+import pystache
from lazr.delegates import delegate_to
from lazr.restful.interfaces import IJSONRequestCache
from lazr.uri import URI
-import pystache
from simplejson import dumps
from simplejson.encoder import JSONEncoderForHTML
from zope.authentication.interfaces import IUnauthenticatedPrincipal
from zope.browserpage import ViewPageTemplateFile
-from zope.component import (
- getAdapter,
- getUtility,
- queryMultiAdapter,
- )
+from zope.component import getAdapter, getUtility, queryMultiAdapter
from zope.formlib.interfaces import InputErrors
from zope.formlib.itemswidgets import RadioWidget
-from zope.interface import (
- implementer,
- Interface,
- )
+from zope.interface import Interface, implementer
from zope.schema.vocabulary import getVocabularyRegistry
from zope.security.proxy import isinstance as zope_isinstance
from zope.traversing.interfaces import IPathAdapter
@@ -58,58 +46,52 @@ from lp.app.browser.tales import (
BugTrackerFormatterAPI,
DateTimeFormatterAPI,
PersonFormatterAPI,
- )
-from lp.app.enums import (
- InformationType,
- ServiceUsage,
- )
-from lp.app.errors import (
- NotFoundError,
- UnexpectedFormData,
- )
+)
+from lp.app.enums import InformationType, ServiceUsage
+from lp.app.errors import NotFoundError, UnexpectedFormData
from lp.app.interfaces.launchpad import (
IHeadingContext,
IPrivacy,
IServiceUsage,
- )
+)
from lp.app.vocabularies import InformationTypeVocabulary
from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
from lp.app.widgets.popup import PersonPickerWidget
from lp.app.widgets.project import ProjectScopeWidget
from lp.bugs.browser.structuralsubscription import (
expose_structural_subscription_data_to_js,
- )
+)
from lp.bugs.browser.widgets.bug import BugTagsWidget
from lp.bugs.browser.widgets.bugtask import NewLineToSpacesWidget
from lp.bugs.interfaces.bug import IBugSet
from lp.bugs.interfaces.bugattachment import BugAttachmentType
from lp.bugs.interfaces.bugtask import (
+ UNRESOLVED_BUGTASK_STATUSES,
BugTaskImportance,
BugTaskStatus,
BugTaskStatusSearch,
BugTaskStatusSearchDisplay,
IBugTask,
IBugTaskSet,
- UNRESOLVED_BUGTASK_STATUSES,
- )
+)
from lp.bugs.interfaces.bugtasksearch import (
+ DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY,
BugBlueprintSearch,
BugBranchSearch,
BugTagsSearchCombinator,
BugTaskSearchParams,
- DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY,
IBugTaskSearch,
IFrontPageBugTaskSearch,
IPersonBugTaskSearch,
IUpstreamProductBugTaskSearch,
- )
+)
from lp.bugs.interfaces.bugtracker import IHasExternalBugTracker
from lp.bugs.interfaces.malone import IMaloneApplication
from lp.layers import FeedsLayer
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
- )
+)
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPerson
@@ -118,33 +100,25 @@ from lp.registry.interfaces.productseries import IProductSeries
from lp.registry.interfaces.projectgroup import IProjectGroup
from lp.registry.interfaces.sourcepackage import ISourcePackage
from lp.services.config import config
-from lp.services.feeds.browser import (
- BugTargetLatestBugsFeedLink,
- FeedsMixin,
- )
+from lp.services.feeds.browser import BugTargetLatestBugsFeedLink, FeedsMixin
from lp.services.helpers import shortlist
from lp.services.propertycache import cachedproperty
-from lp.services.searchbuilder import (
- all,
- any,
- NULL,
- )
+from lp.services.searchbuilder import NULL, all, any
from lp.services.utils import obfuscate_structure
from lp.services.webapp import (
- canonical_url,
- enabled_with_permission,
LaunchpadView,
Link,
NavigationMenu,
- )
+ canonical_url,
+ enabled_with_permission,
+)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.batching import (
- get_batch_properties_for_json_cache,
TableBatchNavigator,
- )
+ get_batch_properties_for_json_cache,
+)
from lp.services.webapp.interfaces import ILaunchBag
-
vocabulary_registry = getVocabularyRegistry()
DISPLAY_BUG_STATUS_FOR_PATCHES = {
@@ -162,7 +136,7 @@ DISPLAY_BUG_STATUS_FOR_PATCHES = {
BugTaskStatus.DOESNOTEXIST: False,
BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: True,
BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: True,
- }
+}
def get_sortorder_from_request(request):
@@ -181,20 +155,20 @@ def get_sortorder_from_request(request):
... LaunchpadTestRequest(form={'orderby': 'priority,-severity'}))
['-importance']
"""
- order_by_string = request.get("orderby", '')
+ order_by_string = request.get("orderby", "")
if order_by_string:
if not zope_isinstance(order_by_string, list):
- order_by = order_by_string.split(',')
+ order_by = order_by_string.split(",")
else:
order_by = order_by_string
else:
order_by = []
# Remove old order_by values that people might have in bookmarks.
- for old_order_by_column in ['priority', 'severity']:
+ for old_order_by_column in ["priority", "severity"]:
if old_order_by_column in order_by:
order_by.remove(old_order_by_column)
- if '-' + old_order_by_column in order_by:
- order_by.remove('-' + old_order_by_column)
+ if "-" + old_order_by_column in order_by:
+ order_by.remove("-" + old_order_by_column)
if order_by:
return order_by
else:
@@ -217,14 +191,15 @@ def get_default_search_params(user):
"""
return BugTaskSearchParams(
- user=user, status=any(*UNRESOLVED_BUGTASK_STATUSES), omit_dupes=True)
+ user=user, status=any(*UNRESOLVED_BUGTASK_STATUSES), omit_dupes=True
+ )
OLD_BUGTASK_STATUS_MAP = {
- 'Unconfirmed': 'New',
- 'Needs Info': 'Incomplete',
- 'Rejected': 'Invalid',
- }
+ "Unconfirmed": "New",
+ "Needs Info": "Incomplete",
+ "Rejected": "Invalid",
+}
def rewrite_old_bugtask_status_query_string(query_string):
@@ -235,11 +210,12 @@ def rewrite_old_bugtask_status_query_string(query_string):
query string.
"""
query_elements = parse_qsl(
- query_string, keep_blank_values=True, strict_parsing=False)
+ query_string, keep_blank_values=True, strict_parsing=False
+ )
query_elements_mapped = []
for name, value in query_elements:
- if name == 'field.status:list':
+ if name == "field.status:list":
value = OLD_BUGTASK_STATUS_MAP.get(value, value)
query_elements_mapped.append((name, value))
@@ -268,6 +244,7 @@ def target_has_expirable_bugs_listing(target):
class BugTaskListingView(LaunchpadView):
"""A view designed for displaying bug tasks in lists."""
+
# Note that this right now is only used in tests and to render
# status in the CVEReportView. It may be a candidate for refactoring
# or removal.
@@ -283,14 +260,13 @@ class BugTaskListingView(LaunchpadView):
status_title = status.title.capitalize()
if not assignee:
- return status_title + ' (unassigned)'
- assignee_html = PersonFormatterAPI(assignee).link('+assignedbugs')
+ return status_title + " (unassigned)"
+ assignee_html = PersonFormatterAPI(assignee).link("+assignedbugs")
- if status in (BugTaskStatus.INVALID,
- BugTaskStatus.FIXCOMMITTED):
- return '%s by %s' % (status_title, assignee_html)
+ if status in (BugTaskStatus.INVALID, BugTaskStatus.FIXCOMMITTED):
+ return "%s by %s" % (status_title, assignee_html)
else:
- return '%s, assigned to %s' % (status_title, assignee_html)
+ return "%s, assigned to %s" % (status_title, assignee_html)
@property
def status_elsewhere(self):
@@ -303,12 +279,18 @@ class BugTaskListingView(LaunchpadView):
return "not filed elsewhere"
fixes_found = len(
- [task for task in related_tasks
- if task.status in (BugTaskStatus.FIXCOMMITTED,
- BugTaskStatus.FIXRELEASED)])
+ [
+ task
+ for task in related_tasks
+ if task.status
+ in (BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED)
+ ]
+ )
if fixes_found:
return "fixed in %d of %d places" % (
- fixes_found, len(bugtask.bug.bugtasks))
+ fixes_found,
+ len(bugtask.bug.bugtasks),
+ )
elif len(related_tasks) == 1:
return "filed in 1 other place"
else:
@@ -326,19 +308,23 @@ class BugsInfoMixin:
def bugs_fixed_elsewhere_url(self):
"""A URL to a list of bugs fixed elsewhere."""
return "%s?field.status_upstream=resolved_upstream" % (
- canonical_url(self.context, view_name='+bugs'))
+ canonical_url(self.context, view_name="+bugs")
+ )
@property
def open_cve_bugs_url(self):
"""A URL to a list of open bugs linked to CVEs."""
return "%s?field.has_cve=on" % (
- canonical_url(self.context, view_name='+bugs'))
+ canonical_url(self.context, view_name="+bugs")
+ )
@property
def open_cve_bugs_has_report(self):
"""Whether or not the context has a CVE report page."""
- return queryMultiAdapter(
- (self.context, self.request), name='+cve') is not None
+ return (
+ queryMultiAdapter((self.context, self.request), name="+cve")
+ is not None
+ )
@property
def pending_bugwatches_url(self):
@@ -351,7 +337,8 @@ class BugsInfoMixin:
if self.context.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
return None
return "%s?field.status_upstream=pending_bugwatch" % (
- canonical_url(self.context, view_name='+bugs'))
+ canonical_url(self.context, view_name="+bugs")
+ )
@property
def expirable_bugs_url(self):
@@ -363,40 +350,42 @@ class BugsInfoMixin:
or `IProductSeries`.
"""
if target_has_expirable_bugs_listing(self.context):
- return canonical_url(self.context, view_name='+expirable-bugs')
+ return canonical_url(self.context, view_name="+expirable-bugs")
else:
return None
@property
def new_bugs_url(self):
"""A URL to a page of new bugs."""
- return get_buglisting_search_filter_url(
- status=BugTaskStatus.NEW.title)
+ return get_buglisting_search_filter_url(status=BugTaskStatus.NEW.title)
@property
def inprogress_bugs_url(self):
"""A URL to a page of inprogress bugs."""
return get_buglisting_search_filter_url(
- status=BugTaskStatus.INPROGRESS.title)
+ status=BugTaskStatus.INPROGRESS.title
+ )
@property
def open_bugs_url(self):
"""A URL to a list of open bugs."""
- return canonical_url(self.context, view_name='+bugs')
+ return canonical_url(self.context, view_name="+bugs")
@property
def critical_bugs_url(self):
"""A URL to a list of critical bugs."""
return get_buglisting_search_filter_url(
status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
- importance=BugTaskImportance.CRITICAL.title)
+ importance=BugTaskImportance.CRITICAL.title,
+ )
@property
def high_bugs_url(self):
"""A URL to a list of high priority bugs."""
return get_buglisting_search_filter_url(
status=[status.title for status in UNRESOLVED_BUGTASK_STATUSES],
- importance=BugTaskImportance.HIGH.title)
+ importance=BugTaskImportance.HIGH.title,
+ )
@property
def my_bugs_url(self):
@@ -414,8 +403,8 @@ class BugsInfoMixin:
if self.user is None:
return None
return get_buglisting_search_filter_url(
- affecting_me=True,
- orderby='-date_last_updated')
+ affecting_me=True, orderby="-date_last_updated"
+ )
@property
def my_reported_bugs_url(self):
@@ -435,9 +424,13 @@ class BugsStatsMixin(BugsInfoMixin):
def _bug_stats(self):
# Circular fail.
from lp.bugs.model.bugsummary import BugSummary
+
bug_task_set = getUtility(IBugTaskSet)
groups = (
- BugSummary.status, BugSummary.importance, BugSummary.has_patch)
+ BugSummary.status,
+ BugSummary.importance,
+ BugSummary.has_patch,
+ )
counts = bug_task_set.countBugs(self.user, [self.context], groups)
# Sum the split out aggregates.
new = 0
@@ -462,8 +455,13 @@ class BugsStatsMixin(BugsInfoMixin):
with_patch += count
open += count
result = dict(
- new=new, open=open, inprogress=inprogress, high=high,
- critical=critical, with_patch=with_patch)
+ new=new,
+ open=open,
+ inprogress=inprogress,
+ high=high,
+ critical=critical,
+ with_patch=with_patch,
+ )
return result
@property
@@ -497,35 +495,38 @@ class BugsStatsMixin(BugsInfoMixin):
or `IProductSeries`.
"""
if target_has_expirable_bugs_listing(self.context):
- return getUtility(IBugTaskSet).findExpirableBugTasks(
- 0, user=self.user, target=self.context).count()
+ return (
+ getUtility(IBugTaskSet)
+ .findExpirableBugTasks(0, user=self.user, target=self.context)
+ .count()
+ )
else:
return None
@property
def new_bugs_count(self):
"""A count of new bugs."""
- return self._bug_stats['new']
+ return self._bug_stats["new"]
@property
def open_bugs_count(self):
"""A count of open bugs."""
- return self._bug_stats['open']
+ return self._bug_stats["open"]
@property
def inprogress_bugs_count(self):
"""A count of in-progress bugs."""
- return self._bug_stats['inprogress']
+ return self._bug_stats["inprogress"]
@property
def critical_bugs_count(self):
"""A count of critical bugs."""
- return self._bug_stats['critical']
+ return self._bug_stats["critical"]
@property
def high_bugs_count(self):
"""A count of high priority bugs."""
- return self._bug_stats['high']
+ return self._bug_stats["high"]
@property
def my_bugs_count(self):
@@ -558,7 +559,7 @@ class BugsStatsMixin(BugsInfoMixin):
@property
def bugs_with_patches_count(self):
"""A count of unresolved bugs with patches."""
- return self._bug_stats['with_patch']
+ return self._bug_stats["with_patch"]
class BugListingPortletInfoView(LaunchpadView, BugsInfoMixin):
@@ -570,40 +571,45 @@ class BugListingPortletStatsView(LaunchpadView, BugsStatsMixin):
def get_buglisting_search_filter_url(
- assignee=None, importance=None, status=None, status_upstream=None,
- has_patches=None, bug_reporter=None,
- affecting_me=None,
- orderby=None):
+ assignee=None,
+ importance=None,
+ status=None,
+ status_upstream=None,
+ has_patches=None,
+ bug_reporter=None,
+ affecting_me=None,
+ orderby=None,
+):
"""Return the given URL with the search parameters specified."""
search_params = []
if assignee is not None:
- search_params.append(('field.assignee', assignee))
+ search_params.append(("field.assignee", assignee))
if importance is not None:
- search_params.append(('field.importance', importance))
+ search_params.append(("field.importance", importance))
if status is not None:
- search_params.append(('field.status', status))
+ search_params.append(("field.status", status))
if status_upstream is not None:
- search_params.append(('field.status_upstream', status_upstream))
+ search_params.append(("field.status_upstream", status_upstream))
if has_patches is not None:
- search_params.append(('field.has_patch', 'on'))
+ search_params.append(("field.has_patch", "on"))
if bug_reporter is not None:
- search_params.append(('field.bug_reporter', bug_reporter))
+ search_params.append(("field.bug_reporter", bug_reporter))
if affecting_me is not None:
- search_params.append(('field.affects_me', 'on'))
+ search_params.append(("field.affects_me", "on"))
if orderby is not None:
- search_params.append(('orderby', orderby))
+ search_params.append(("orderby", orderby))
query_string = urlencode(search_params, doseq=True)
search_filter_url = "+bugs?search=Search"
- if query_string != '':
+ if query_string != "":
search_filter_url += "&" + query_string
return search_filter_url
-@delegate_to(IBugTask, context='bugtask')
+@delegate_to(IBugTask, context="bugtask")
class BugTaskListingItem:
"""A decorated bug task.
@@ -612,9 +618,17 @@ class BugTaskListingItem:
prefetched by the view and decorate the bug task.
"""
- def __init__(self, bugtask, has_bug_branch,
- has_specification, has_patch, tags,
- people, request=None, target_context=None):
+ def __init__(
+ self,
+ bugtask,
+ has_bug_branch,
+ has_specification,
+ has_patch,
+ tags,
+ people,
+ request=None,
+ target_context=None,
+ ):
self.bugtask = bugtask
self.review_action_widget = None
self.has_bug_branch = has_bug_branch
@@ -628,30 +642,34 @@ class BugTaskListingItem:
@property
def last_significant_change_date(self):
"""The date of the last significant change."""
- return (self.bugtask.date_closed or self.bugtask.date_fix_committed or
- self.bugtask.date_inprogress or self.bugtask.date_left_new or
- self.bugtask.datecreated)
+ return (
+ self.bugtask.date_closed
+ or self.bugtask.date_fix_committed
+ or self.bugtask.date_inprogress
+ or self.bugtask.date_left_new
+ or self.bugtask.datecreated
+ )
@property
def bug_heat_html(self):
"""Returns the bug heat flames HTML."""
- return (
- '<span class="sprite flame">%d</span>'
- % self.bugtask.bug.heat)
+ return '<span class="sprite flame">%d</span>' % self.bugtask.bug.heat
@property
def model(self):
"""Provide flattened data about bugtask for simple templaters."""
age = DateTimeFormatterAPI(self.bug.datecreated).durationsince()
- age += ' old'
+ age += " old"
date_last_updated = self.bug.date_last_message
- if (date_last_updated is None or
- self.bug.date_last_updated > date_last_updated):
+ if (
+ date_last_updated is None
+ or self.bug.date_last_updated > date_last_updated
+ ):
date_last_updated = self.bug.date_last_updated
last_updated_formatter = DateTimeFormatterAPI(date_last_updated)
last_updated = last_updated_formatter.displaydate()
- badges = getAdapter(self, IPathAdapter, 'image').badges()
- target_image = getAdapter(self.target, IPathAdapter, 'image')
+ badges = getAdapter(self, IPathAdapter, "image").badges()
+ target_image = getAdapter(self.target, IPathAdapter, "image")
if self.bugtask.milestone is not None:
milestone_name = self.bugtask.milestone.displayname
else:
@@ -665,74 +683,79 @@ class BugTaskListingItem:
# are related to a user account) is intercepted
if self.target_context is None:
base_tag_url = "%s/?field.tag=" % canonical_url(
- self.bugtask.target,
- view_name="+bugs")
+ self.bugtask.target, view_name="+bugs"
+ )
else:
base_tag_url = "%s/?field.tag=" % canonical_url(
- self.target_context,
- view_name="+bugs")
+ self.target_context, view_name="+bugs"
+ )
flattened = {
- 'age': age,
- 'assignee': assignee,
- 'bug_url': canonical_url(self.bugtask),
- 'bugtarget': self.bugtargetdisplayname,
- 'bugtarget_css': target_image.sprite_css(),
- 'bug_heat_html': self.bug_heat_html,
- 'badges': badges,
- 'id': self.bug.id,
- 'importance': self.importance.title,
- 'importance_class': 'importance' + self.importance.name,
- 'information_type': self.bug.information_type.title,
- 'last_updated': last_updated,
- 'milestone_name': milestone_name,
- 'reporter': reporter.displayname,
- 'status': self.status.title,
- 'status_class': 'status' + self.status.name,
- 'tags': [{'url': base_tag_url + quote(tag), 'tag': tag}
- for tag in self.tags],
- 'title': self.bug.title,
- }
+ "age": age,
+ "assignee": assignee,
+ "bug_url": canonical_url(self.bugtask),
+ "bugtarget": self.bugtargetdisplayname,
+ "bugtarget_css": target_image.sprite_css(),
+ "bug_heat_html": self.bug_heat_html,
+ "badges": badges,
+ "id": self.bug.id,
+ "importance": self.importance.title,
+ "importance_class": "importance" + self.importance.name,
+ "information_type": self.bug.information_type.title,
+ "last_updated": last_updated,
+ "milestone_name": milestone_name,
+ "reporter": reporter.displayname,
+ "status": self.status.title,
+ "status_class": "status" + self.status.name,
+ "tags": [
+ {"url": base_tag_url + quote(tag), "tag": tag}
+ for tag in self.tags
+ ],
+ "title": self.bug.title,
+ }
# This is a total hack, but pystache will run both truth/false values
# for an empty list for some reason, and it "works" if it's just a
# flag like this. We need this value for the mustache template to be
# able to tell that there are no tags without looking at the list.
- flattened['has_tags'] = True if len(flattened['tags']) else False
+ flattened["has_tags"] = True if len(flattened["tags"]) else False
return flattened
class BugListingBatchNavigator(TableBatchNavigator):
"""A specialised batch navigator to load smartly extra bug information."""
- def __init__(self, tasks, request, columns_to_show, size,
- target_context=None):
+ def __init__(
+ self, tasks, request, columns_to_show, size, target_context=None
+ ):
self.request = request
self.target_context = target_context
self.user = getUtility(ILaunchBag).user
self.field_visibility_defaults = {
- 'show_datecreated': False,
- 'show_assignee': False,
- 'show_targetname': True,
- 'show_heat': True,
- 'show_id': True,
- 'show_importance': True,
- 'show_information_type': False,
- 'show_date_last_updated': False,
- 'show_milestone_name': False,
- 'show_reporter': False,
- 'show_status': True,
- 'show_tag': False,
+ "show_datecreated": False,
+ "show_assignee": False,
+ "show_targetname": True,
+ "show_heat": True,
+ "show_id": True,
+ "show_importance": True,
+ "show_information_type": False,
+ "show_date_last_updated": False,
+ "show_milestone_name": False,
+ "show_reporter": False,
+ "show_status": True,
+ "show_tag": False,
}
self.field_visibility = None
self._setFieldVisibility()
TableBatchNavigator.__init__(
- self, tasks, request, columns_to_show=columns_to_show, size=size)
+ self, tasks, request, columns_to_show=columns_to_show, size=size
+ )
@cachedproperty
def bug_badge_properties(self):
return getUtility(IBugTaskSet).getBugTaskBadgeProperties(
- self.currentBatch())
+ self.currentBatch()
+ )
@cachedproperty
def tags_for_batch(self):
@@ -746,12 +769,12 @@ class BugListingBatchNavigator(TableBatchNavigator):
def getCookieName(self):
"""Return the cookie name used in bug listings js code."""
- cookie_name_template = '%s-buglist-fields'
- cookie_name = ''
+ cookie_name_template = "%s-buglist-fields"
+ cookie_name = ""
if self.user is not None:
cookie_name = cookie_name_template % self.user.name
else:
- cookie_name = cookie_name_template % 'anon'
+ cookie_name = cookie_name_template % "anon"
return cookie_name
def _setFieldVisibility(self):
@@ -774,14 +797,15 @@ class BugListingBatchNavigator(TableBatchNavigator):
if field not in self.field_visibility:
continue
# We only record True or False for field values.
- self.field_visibility[field] = (value == 'true')
+ self.field_visibility[field] = value == "true"
def _getListingItem(self, bugtask):
"""Return a decorated bugtask for the bug listing."""
badge_property = self.bug_badge_properties[bugtask]
tags = self.tags_for_batch.get(bugtask.id, ())
- if (IMaloneApplication.providedBy(self.target_context) or
- IPerson.providedBy(self.target_context)):
+ if IMaloneApplication.providedBy(
+ self.target_context
+ ) or IPerson.providedBy(self.target_context):
# XXX Tom Berger bug=529846
# When we have a specific interface for things that have bug heat
# it would be better to use that for the check here instead.
@@ -790,13 +814,14 @@ class BugListingBatchNavigator(TableBatchNavigator):
target_context = self.target_context
return BugTaskListingItem(
bugtask,
- badge_property['has_branch'],
- badge_property['has_specification'],
- badge_property['has_patch'],
+ badge_property["has_branch"],
+ badge_property["has_specification"],
+ badge_property["has_patch"],
tags,
self.bugtask_people,
request=self.request,
- target_context=target_context)
+ target_context=target_context,
+ )
def getBugListingItems(self):
"""Return a decorated list of visible bug tasks."""
@@ -805,14 +830,16 @@ class BugListingBatchNavigator(TableBatchNavigator):
@cachedproperty
def mustache_template(self):
template_path = os.path.join(
- config.root, 'lib/lp/bugs/templates/buglisting.mustache')
+ config.root, "lib/lp/bugs/templates/buglisting.mustache"
+ )
with open(template_path) as template_file:
return template_file.read()
@property
def mustache_listings(self):
- return 'LP.mustache_listings = %s;' % dumps(
- self.mustache_template, cls=JSONEncoderForHTML)
+ return "LP.mustache_listings = %s;" % dumps(
+ self.mustache_template, cls=JSONEncoderForHTML
+ )
@property
def mustache(self):
@@ -820,14 +847,14 @@ class BugListingBatchNavigator(TableBatchNavigator):
objects = IJSONRequestCache(self.request).objects
if IUnauthenticatedPrincipal.providedBy(self.request.principal):
objects = obfuscate_structure(objects)
- model = dict(objects['mustache_model'])
+ model = dict(objects["mustache_model"])
model.update(self.field_visibility)
return pystache.render(self.mustache_template, model)
@property
def model(self):
items = [bugtask.model for bugtask in self.getBugListingItems()]
- return {'items': items}
+ return {"items": items}
class IBugTaskSearchListingMenu(Interface):
@@ -836,70 +863,63 @@ class IBugTaskSearchListingMenu(Interface):
class BugTaskSearchListingMenu(NavigationMenu):
"""The search listing navigation menu."""
+
usedfor = IBugTaskSearchListingMenu
- facet = 'bugs'
+ facet = "bugs"
@property
def links(self):
bug_target = self.context.context
if IDistroSeries.providedBy(bug_target):
- return (
- 'nominations',
- )
+ return ("nominations",)
if IProductSeries.providedBy(bug_target):
- return (
- 'nominations',
- )
+ return ("nominations",)
else:
return ()
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def bugsupervisor(self):
- return Link('+bugsupervisor', 'Change bug supervisor', icon='edit')
+ return Link("+bugsupervisor", "Change bug supervisor", icon="edit")
def nominations(self):
- return Link('+nominations', 'Review nominations', icon='bug')
+ return Link("+nominations", "Review nominations", icon="bug")
# All sort orders supported by BugTaskSet.search() and a title for
# them. Keep in sync with lp.bugs.model.bugtasksearch.orderby_expression.
SORT_KEYS = [
- ('importance', 'Importance', 'desc'),
- ('status', 'Status', 'asc'),
- ('information_type', 'Information Type', 'asc'),
- ('id', 'Number', 'desc'),
- ('title', 'Title', 'asc'),
- ('targetname', 'Package/Project/Series name', 'asc'),
- ('milestone_name', 'Milestone', 'asc'),
- ('date_last_updated', 'Date last updated', 'desc'),
- ('assignee', 'Assignee', 'asc'),
- ('reporter', 'Reporter', 'asc'),
- ('datecreated', 'Age', 'desc'),
- ('tag', 'Tags', 'asc'),
- ('heat', 'Heat', 'desc'),
- ('date_closed', 'Date closed', 'desc'),
- ('dateassigned', 'Date when the bug task was assigned', 'desc'),
- ('number_of_duplicates', 'Number of duplicates', 'desc'),
- ('latest_patch_uploaded', 'Date latest patch uploaded', 'desc'),
- ('message_count', 'Number of comments', 'desc'),
- ('milestone', 'Milestone ID', 'desc'),
- ('task', 'Bug task ID', 'desc'),
- ('users_affected_count', 'Number of affected users', 'desc'),
- ]
+ ("importance", "Importance", "desc"),
+ ("status", "Status", "asc"),
+ ("information_type", "Information Type", "asc"),
+ ("id", "Number", "desc"),
+ ("title", "Title", "asc"),
+ ("targetname", "Package/Project/Series name", "asc"),
+ ("milestone_name", "Milestone", "asc"),
+ ("date_last_updated", "Date last updated", "desc"),
+ ("assignee", "Assignee", "asc"),
+ ("reporter", "Reporter", "asc"),
+ ("datecreated", "Age", "desc"),
+ ("tag", "Tags", "asc"),
+ ("heat", "Heat", "desc"),
+ ("date_closed", "Date closed", "desc"),
+ ("dateassigned", "Date when the bug task was assigned", "desc"),
+ ("number_of_duplicates", "Number of duplicates", "desc"),
+ ("latest_patch_uploaded", "Date latest patch uploaded", "desc"),
+ ("message_count", "Number of comments", "desc"),
+ ("milestone", "Milestone ID", "desc"),
+ ("task", "Bug task ID", "desc"),
+ ("users_affected_count", "Number of affected users", "desc"),
+]
@implementer(IBugTaskSearchListingMenu)
class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
"""View that renders a list of bugs for a given set of search criteria."""
- related_features = {
- 'bugs.dynamic_bug_listings.pre_fetch': False
- }
+ related_features = {"bugs.dynamic_bug_listings.pre_fetch": False}
# Only include <link> tags for bug feeds when using this view.
- feed_types = (
- BugTargetLatestBugsFeedLink,
- )
+ feed_types = (BugTargetLatestBugsFeedLink,)
# These widgets are customised so as to keep the presentation of this view
# and its descendants consistent after refactoring to use
@@ -949,8 +969,9 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
@property
def can_have_external_bugtracker(self):
- return (IProduct.providedBy(self.context)
- or IProductSeries.providedBy(self.context))
+ return IProduct.providedBy(self.context) or IProductSeries.providedBy(
+ self.context
+ )
@property
def bugtracker(self):
@@ -959,11 +980,11 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
:returns: str which may contain HTML.
"""
if self.bug_tracking_usage == ServiceUsage.LAUNCHPAD:
- return 'Launchpad'
+ return "Launchpad"
elif self.external_bugtracker:
return BugTrackerFormatterAPI(self.external_bugtracker).link(None)
else:
- return 'None specified'
+ return "None specified"
@cachedproperty
def upstream_project(self):
@@ -999,12 +1020,14 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
:returns: `IProduct` or None
"""
product = self.upstream_project
- if (product is not None and
- product.bug_tracking_usage == ServiceUsage.LAUNCHPAD):
+ if (
+ product is not None
+ and product.bug_tracking_usage == ServiceUsage.LAUNCHPAD
+ ):
return product
return None
- page_title = 'Bugs'
+ page_title = "Bugs"
@property
def label(self):
@@ -1029,7 +1052,7 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
view, but it does not match this view's bug listing when
any search parameters are passed in.
"""
- if self.request.get('QUERY_STRING', '') == '':
+ if self.request.get("QUERY_STRING", "") == "":
# There is no query in this request, so it's okay for this page to
# have its feed links.
return super().feed_links
@@ -1043,13 +1066,15 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
Look for old status names and redirect to a new location if found.
"""
- query_string = self.request.get('QUERY_STRING')
+ query_string = self.request.get("QUERY_STRING")
if query_string:
- query_string_rewritten = (
- rewrite_old_bugtask_status_query_string(query_string))
+ query_string_rewritten = rewrite_old_bugtask_status_query_string(
+ query_string
+ )
if query_string_rewritten != query_string:
redirect_uri = URI(self.request.getURL()).replace(
- query=query_string_rewritten)
+ query=query_string_rewritten
+ )
self.request.response.redirect(str(redirect_uri), status=301)
return
@@ -1064,37 +1089,46 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
self._validate(None, {})
expose_structural_subscription_data_to_js(
- self.context, self.request, self.user)
- can_view = (IPrivacy(self.context, None) is None
- or check_permission('launchpad.View', self.context))
- if (can_view and
- not FeedsLayer.providedBy(self.request) and
- not self.request.form.get('advanced')):
+ self.context, self.request, self.user
+ )
+ can_view = IPrivacy(self.context, None) is None or check_permission(
+ "launchpad.View", self.context
+ )
+ if (
+ can_view
+ and not FeedsLayer.providedBy(self.request)
+ and not self.request.form.get("advanced")
+ ):
cache = IJSONRequestCache(self.request)
- view_names = {reg.name for reg
- in iter_view_registrations(self.__class__)}
+ view_names = {
+ reg.name for reg in iter_view_registrations(self.__class__)
+ }
if len(view_names) != 1:
raise AssertionError("Ambiguous view name.")
- cache.objects['view_name'] = view_names.pop()
+ cache.objects["view_name"] = view_names.pop()
batch_navigator = self.search()
- cache.objects['mustache_model'] = batch_navigator.model
+ cache.objects["mustache_model"] = batch_navigator.model
cache.objects.update(
- get_batch_properties_for_json_cache(self, batch_navigator))
- cache.objects['field_visibility'] = (
- batch_navigator.field_visibility)
- cache.objects['field_visibility_defaults'] = (
- batch_navigator.field_visibility_defaults)
- cache.objects['cbl_cookie_name'] = (
- batch_navigator.getCookieName())
-
- cache.objects['order_by'] = ','.join(
- get_sortorder_from_request(self.request))
- cache.objects['sort_keys'] = SORT_KEYS
+ get_batch_properties_for_json_cache(self, batch_navigator)
+ )
+ cache.objects[
+ "field_visibility"
+ ] = batch_navigator.field_visibility
+ cache.objects[
+ "field_visibility_defaults"
+ ] = batch_navigator.field_visibility_defaults
+ cache.objects["cbl_cookie_name"] = batch_navigator.getCookieName()
+
+ cache.objects["order_by"] = ",".join(
+ get_sortorder_from_request(self.request)
+ )
+ cache.objects["sort_keys"] = SORT_KEYS
@property
def show_config_portlet(self):
- if (IDistribution.providedBy(self.context) or
- IProduct.providedBy(self.context)):
+ if IDistribution.providedBy(self.context) or IProduct.providedBy(
+ self.context
+ ):
return True
else:
return False
@@ -1111,33 +1145,55 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
sourcepackage_context = self._sourcePackageContext()
ociproject_context = self._ociprojectContext()
- if (upstream_context or productseries_context or
- distrosourcepackage_context or sourcepackage_context):
+ if (
+ upstream_context
+ or productseries_context
+ or distrosourcepackage_context
+ or sourcepackage_context
+ ):
return ["id", "summary", "importance", "status", "heat"]
elif distribution_context or distroseries_context:
return [
- "id", "summary", "packagename", "importance", "status",
- "heat"]
+ "id",
+ "summary",
+ "packagename",
+ "importance",
+ "status",
+ "heat",
+ ]
elif project_context:
return [
- "id", "summary", "productname", "importance", "status",
- "heat"]
+ "id",
+ "summary",
+ "productname",
+ "importance",
+ "status",
+ "heat",
+ ]
elif ociproject_context:
return [
- "id", "summary", "ociproject", "importance", "status", "heat"]
+ "id",
+ "summary",
+ "ociproject",
+ "importance",
+ "status",
+ "heat",
+ ]
else:
raise AssertionError(
"Unrecognized context; don't know which report "
- "columns to show.")
+ "columns to show."
+ )
bugtask_table_template = ViewPageTemplateFile(
- '../templates/bugs-table-include.pt')
+ "../templates/bugs-table-include.pt"
+ )
@property
def template(self):
- query_string = self.request.get('QUERY_STRING') or ''
+ query_string = self.request.get("QUERY_STRING") or ""
query_params = parse_qs(query_string)
- if 'batch_request' in query_params:
+ if "batch_request" in query_params:
return self.bugtask_table_template
else:
return super().template
@@ -1150,13 +1206,19 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
"""
# The only way the user should get these field values incorrect is
# through a stale bookmark or a hand-hacked URL.
- for field_name in ("status", "importance", "milestone", "component",
- "status_upstream"):
+ for field_name in (
+ "status",
+ "importance",
+ "milestone",
+ "component",
+ "status_upstream",
+ ):
if self.getFieldError(field_name):
raise UnexpectedFormData(
"Unexpected value for field '%s'. Perhaps your bookmarks "
- "are out of date or you changed the URL by hand?" %
- field_name)
+ "are out of date or you changed the URL by hand?"
+ % field_name
+ )
sort_column_names = {sort_key[0] for sort_key in SORT_KEYS}
orderby = get_sortorder_from_request(self.request)
@@ -1166,14 +1228,16 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
if orderby_col not in sort_column_names:
raise UnexpectedFormData(
- "Unknown sort column '%s'" % orderby_col)
+ "Unknown sort column '%s'" % orderby_col
+ )
def setUpWidgets(self):
"""Customize the onKeyPress event of the assignee chooser."""
LaunchpadFormView.setUpWidgets(self)
- self.widgets["assignee"].onKeyPress = (
- "selectWidget('assignee_option', event)")
+ self.widgets[
+ "assignee"
+ ].onKeyPress = "selectWidget('assignee_option', event)"
def validate(self, data):
"""Validates the form."""
@@ -1190,15 +1254,19 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
convert the old string parameter into a list.
"""
old_upstream_status_values_to_new_values = {
- 'only_resolved_upstream': 'resolved_upstream'}
-
- status_upstream = self.request.get('field.status_upstream')
- if (not isinstance(status_upstream, list) and
- status_upstream in old_upstream_status_values_to_new_values):
- self.request.form['field.status_upstream'] = [
- old_upstream_status_values_to_new_values[status_upstream]]
- elif status_upstream == '':
- del self.request.form['field.status_upstream']
+ "only_resolved_upstream": "resolved_upstream"
+ }
+
+ status_upstream = self.request.get("field.status_upstream")
+ if (
+ not isinstance(status_upstream, list)
+ and status_upstream in old_upstream_status_values_to_new_values
+ ):
+ self.request.form["field.status_upstream"] = [
+ old_upstream_status_values_to_new_values[status_upstream]
+ ]
+ elif status_upstream == "":
+ del self.request.form["field.status_upstream"]
else:
# The value of status_upstream is either correct, so nothing to
# do, or it has some other error, which is handled in
@@ -1220,7 +1288,7 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
if data:
searchtext = data.get("searchtext")
if searchtext:
- if searchtext.startswith('#'):
+ if searchtext.startswith("#"):
searchtext = searchtext[1:]
if searchtext.isdigit():
try:
@@ -1232,32 +1300,33 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
assignee_option = self.request.form.get("assignee_option")
if assignee_option == "none":
- data['assignee'] = NULL
+ data["assignee"] = NULL
has_patch = data.pop("has_patch", False)
if has_patch:
data["attachmenttype"] = BugAttachmentType.PATCH
- has_branches = data.get('has_branches', True)
- has_no_branches = data.get('has_no_branches', True)
+ has_branches = data.get("has_branches", True)
+ has_no_branches = data.get("has_no_branches", True)
if has_branches and not has_no_branches:
- data['linked_branches'] = BugBranchSearch.BUGS_WITH_BRANCHES
+ data["linked_branches"] = BugBranchSearch.BUGS_WITH_BRANCHES
elif not has_branches and has_no_branches:
- data['linked_branches'] = (
- BugBranchSearch.BUGS_WITHOUT_BRANCHES)
+ data["linked_branches"] = BugBranchSearch.BUGS_WITHOUT_BRANCHES
else:
- data['linked_branches'] = BugBranchSearch.ALL
+ data["linked_branches"] = BugBranchSearch.ALL
- has_blueprints = data.get('has_blueprints', True)
- has_no_blueprints = data.get('has_no_blueprints', True)
+ has_blueprints = data.get("has_blueprints", True)
+ has_no_blueprints = data.get("has_no_blueprints", True)
if has_blueprints and not has_no_blueprints:
- data['linked_blueprints'] = (
- BugBlueprintSearch.BUGS_WITH_BLUEPRINTS)
+ data[
+ "linked_blueprints"
+ ] = BugBlueprintSearch.BUGS_WITH_BLUEPRINTS
elif not has_blueprints and has_no_blueprints:
- data['linked_blueprints'] = (
- BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS)
+ data[
+ "linked_blueprints"
+ ] = BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS
else:
- data['linked_blueprints'] = BugBlueprintSearch.ALL
+ data["linked_blueprints"] = BugBlueprintSearch.ALL
# Filter appropriately if the user wants to restrict the
# search to only bugs with no package information.
@@ -1270,7 +1339,7 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
# "Normalize" the form data into search arguments.
form_values = {}
for key, value in data.items():
- if key in ('tag'):
+ if key in ("tag"):
# Skip tag-related parameters, they
# are handled later on.
continue
@@ -1280,24 +1349,25 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
else:
form_values[key] = value
- if 'tag' in data:
+ if "tag" in data:
# Tags require special handling, since they can be used
# to search either inclusively or exclusively.
# We take a look at the `tags_combinator` field, and wrap
# the tag list in the appropriate search directive (either
# `any` or `all`). If no value is supplied, we assume `any`,
# in order to remain compatible with old saved search URLs.
- tags = data['tag']
+ tags = data["tag"]
tags_combinator_all = (
- 'tags_combinator' in data and
- data['tags_combinator'] == BugTagsSearchCombinator.ALL)
+ "tags_combinator" in data
+ and data["tags_combinator"] == BugTagsSearchCombinator.ALL
+ )
if zope_isinstance(tags, (list, tuple)) and len(tags) > 0:
if tags_combinator_all:
- form_values['tag'] = all(*tags)
+ form_values["tag"] = all(*tags)
else:
- form_values['tag'] = any(*tags)
+ form_values["tag"] = any(*tags)
else:
- form_values['tag'] = tags
+ form_values["tag"] = tags
search_params = get_default_search_params(self.user)
search_params.orderby = get_sortorder_from_request(self.request)
@@ -1306,27 +1376,30 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
return search_params
def _buildUpstreamStatusParams(self, data):
- """ Convert the status_upstream value to parameters we can
+ """Convert the status_upstream value to parameters we can
send to BugTaskSet.search().
"""
- if 'status_upstream' in data:
- status_upstream = data['status_upstream']
- if 'pending_bugwatch' in status_upstream:
- data['pending_bugwatch_elsewhere'] = True
- if 'resolved_upstream' in status_upstream:
- data['resolved_upstream'] = True
- if 'open_upstream' in status_upstream:
- data['open_upstream'] = True
- if 'hide_upstream' in status_upstream:
- data['has_no_upstream_bugtask'] = True
- del data['status_upstream']
+ if "status_upstream" in data:
+ status_upstream = data["status_upstream"]
+ if "pending_bugwatch" in status_upstream:
+ data["pending_bugwatch_elsewhere"] = True
+ if "resolved_upstream" in status_upstream:
+ data["resolved_upstream"] = True
+ if "open_upstream" in status_upstream:
+ data["open_upstream"] = True
+ if "hide_upstream" in status_upstream:
+ data["has_no_upstream_bugtask"] = True
+ del data["status_upstream"]
def _getBatchNavigator(self, tasks):
"""Return the batch navigator to be used to batch the bugtasks."""
return BugListingBatchNavigator(
- tasks, self.request, columns_to_show=self.columns_to_show,
+ tasks,
+ self.request,
+ columns_to_show=self.columns_to_show,
size=config.malone.buglist_batch_size,
- target_context=self.context)
+ target_context=self.context,
+ )
def buildBugTaskSearchParams(self, searchtext=None, extra_params=None):
"""Build the parameters to submit to the `searchTasks` method.
@@ -1358,14 +1431,14 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
# A mapping of parameters that appear in the destination
# with a different name, or are being dropped altogether.
param_names_map = {
- 'searchtext': 'search_text',
- 'omit_dupes': 'omit_duplicates',
- 'subscriber': 'bug_subscriber',
- 'tag': 'tags',
+ "searchtext": "search_text",
+ "omit_dupes": "omit_duplicates",
+ "subscriber": "bug_subscriber",
+ "tag": "tags",
# The correct value is being retrieved
# using get_sortorder_from_request()
- 'orderby': None,
- }
+ "orderby": None,
+ }
for key, value in data.items():
if key in param_names_map:
@@ -1377,9 +1450,9 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
assignee_option = self.request.form.get("assignee_option")
if assignee_option == "none":
- params['assignee'] = NULL
+ params["assignee"] = NULL
- params['order_by'] = get_sortorder_from_request(self.request)
+ params["order_by"] = get_sortorder_from_request(self.request)
return params
@@ -1396,12 +1469,14 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
"""
if self._batch_navigator is None:
unbatchedTasks = self.searchUnbatched(
- searchtext, context, extra_params)
+ searchtext, context, extra_params
+ )
self._batch_navigator = self._getBatchNavigator(unbatchedTasks)
return self._batch_navigator
- def searchUnbatched(self, searchtext=None, context=None,
- extra_params=None):
+ def searchUnbatched(
+ self, searchtext=None, context=None, extra_params=None
+ ):
"""Return a `SelectResults` object for the GET search criteria.
:param searchtext: Text that must occur in the bug report. If
@@ -1417,40 +1492,46 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
context = self.context
search_params = self.buildSearchParams(
- searchtext=searchtext, extra_params=extra_params)
+ searchtext=searchtext, extra_params=extra_params
+ )
search_params.user = self.user
try:
tasks = context.searchTasks(search_params)
except ValueError as e:
self.request.response.addErrorNotification(str(e))
- self.request.response.redirect(canonical_url(
- self.context, rootsite='bugs', view_name='+bugs'))
+ self.request.response.redirect(
+ canonical_url(self.context, rootsite="bugs", view_name="+bugs")
+ )
tasks = None
return tasks
def getWidgetValues(
- self, vocabulary_name=None, vocabulary=None, default_values=()):
+ self, vocabulary_name=None, vocabulary=None, default_values=()
+ ):
"""Return data used to render a field's widget.
Either `vocabulary_name` or `vocabulary` must be supplied."""
widget_values = []
if vocabulary is None:
- assert vocabulary_name is not None, 'No vocabulary specified.'
- vocabulary = vocabulary_registry.get(
- self.context, vocabulary_name)
+ assert vocabulary_name is not None, "No vocabulary specified."
+ vocabulary = vocabulary_registry.get(self.context, vocabulary_name)
for term in vocabulary:
widget_values.append(
dict(
- value=term.token, title=term.title or term.token,
- checked=term.value in default_values))
+ value=term.token,
+ title=term.title or term.token,
+ checked=term.value in default_values,
+ )
+ )
return shortlist(widget_values, longest_expected=13)
def getStatusWidgetValues(self):
"""Return data used to render the status checkboxes."""
return self.getWidgetValues(
vocabulary=BugTaskStatusSearchDisplay,
- default_values=DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY)
+ default_values=DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY,
+ )
def getImportanceWidgetValues(self):
"""Return data used to render the Importance checkboxes."""
@@ -1458,10 +1539,12 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
def getInformationTypeWidgetValues(self):
"""Return data used to render the Information Type checkboxes."""
- if (IProduct.providedBy(self.context)
- or IDistribution.providedBy(self.context)):
+ if IProduct.providedBy(self.context) or IDistribution.providedBy(
+ self.context
+ ):
vocab = InformationTypeVocabulary(
- types=self.context.getAllowedBugInformationTypes())
+ types=self.context.getAllowedBugInformationTypes()
+ )
else:
vocab = InformationType
return self.getWidgetValues(vocabulary=vocab)
@@ -1482,10 +1565,13 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
"""Show the component widget on the advanced search page?"""
context = self.context
return (
- (IDistribution.providedBy(context) and
- context.currentseries is not None) or
- IDistroSeries.providedBy(context) or
- ISourcePackage.providedBy(context))
+ (
+ IDistribution.providedBy(context)
+ and context.currentseries is not None
+ )
+ or IDistroSeries.providedBy(context)
+ or ISourcePackage.providedBy(context)
+ )
def shouldShowStructuralSubscriberWidget(self):
"""Should the structural subscriber widget be shown on the page?
@@ -1500,8 +1586,9 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
The widget will be shown only on a distribution or
distroseries's advanced search page.
"""
- return (IDistribution.providedBy(self.context) or
- IDistroSeries.providedBy(self.context))
+ return IDistribution.providedBy(
+ self.context
+ ) or IDistroSeries.providedBy(self.context)
def shouldShowReporterWidget(self):
"""Should the reporter widget be shown on the advanced search page?"""
@@ -1512,10 +1599,13 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
for different series.
"""
return (
- IDistribution.providedBy(self.context) and self.context.series
+ IDistribution.providedBy(self.context)
+ and self.context.series
or IDistroSeries.providedBy(self.context)
- or IProduct.providedBy(self.context) and self.context.series
- or IProductSeries.providedBy(self.context))
+ or IProduct.providedBy(self.context)
+ and self.context.series
+ or IProductSeries.providedBy(self.context)
+ )
def shouldShowSubscriberWidget(self):
"""Show the subscriber widget on the advanced search page?"""
@@ -1524,8 +1614,9 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
def shouldShowUpstreamStatusBox(self):
"""Should the upstream status filtering widgets be shown?"""
return self.isUpstreamProduct or not (
- IProduct.providedBy(self.context) or
- IProjectGroup.providedBy(self.context))
+ IProduct.providedBy(self.context)
+ or IProjectGroup.providedBy(self.context)
+ )
def shouldShowTeamPortlet(self):
"""Should the User's Teams portlet me shown in the results?"""
@@ -1539,15 +1630,15 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
target = self.context
if IDistribution.providedBy(target):
- return 'Package or series subscriber'
+ return "Package or series subscriber"
elif IDistroSeries.providedBy(target):
- return 'Package subscriber'
+ return "Package subscriber"
elif IProduct.providedBy(target):
- return 'Series subscriber'
+ return "Series subscriber"
elif IProjectGroup.providedBy(target):
- return 'Project or series subscriber'
+ return "Project or series subscriber"
elif IPerson.providedBy(target):
- return 'Project, distribution, package, or series subscriber'
+ return "Project, distribution, package, or series subscriber"
else:
return None
@@ -1570,8 +1661,7 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
def shouldShowAdvancedForm(self):
"""Return True if the advanced form should be shown, or False."""
- if (self.request.form.get('advanced')
- or self.form_has_errors):
+ if self.request.form.get("advanced") or self.form_has_errors:
return True
else:
return False
@@ -1588,21 +1678,28 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
def validateVocabulariesAdvancedForm(self):
"""Provides a meaningful message for vocabulary validation errors."""
error_message = _(
- "There's no person with the name or email address '%s'.")
+ "There's no person with the name or email address '%s'."
+ )
- for name in ('assignee', 'bug_reporter', 'structural_subscriber',
- 'bug_commenter', 'subscriber'):
+ for name in (
+ "assignee",
+ "bug_reporter",
+ "structural_subscriber",
+ "bug_commenter",
+ "subscriber",
+ ):
if self.getFieldError(name):
self.setFieldError(
- name, error_message %
- self.request.get('field.%s' % name))
+ name, error_message % self.request.get("field.%s" % name)
+ )
@property
def isUpstreamProduct(self):
"""Is the context a Product that does not use Malone?"""
return (
IProduct.providedBy(self.context)
- and self.context.bug_tracking_usage != ServiceUsage.LAUNCHPAD)
+ and self.context.bug_tracking_usage != ServiceUsage.LAUNCHPAD
+ )
def _upstreamContext(self):
"""Is this page being viewed in an upstream context?
@@ -1674,8 +1771,8 @@ class BugTaskSearchListingView(LaunchpadFormView, FeedsMixin, BugsInfoMixin):
answers_usage = IServiceUsage(self.context).answers_usage
if answers_usage == ServiceUsage.LAUNCHPAD:
return canonical_url(
- self.context, rootsite='answers',
- view_name='+addquestion')
+ self.context, rootsite="answers", view_name="+addquestion"
+ )
else:
return None
@@ -1685,22 +1782,22 @@ class BugTargetView(LaunchpadView):
def latestBugTasks(self, quantity=5):
"""Return <quantity> latest bugs reported against this target."""
- params = BugTaskSearchParams(orderby="-datecreated",
- omit_dupes=True,
- user=getUtility(ILaunchBag).user)
+ params = BugTaskSearchParams(
+ orderby="-datecreated",
+ omit_dupes=True,
+ user=getUtility(ILaunchBag).user,
+ )
tasklist = self.context.searchTasks(params)
return tasklist[:quantity]
class TextualBugTaskSearchListingView(BugTaskSearchListingView):
- """View that renders a list of bug IDs for a given set of search criteria.
- """
+ """View that renders a list of bug IDs for a set of search criteria."""
def render(self):
"""Render the BugTarget for text display."""
- self.request.response.setHeader(
- 'Content-type', 'text/plain')
+ self.request.response.setHeader("Content-type", "text/plain")
# This uses the BugTaskSet internal API instead of using the
# standard searchTasks() because the latter can retrieve a lot
@@ -1709,18 +1806,26 @@ class TextualBugTaskSearchListingView(BugTaskSearchListingView):
search_params = self.buildSearchParams()
search_params.setTarget(self.context)
- return "".join("%d\n" % bug_id for bug_id in
- getUtility(IBugTaskSet).searchBugIds(search_params))
+ return "".join(
+ "%d\n" % bug_id
+ for bug_id in getUtility(IBugTaskSet).searchBugIds(search_params)
+ )
class BugsBugTaskSearchListingView(BugTaskSearchListingView):
"""Search all bug reports."""
- columns_to_show = ["id", "summary", "bugtargetdisplayname",
- "importance", "status", "heat"]
+ columns_to_show = [
+ "id",
+ "summary",
+ "bugtargetdisplayname",
+ "importance",
+ "status",
+ "heat",
+ ]
schema = IFrontPageBugTaskSearch
custom_widget_scope = ProjectScopeWidget
- label = page_title = 'Search all bug reports'
+ label = page_title = "Search all bug reports"
def initialize(self):
"""Initialize the view for the request."""
@@ -1738,16 +1843,18 @@ class BugsBugTaskSearchListingView(BugTaskSearchListingView):
which will handle the error.
"""
try:
- search_target = self.widgets['scope'].getInputValue()
+ search_target = self.widgets["scope"].getInputValue()
except InputErrors:
- query_string = self.request['QUERY_STRING']
+ query_string = self.request["QUERY_STRING"]
bugs_url = "%s?%s" % (canonical_url(self.context), query_string)
self.request.response.redirect(bugs_url)
else:
if search_target is not None:
- query_string = self.request['QUERY_STRING']
+ query_string = self.request["QUERY_STRING"]
search_url = "%s/+bugs?%s" % (
- canonical_url(search_target), query_string)
+ canonical_url(search_target),
+ query_string,
+ )
self.request.response.redirect(search_url)
@@ -1762,23 +1869,33 @@ class BugTaskExpirableListingView(BugTaskSearchListingView):
@property
def columns_to_show(self):
"""Show the columns that summarise expirable bugs."""
- if (IDistribution.providedBy(self.context)
- or IDistroSeries.providedBy(self.context)):
+ if IDistribution.providedBy(self.context) or IDistroSeries.providedBy(
+ self.context
+ ):
return [
- 'id', 'summary', 'packagename', 'date_last_updated', 'heat']
+ "id",
+ "summary",
+ "packagename",
+ "date_last_updated",
+ "heat",
+ ]
else:
- return ['id', 'summary', 'date_last_updated', 'heat']
+ return ["id", "summary", "date_last_updated", "heat"]
def search(self):
"""Return an `ITableBatchNavigator` for the expirable bugtasks."""
bugtaskset = getUtility(IBugTaskSet)
bugtasks = bugtaskset.findExpirableBugTasks(
- user=self.user, target=self.context, min_days_old=0)
+ user=self.user, target=self.context, min_days_old=0
+ )
return BugListingBatchNavigator(
- bugtasks, self.request, columns_to_show=self.columns_to_show,
- size=config.malone.buglist_batch_size)
+ bugtasks,
+ self.request,
+ columns_to_show=self.columns_to_show,
+ size=config.malone.buglist_batch_size,
+ )
- page_title = 'Expirable bugs'
+ page_title = "Expirable bugs"
@property
def label(self):
@@ -1790,7 +1907,7 @@ class BugTaskExpirableListingView(BugTaskSearchListingView):
class BugNominationsView(BugTaskSearchListingView):
"""View for accepting/declining bug nominations."""
- page_title = 'Nominated bugs'
+ page_title = "Nominated bugs"
@property
def label(self):
@@ -1804,7 +1921,10 @@ class BugNominationsView(BugTaskSearchListingView):
main_context = self.context.product
else:
raise AssertionError(
- 'Unknown nomination target: %r' % self.context)
+ "Unknown nomination target: %r" % self.context
+ )
return BugTaskSearchListingView.search(
- self, context=main_context,
- extra_params=dict(nominated_for=self.context))
+ self,
+ context=main_context,
+ extra_params=dict(nominated_for=self.context),
+ )
diff --git a/lib/lp/bugs/browser/bugmessage.py b/lib/lp/bugs/browser/bugmessage.py
index 1ba55da..b0267a1 100644
--- a/lib/lp/bugs/browser/bugmessage.py
+++ b/lib/lp/bugs/browser/bugmessage.py
@@ -4,8 +4,8 @@
"""IBugMessage-related browser view classes."""
__all__ = [
- 'BugMessageAddFormView',
- ]
+ "BugMessageAddFormView",
+]
from io import BytesIO
@@ -13,10 +13,7 @@ from zope.component import getUtility
from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import TextAreaWidget
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
from lp.bugs.browser.bugattachment import BugAttachmentContentCheck
from lp.bugs.interfaces.bugmessage import IBugMessageAddForm
from lp.bugs.interfaces.bugwatch import IBugWatchSet
@@ -30,13 +27,14 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
initial_focus_widget = None
custom_widget_comment = CustomWidgetFactory(
- TextAreaWidget, cssClass='comment-text')
+ TextAreaWidget, cssClass="comment-text"
+ )
page_title = "Add a comment or attachment"
@property
def label(self):
- return 'Add a comment or attachment to bug #%d' % self.context.bug.id
+ return "Add a comment or attachment to bug #%d" % self.context.bug.id
@property
def initial_values(self):
@@ -54,57 +52,61 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
# Ensure either a comment or filecontent was provide, but only
# if no errors have already been noted.
if len(self.errors) == 0:
- comment = data.get('comment') or ''
- filecontent = data.get('filecontent', None)
+ comment = data.get("comment") or ""
+ filecontent = data.get("filecontent", None)
if not comment.strip() and not filecontent:
- self.addError("Either a comment or attachment "
- "must be provided.")
+ self.addError(
+ "Either a comment or attachment " "must be provided."
+ )
- @action("Post Comment", name='save')
+ @action("Post Comment", name="save")
def save_action(self, action, data):
"""Add the comment and/or attachment."""
bug = self.context.bug
# Subscribe to this bug if the checkbox exists and was selected
- if data.get('email_me'):
+ if data.get("email_me"):
bug.subscribe(self.user, self.user)
# XXX: Bjorn Tillenius 2005-06-16:
# Write proper FileUpload field and widget instead of this hack.
- file_ = self.request.form.get(self.widgets['filecontent'].name)
+ file_ = self.request.form.get(self.widgets["filecontent"].name)
message = None
- if data['comment'] or file_:
- bugwatch_id = data.get('bugwatch_id')
+ if data["comment"] or file_:
+ bugwatch_id = data.get("bugwatch_id")
if bugwatch_id is not None:
bugwatch = getUtility(IBugWatchSet).get(bugwatch_id)
else:
bugwatch = None
- message = bug.newMessage(subject=data.get('subject'),
- content=data['comment'],
- owner=self.user,
- bugwatch=bugwatch)
+ message = bug.newMessage(
+ subject=data.get("subject"),
+ content=data["comment"],
+ owner=self.user,
+ bugwatch=bugwatch,
+ )
# A blank comment with only a subect 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.
- if data['comment']:
+ if data["comment"]:
self.request.response.addNotification(
- "Thank you for your comment.")
+ "Thank you for your comment."
+ )
self.next_url = canonical_url(self.context)
if file_:
# Slashes in filenames cause problems, convert them to dashes
# instead.
- filename = file_.filename.replace('/', '-')
+ 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 "attachment_description" in data:
+ file_description = data["attachment_description"]
if not file_description:
file_description = filename
@@ -116,24 +118,33 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
# guess is wrong.
patch_flag_consistent = (
self.attachmentTypeConsistentWithContentType(
- data['patch'], filename, data['filecontent']))
+ data["patch"], filename, data["filecontent"]
+ )
+ )
if not patch_flag_consistent:
guessed_type = self.guessContentType(
- filename, data['filecontent'])
- is_patch = guessed_type == 'text/x-diff'
+ filename, data["filecontent"]
+ )
+ is_patch = guessed_type == "text/x-diff"
else:
- is_patch = data['patch']
+ is_patch = data["patch"]
attachment = bug.addAttachment(
- owner=self.user, data=BytesIO(data['filecontent']),
- filename=filename, description=file_description,
- comment=message, is_patch=is_patch)
+ owner=self.user,
+ data=BytesIO(data["filecontent"]),
+ filename=filename,
+ description=file_description,
+ comment=message,
+ is_patch=is_patch,
+ )
if not patch_flag_consistent:
self.next_url = self.nextUrlForInconsistentPatchFlags(
- attachment)
+ attachment
+ )
self.request.response.addNotification(
- "Attachment %s added to bug." % filename)
+ "Attachment %s added to bug." % filename
+ )
def shouldShowEmailMeWidget(self):
"""Should the subscribe checkbox be shown?"""
diff --git a/lib/lp/bugs/browser/bugnomination.py b/lib/lp/bugs/browser/bugnomination.py
index 6a61931..7319d28 100644
--- a/lib/lp/bugs/browser/bugnomination.py
+++ b/lib/lp/bugs/browser/bugnomination.py
@@ -4,31 +4,23 @@
"""Browser view classes related to bug nominations."""
__all__ = [
- 'BugNominationContextMenu',
- 'BugNominationView',
- 'BugNominationEditView',
- 'BugNominationTableRowView']
+ "BugNominationContextMenu",
+ "BugNominationView",
+ "BugNominationEditView",
+ "BugNominationTableRowView",
+]
from zope.component import getUtility
from zope.interface import Interface
from lp import _
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
from lp.bugs.browser.bug import BugContextMenu
-from lp.bugs.interfaces.bugnomination import (
- IBugNomination,
- IBugNominationForm,
- )
+from lp.bugs.interfaces.bugnomination import IBugNomination, IBugNominationForm
from lp.bugs.interfaces.cve import ICveSet
from lp.services.features import getFeatureFlag
-from lp.services.webapp import (
- canonical_url,
- LaunchpadView,
- )
+from lp.services.webapp import LaunchpadView, canonical_url
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.interfaces import ILaunchBag
@@ -46,16 +38,16 @@ class BugNominationView(LaunchpadFormView):
def initialize(self):
LaunchpadFormView.initialize(self)
# Update the submit label based on the user's permission.
- submit_action = self.__class__.actions.byname['actions.submit']
+ submit_action = self.__class__.actions.byname["actions.submit"]
if self.userCanTarget():
submit_action.label = _("Target")
elif self.userCanNominate():
submit_action.label = _("Nominate")
else:
self.request.response.addErrorNotification(
- "You do not have permission to nominate this bug.")
- self.request.response.redirect(
- canonical_url(self.current_bugtask))
+ "You do not have permission to nominate this bug."
+ )
+ self.request.response.redirect(canonical_url(self.current_bugtask))
@property
def label(self):
@@ -72,21 +64,18 @@ class BugNominationView(LaunchpadFormView):
def userCanTarget(self):
"""Can the current user target the bug to a series?"""
- return (
- self.current_bugtask.userHasDriverPrivileges(self.user)
- or (getFeatureFlag('bugs.nominations.bug_supervisors_can_target')
- and self.current_bugtask.userHasBugSupervisorPrivileges(
- self.user)))
+ return self.current_bugtask.userHasDriverPrivileges(self.user) or (
+ getFeatureFlag("bugs.nominations.bug_supervisors_can_target")
+ and self.current_bugtask.userHasBugSupervisorPrivileges(self.user)
+ )
def userCanNominate(self):
"""Can the current user nominate the bug for a series?"""
- return self.current_bugtask.userHasBugSupervisorPrivileges(
- self.user)
+ return self.current_bugtask.userHasBugSupervisorPrivileges(self.user)
def userCanChangeDriver(self):
"""Can the current user set the release management team?"""
- return check_permission(
- "launchpad.Edit", self.getReleaseContext())
+ return check_permission("launchpad.Edit", self.getReleaseContext())
def getReleaseManager(self):
"""Return the IPerson or ITeam that does release management."""
@@ -111,25 +100,27 @@ class BugNominationView(LaunchpadFormView):
for series in nominatable_series:
nomination = self.context.bug.addNomination(
- target=series, owner=self.user)
+ target=series, owner=self.user
+ )
# If the user has the permission to approve the nomination,
# we approve it automatically.
if nomination.canApprove(self.user):
nomination.approve(self.user)
approved_nominations.append(
- nomination.target.bugtargetdisplayname)
+ nomination.target.bugtargetdisplayname
+ )
else:
nominated_series.append(series.bugtargetdisplayname)
if approved_nominations:
self.request.response.addNotification(
- "Targeted bug to: %s" %
- ", ".join(approved_nominations))
+ "Targeted bug to: %s" % ", ".join(approved_nominations)
+ )
if nominated_series:
self.request.response.addNotification(
- "Added nominations for: %s" %
- ", ".join(nominated_series))
+ "Added nominations for: %s" % ", ".join(nominated_series)
+ )
@property
def next_url(self):
@@ -149,10 +140,10 @@ class BugNominationTableRowView(LaunchpadView):
def getNominationEditLink(self):
"""Return a link to the nomination edit form."""
- return (
- "%s/nominations/%d/+editstatus" % (
- canonical_url(getUtility(ILaunchBag).bugtask),
- self.context.id))
+ return "%s/nominations/%d/+editstatus" % (
+ canonical_url(getUtility(ILaunchBag).bugtask),
+ self.context.id,
+ )
def getApproveDeclineLinkText(self):
"""Return a string used for the approve/decline form expander link."""
@@ -163,7 +154,8 @@ class BugNominationTableRowView(LaunchpadView):
else:
assert (
"Expected nomination to be Proposed or Declined. "
- "Got status: %s" % self.context.status.title)
+ "Got status: %s" % self.context.status.title
+ )
def userCanMakeDecisionForNomination(self):
"""Can the user approve/decline this nomination?"""
@@ -183,12 +175,14 @@ class BugNominationEditView(LaunchpadFormView):
@property
def label(self):
- return 'Approve or decline nomination for bug #%d in %s' % (
- self.context.bug.id, self.context.target.bugtargetdisplayname)
+ return "Approve or decline nomination for bug #%d in %s" % (
+ self.context.bug.id,
+ self.context.target.bugtargetdisplayname,
+ )
@property
def page_title(self):
- text = 'Review nomination for %s'
+ text = "Review nomination for %s"
return text % self.context.target.bugtargetdisplayname
def initialize(self):
@@ -199,7 +193,8 @@ class BugNominationEditView(LaunchpadFormView):
def action_url(self):
return "%s/nominations/%d/+editstatus" % (
canonical_url(self.current_bugtask),
- self.context.id)
+ self.context.id,
+ )
def shouldShowApproveButton(self, action):
"""Should the approve button be shown?"""
@@ -213,15 +208,17 @@ class BugNominationEditView(LaunchpadFormView):
def approve(self, action, data):
self.context.approve(self.user)
self.request.response.addNotification(
- "Approved nomination for %s" %
- self.context.target.bugtargetdisplayname)
+ "Approved nomination for %s"
+ % self.context.target.bugtargetdisplayname
+ )
@action(_("Decline"), name="decline", condition=shouldShowDeclineButton)
def decline(self, action, data):
self.context.decline(self.user)
self.request.response.addNotification(
- "Declined nomination for %s" %
- self.context.target.bugtargetdisplayname)
+ "Declined nomination for %s"
+ % self.context.target.bugtargetdisplayname
+ )
@property
def next_url(self):
diff --git a/lib/lp/bugs/browser/bugsubscription.py b/lib/lp/bugs/browser/bugsubscription.py
index b28876c..0710d66 100644
--- a/lib/lp/bugs/browser/bugsubscription.py
+++ b/lib/lp/bugs/browser/bugsubscription.py
@@ -4,56 +4,47 @@
"""Views for BugSubscription."""
__all__ = [
- 'AdvancedSubscriptionMixin',
- 'BugMuteSelfView',
- 'BugPortletSubscribersWithDetails',
- 'BugSubscriptionAddView',
- 'BugSubscriptionListView',
- ]
+ "AdvancedSubscriptionMixin",
+ "BugMuteSelfView",
+ "BugPortletSubscribersWithDetails",
+ "BugSubscriptionAddView",
+ "BugSubscriptionListView",
+]
from lazr.delegates import delegate_to
-from lazr.restful.interfaces import (
- IJSONRequestCache,
- IWebServiceClientRequest,
- )
+from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
from simplejson import dumps
from zope import formlib
from zope.formlib.itemswidgets import RadioWidget
from zope.formlib.widget import CustomWidgetFactory
from zope.schema import Choice
-from zope.schema.vocabulary import (
- SimpleTerm,
- SimpleVocabulary,
- )
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
from lp import _
from lp.app.browser.launchpadform import (
- action,
LaunchpadFormView,
ReturnToReferrerMixin,
- )
+ action,
+)
from lp.app.errors import SubscriptionPrivacyViolation
from lp.bugs.browser.structuralsubscription import (
expose_structural_subscription_data_to_js,
- )
+)
from lp.bugs.enums import BugNotificationLevel
from lp.bugs.interfaces.bug import IBug
from lp.bugs.interfaces.bugsubscription import IBugSubscription
from lp.bugs.model.personsubscriptioninfo import PersonSubscriptions
from lp.bugs.model.structuralsubscription import (
get_structural_subscriptions_for_bug,
- )
+)
from lp.services.propertycache import cachedproperty
-from lp.services.webapp import (
- canonical_url,
- LaunchpadView,
- )
+from lp.services.webapp import LaunchpadView, canonical_url
from lp.services.webapp.authorization import (
check_permission,
precache_permission_for_objects,
- )
+)
from lp.services.webapp.escaping import structured
@@ -62,28 +53,30 @@ class BugSubscriptionAddView(LaunchpadFormView):
schema = IBugSubscription
- field_names = ['person']
+ field_names = ["person"]
def setUpFields(self):
"""Set up 'person' as an input field."""
super().setUpFields()
- self.form_fields['person'].for_input = True
+ self.form_fields["person"].for_input = True
- @action('Subscribe user', name='add')
+ @action("Subscribe user", name="add")
def add_action(self, action, data):
- person = data['person']
+ person = data["person"]
try:
self.context.bug.subscribe(
- person, self.user, suppress_notify=False)
+ person, self.user, suppress_notify=False
+ )
except SubscriptionPrivacyViolation as error:
- self.setFieldError('person', str(error))
+ self.setFieldError("person", str(error))
else:
if person.is_team:
- message = '%s team has been subscribed to this bug.'
+ message = "%s team has been subscribed to this bug."
else:
- message = '%s has been subscribed to this bug.'
+ message = "%s has been subscribed to this bug."
self.request.response.addInfoNotification(
- message % person.displayname)
+ message % person.displayname
+ )
@property
def next_url(self):
@@ -93,7 +86,7 @@ class BugSubscriptionAddView(LaunchpadFormView):
@property
def label(self):
- return 'Subscribe someone else to bug #%i' % self.context.bug.id
+ return "Subscribe someone else to bug #%i" % self.context.bug.id
page_title = label
@@ -122,57 +115,68 @@ class AdvancedSubscriptionMixin:
# form. The BugNotificationLevel descriptions are too generic.
bug_notification_level_terms = [
SimpleTerm(
- level, level.title,
- self._bug_notification_level_descriptions[level])
+ level,
+ level.title,
+ self._bug_notification_level_descriptions[level],
+ )
# We reorder the items so that COMMENTS comes first.
- for level in sorted(BugNotificationLevel.items, reverse=True)]
+ for level in sorted(BugNotificationLevel.items, reverse=True)
+ ]
bug_notification_vocabulary = SimpleVocabulary(
- bug_notification_level_terms)
+ bug_notification_level_terms
+ )
if self.current_user_subscription is not None:
default_value = (
- self.current_user_subscription.bug_notification_level)
+ self.current_user_subscription.bug_notification_level
+ )
else:
default_value = BugNotificationLevel.COMMENTS
bug_notification_level_field = Choice(
- __name__='bug_notification_level', title=_("Tell me when"),
- vocabulary=bug_notification_vocabulary, required=True,
- default=default_value)
+ __name__="bug_notification_level",
+ title=_("Tell me when"),
+ vocabulary=bug_notification_vocabulary,
+ required=True,
+ default=default_value,
+ )
return bug_notification_level_field
def _setUpBugNotificationLevelField(self):
"""Set up the bug_notification_level field."""
- self.form_fields = self.form_fields.omit('bug_notification_level')
+ self.form_fields = self.form_fields.omit("bug_notification_level")
self.form_fields += formlib.form.Fields(
- self._bug_notification_level_field)
- self.form_fields['bug_notification_level'].custom_widget = (
- CustomWidgetFactory(RadioWidget))
+ self._bug_notification_level_field
+ )
+ self.form_fields[
+ "bug_notification_level"
+ ].custom_widget = CustomWidgetFactory(RadioWidget)
-class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
- ReturnToReferrerMixin,
- AdvancedSubscriptionMixin):
+class BugSubscriptionSubscribeSelfView(
+ LaunchpadFormView, ReturnToReferrerMixin, AdvancedSubscriptionMixin
+):
"""A view to handle the +subscribe page for a bug."""
schema = IBugSubscription
- page_title = 'Subscription options'
+ page_title = "Subscription options"
# A mapping of BugNotificationLevel values to descriptions to be
# shown on the +subscribe page.
_bug_notification_level_descriptions = {
BugNotificationLevel.COMMENTS: (
- "a change is made to this bug or a new comment is added, "),
+ "a change is made to this bug or a new comment is added, "
+ ),
BugNotificationLevel.METADATA: (
"any change is made to this bug, other than a new comment "
- "being added, or"),
- BugNotificationLevel.LIFECYCLE: (
- "this bug is fixed or re-opened."),
- }
+ "being added, or"
+ ),
+ BugNotificationLevel.LIFECYCLE: ("this bug is fixed or re-opened."),
+ }
@property
def field_names(self):
- return ['bug_notification_level']
+ return ["bug_notification_level"]
@property
def next_url(self):
@@ -216,26 +220,28 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
@cachedproperty
def _update_subscription_term(self):
label = "update my current subscription"
- return SimpleTerm(
- 'update-subscription', 'update-subscription', label)
+ return SimpleTerm("update-subscription", "update-subscription", label)
@cachedproperty
def _unsubscribe_current_user_term(self):
if self.user_is_muted:
label = "unmute bug mail from this bug"
else:
- label = 'unsubscribe me from this bug'
+ label = "unsubscribe me from this bug"
return SimpleTerm(self.user, self.user.name, label)
@cachedproperty
def _unmute_user_term(self):
if self.user_is_subscribed_directly:
return SimpleTerm(
- 'update-subscription', 'update-subscription',
- "unmute bug mail from this bug and restore my subscription")
+ "update-subscription",
+ "update-subscription",
+ "unmute bug mail from this bug and restore my subscription",
+ )
else:
- return SimpleTerm(self.user, self.user.name,
- "unmute bug mail from this bug")
+ return SimpleTerm(
+ self.user, self.user.name, "unmute bug mail from this bug"
+ )
@cachedproperty
def _subscription_field(self):
@@ -252,49 +258,65 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
else:
if self.user_is_subscribed_directly:
subscription_terms.append(
- self._update_subscription_term)
+ self._update_subscription_term
+ )
subscription_terms.insert(
- 0, self._unsubscribe_current_user_term)
+ 0, self._unsubscribe_current_user_term
+ )
self_subscribed = True
else:
subscription_terms.append(
SimpleTerm(
- person, person.name,
+ person,
+ person.name,
structured(
'unsubscribe <a href="%s">%s</a> from this bug',
canonical_url(person),
- person.displayname).escapedtext))
+ person.displayname,
+ ).escapedtext,
+ )
+ )
if not self_subscribed:
if not is_really_muted:
- subscription_terms.insert(0,
+ subscription_terms.insert(
+ 0,
SimpleTerm(
- self.user, self.user.name,
- 'subscribe me to this bug'))
+ self.user, self.user.name, "subscribe me to this bug"
+ ),
+ )
elif not self.user_is_subscribed_directly:
- subscription_terms.insert(0,
+ subscription_terms.insert(
+ 0,
SimpleTerm(
- 'update-subscription', 'update-subscription',
- 'unmute bug mail from this bug and subscribe me to '
- 'this bug'))
+ "update-subscription",
+ "update-subscription",
+ "unmute bug mail from this bug and subscribe me to "
+ "this bug",
+ ),
+ )
# Add punctuation to the list of terms.
if len(subscription_terms) > 1:
for term in subscription_terms[:-1]:
- term.title += ','
- subscription_terms[-2].title += ' or'
- subscription_terms[-1].title += '.'
+ term.title += ","
+ subscription_terms[-2].title += " or"
+ subscription_terms[-1].title += "."
subscription_vocabulary = SimpleVocabulary(subscription_terms)
if self.user_is_subscribed_directly or self.user_is_muted:
default_subscription_value = self._update_subscription_term.value
else:
default_subscription_value = (
- subscription_vocabulary.getTermByToken(self.user.name).value)
+ subscription_vocabulary.getTermByToken(self.user.name).value
+ )
subscription_field = Choice(
- __name__='subscription', title=_("Subscription options"),
- vocabulary=subscription_vocabulary, required=True,
- default=default_subscription_value)
+ __name__="subscription",
+ title=_("Subscription options"),
+ vocabulary=subscription_vocabulary,
+ required=True,
+ default=default_subscription_value,
+ )
return subscription_field
def setUpFields(self):
@@ -305,31 +327,33 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
self.form_fields += formlib.form.Fields(self._subscription_field)
self._setUpBugNotificationLevelField()
- self.form_fields['subscription'].custom_widget = CustomWidgetFactory(
- RadioWidget)
+ self.form_fields["subscription"].custom_widget = CustomWidgetFactory(
+ RadioWidget
+ )
def setUpWidgets(self):
"""See `LaunchpadFormView`."""
super().setUpWidgets()
- self.widgets['subscription'].widget_class = 'bug-subscription-basic'
- self.widgets['bug_notification_level'].widget_class = (
- 'bug-notification-level-field')
- if (len(self.form_fields['subscription'].field.vocabulary) == 1):
+ self.widgets["subscription"].widget_class = "bug-subscription-basic"
+ self.widgets[
+ "bug_notification_level"
+ ].widget_class = "bug-notification-level-field"
+ if len(self.form_fields["subscription"].field.vocabulary) == 1:
# We hide the subscription widget if the user isn't
# subscribed, since we know who the subscriber is and we
# don't need to present them with a single radio button.
- self.widgets['subscription'].visible = False
+ self.widgets["subscription"].visible = False
else:
# We show the subscription widget when the user is
# subscribed via a team, because they can either
# subscribe theirself or unsubscribe their team.
- self.widgets['subscription'].visible = True
+ self.widgets["subscription"].visible = True
if self.user_is_subscribed_to_dupes_only:
# If the user is subscribed via a duplicate but is not
# directly subscribed, we hide the
# bug_notification_level field, since it's not used.
- self.widgets['bug_notification_level'].visible = False
+ self.widgets["bug_notification_level"].visible = False
@cachedproperty
def user_is_muted(self):
@@ -349,15 +373,17 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
def user_is_subscribed(self):
"""Is the user subscribed to this bug?"""
return (
- self.user_is_subscribed_directly or
- self.user_is_subscribed_to_dupes)
+ self.user_is_subscribed_directly
+ or self.user_is_subscribed_to_dupes
+ )
@property
def user_is_subscribed_to_dupes_only(self):
"""Is the user subscribed to this bug only via a dupe?"""
return (
- self.user_is_subscribed_to_dupes and
- not self.user_is_subscribed_directly)
+ self.user_is_subscribed_to_dupes
+ and not self.user_is_subscribed_directly
+ )
def shouldShowUnsubscribeFromDupesWarning(self):
"""Should we warn the user about unsubscribing and duplicates?
@@ -375,14 +401,15 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
return False
- @action('Continue', name='continue')
+ @action("Continue", name="continue")
def subscribe_action(self, action, data):
"""Handle subscription requests."""
- subscription_person = self.widgets['subscription'].getInputValue()
- bug_notification_level = data.get('bug_notification_level', None)
+ subscription_person = self.widgets["subscription"].getInputValue()
+ bug_notification_level = data.get("bug_notification_level", None)
- if (subscription_person == self._update_subscription_term.value and
- (self.user_is_subscribed or self.user_is_muted)):
+ if subscription_person == self._update_subscription_term.value and (
+ self.user_is_subscribed or self.user_is_muted
+ ):
if self.user_is_muted:
self._handleUnmute()
if self.user_is_subscribed:
@@ -391,8 +418,9 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
self._handleSubscribe(level=bug_notification_level)
elif self.user_is_muted and subscription_person == self.user:
self._handleUnmute()
- elif (not self.user_is_subscribed and
- (subscription_person == self.user)):
+ elif not self.user_is_subscribed and (
+ subscription_person == self.user
+ ):
self._handleSubscribe(bug_notification_level)
else:
self._handleUnsubscribe(subscription_person)
@@ -402,7 +430,8 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
"""Handle a subscribe request."""
self.context.bug.subscribe(self.user, self.user, level=level)
self.request.response.addNotification(
- "You have subscribed to this bug report.")
+ "You have subscribed to this bug report."
+ )
def _handleUnsubscribe(self, user):
"""Handle an unsubscribe request."""
@@ -421,12 +450,15 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
# if the bug is private, the current user will be prevented from
# calling methods on the main bug after they unsubscribe from it.
unsubed_dupes = self.context.bug.unsubscribeFromDupes(
- self.user, self.user)
+ self.user, self.user
+ )
self.context.bug.unsubscribe(self.user, self.user)
self.request.response.addNotification(
structured(
- self._getUnsubscribeNotification(self.user, unsubed_dupes)))
+ self._getUnsubscribeNotification(self.user, unsubed_dupes)
+ )
+ )
# Because the unsubscribe above may change what the security policy
# says about the bug, we need to clear its cache.
@@ -439,23 +471,25 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
def _handleUnsubscribeOtherUser(self, user):
"""Handle unsubscribing someone other than the current user."""
- assert user != self.user, (
- "Expected a user other than the currently logged-in user.")
+ assert (
+ user != self.user
+ ), "Expected a user other than the currently logged-in user."
# We'll also unsubscribe the other user from dupes of this bug,
# otherwise they'll keep getting this bug's mail.
self.context.bug.unsubscribe(user, self.user)
unsubed_dupes = self.context.bug.unsubscribeFromDupes(user, user)
self.request.response.addNotification(
- structured(
- self._getUnsubscribeNotification(user, unsubed_dupes)))
+ structured(self._getUnsubscribeNotification(user, unsubed_dupes))
+ )
def _handleUpdateSubscription(self, level):
"""Handle updating a user's subscription."""
subscription = self.current_user_subscription
subscription.bug_notification_level = level
self.request.response.addNotification(
- "Your bug report subscription has been updated.")
+ "Your bug report subscription has been updated."
+ )
def _getUnsubscribeNotification(self, user, unsubed_dupes):
"""Construct and return the unsubscribe-from-bug feedback message.
@@ -467,7 +501,8 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
current_bug = self.context.bug
current_user = self.user
unsubed_dupes_msg_fragment = self._getUnsubscribedDupesMsgFragment(
- unsubed_dupes)
+ unsubed_dupes
+ )
if user == current_user:
# Consider that the current user may have been "locked out"
@@ -478,17 +513,23 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
# special-casing needed.
return structured(
"You have been unsubscribed from bug %s%s.",
- current_bug.id, unsubed_dupes_msg_fragment).escapedtext
+ current_bug.id,
+ unsubed_dupes_msg_fragment,
+ ).escapedtext
else:
return structured(
"You have been unsubscribed from bug %s%s. You no "
"longer have access to this private bug.",
- current_bug.id, unsubed_dupes_msg_fragment).escapedtext
+ current_bug.id,
+ unsubed_dupes_msg_fragment,
+ ).escapedtext
else:
return structured(
"%s has been unsubscribed from bug %s%s.",
- user.displayname, current_bug.id,
- unsubed_dupes_msg_fragment).escapedtext
+ user.displayname,
+ current_bug.id,
+ unsubed_dupes_msg_fragment,
+ ).escapedtext
def _getUnsubscribedDupesMsgFragment(self, unsubed_dupes):
"""Return the duplicates fragment of the unsubscription notification.
@@ -501,13 +542,18 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
dupe_links = []
for unsubed_dupe in unsubed_dupes:
- dupe_links.append(structured(
- '<a href="%s" title="%s">#%s</a>',
- canonical_url(unsubed_dupe), unsubed_dupe.title,
- unsubed_dupe.id))
+ dupe_links.append(
+ structured(
+ '<a href="%s" title="%s">#%s</a>',
+ canonical_url(unsubed_dupe),
+ unsubed_dupe.title,
+ unsubed_dupe.id,
+ )
+ )
# We can't current join structured()s, so do it manually.
dupe_links_string = structured(
- ", ".join(['%s'] * len(dupe_links)), *dupe_links)
+ ", ".join(["%s"] * len(dupe_links)), *dupe_links
+ )
num_dupes = len(unsubed_dupes)
if num_dupes > 1:
@@ -518,8 +564,10 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
return structured(
" and %(num_dupes)s duplicate%(plural_suffix)s "
"(%(dupe_links_string)s)",
- num_dupes=num_dupes, plural_suffix=plural_suffix,
- dupe_links_string=dupe_links_string)
+ num_dupes=num_dupes,
+ plural_suffix=plural_suffix,
+ dupe_links_string=dupe_links_string,
+ )
class BugPortletSubscribersWithDetails(LaunchpadView):
@@ -551,21 +599,23 @@ class BugPortletSubscribersWithDetails(LaunchpadView):
# The security adaptor will do the job also but we don't want or
# need the expense of running several complex SQL queries.
precache_permission_for_objects(
- self.request, 'launchpad.LimitedView', [person])
+ self.request, "launchpad.LimitedView", [person]
+ )
subscriber = {
- 'name': person.name,
- 'display_name': person.displayname,
- 'web_link': canonical_url(person, rootsite='mainsite'),
- 'self_link': absoluteURL(person, self.api_request),
- 'is_team': person.is_team,
- 'can_edit': can_edit,
- 'display_subscribed_by': subscription.display_subscribed_by,
- }
+ "name": person.name,
+ "display_name": person.displayname,
+ "web_link": canonical_url(person, rootsite="mainsite"),
+ "self_link": absoluteURL(person, self.api_request),
+ "is_team": person.is_team,
+ "can_edit": can_edit,
+ "display_subscribed_by": subscription.display_subscribed_by,
+ }
record = {
- 'subscriber': subscriber,
- 'subscription_level': str(
- removeSecurityProxy(subscription.bug_notification_level)),
- }
+ "subscriber": subscriber,
+ "subscription_level": str(
+ removeSecurityProxy(subscription.bug_notification_level)
+ ),
+ }
data.append(record)
return data
@@ -581,7 +631,8 @@ class BugPortletSubscribersWithDetails(LaunchpadView):
include_private = self.user is not None
if include_private:
precache_permission_for_objects(
- self.request, 'launchpad.LimitedView', others)
+ self.request, "launchpad.LimitedView", others
+ )
for person in others:
if person == self.user:
# Skip the current user viewing the page,
@@ -590,17 +641,17 @@ class BugPortletSubscribersWithDetails(LaunchpadView):
# Do not include private teams if there's no logged in user.
continue
subscriber = {
- 'name': person.name,
- 'display_name': person.displayname,
- 'web_link': canonical_url(person, rootsite='mainsite'),
- 'self_link': absoluteURL(person, self.api_request),
- 'is_team': person.is_team,
- 'can_edit': False,
- }
+ "name": person.name,
+ "display_name": person.displayname,
+ "web_link": canonical_url(person, rootsite="mainsite"),
+ "self_link": absoluteURL(person, self.api_request),
+ "is_team": person.is_team,
+ "can_edit": False,
+ }
record = {
- 'subscriber': subscriber,
- 'subscription_level': 'Maybe',
- }
+ "subscriber": subscriber,
+ "subscription_level": "Maybe",
+ }
data.append(record)
return data
@@ -610,11 +661,11 @@ class BugPortletSubscribersWithDetails(LaunchpadView):
def render(self):
"""Override the default render() to return only JSON."""
- self.request.response.setHeader('content-type', 'application/json')
+ self.request.response.setHeader("content-type", "application/json")
return self.subscriber_data_js
-@delegate_to(IBugSubscription, context='subscription')
+@delegate_to(IBugSubscription, context="subscription")
class SubscriptionAttrDecorator:
"""A BugSubscription with added attributes for HTML/JS."""
@@ -623,7 +674,7 @@ class SubscriptionAttrDecorator:
@property
def css_name(self):
- return 'subscriber-%s' % self.subscription.person.id
+ return "subscriber-%s" % self.subscription.person.id
class BugSubscriptionListView(LaunchpadView):
@@ -631,17 +682,18 @@ class BugSubscriptionListView(LaunchpadView):
def initialize(self):
super().initialize()
- subscriptions = list(get_structural_subscriptions_for_bug(
- self.context.bug, self.user))
+ subscriptions = list(
+ get_structural_subscriptions_for_bug(self.context.bug, self.user)
+ )
expose_structural_subscription_data_to_js(
- self.context, self.request, self.user, subscriptions)
- subscriptions_info = PersonSubscriptions(
- self.user, self.context.bug)
+ self.context, self.request, self.user, subscriptions
+ )
+ subscriptions_info = PersonSubscriptions(self.user, self.context.bug)
subdata, references = subscriptions_info.getDataForClient()
cache = IJSONRequestCache(self.request).objects
cache.update(references)
- cache['bug_subscription_info'] = subdata
- cache['bug_is_private'] = self.context.bug.private
+ cache["bug_subscription_info"] = subdata
+ cache["bug_is_private"] = self.context.bug.private
@property
def label(self):
@@ -675,18 +727,24 @@ class BugMuteSelfView(LaunchpadFormView):
self.is_muted = self.context.bug.isMuted(self.user)
super().initialize()
- @action('Mute bug mail',
- name='mute',
- condition=lambda form, action: not form.is_muted)
+ @action(
+ "Mute bug mail",
+ name="mute",
+ condition=lambda form, action: not form.is_muted,
+ )
def mute_action(self, action, data):
self.context.bug.mute(self.user, self.user)
self.request.response.addInfoNotification(
- "Mail for bug #%s has been muted." % self.context.bug.id)
+ "Mail for bug #%s has been muted." % self.context.bug.id
+ )
- @action('Unmute bug mail',
- name='unmute',
- condition=lambda form, action: form.is_muted)
+ @action(
+ "Unmute bug mail",
+ name="unmute",
+ condition=lambda form, action: form.is_muted,
+ )
def unmute_action(self, action, data):
self.context.bug.unmute(self.user, self.user)
self.request.response.addInfoNotification(
- "Mail for bug #%s has been unmuted." % self.context.bug.id)
+ "Mail for bug #%s has been unmuted." % self.context.bug.id
+ )
diff --git a/lib/lp/bugs/browser/bugsubscriptionfilter.py b/lib/lp/bugs/browser/bugsubscriptionfilter.py
index e12219f..6e41c8d 100644
--- a/lib/lp/bugs/browser/bugsubscriptionfilter.py
+++ b/lib/lp/bugs/browser/bugsubscriptionfilter.py
@@ -5,16 +5,13 @@
__all__ = [
"BugSubscriptionFilterView",
- ]
+]
from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import TextWidget
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadEditFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadEditFormView, action
from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
from lp.bugs.browser.bugsubscription import AdvancedSubscriptionMixin
from lp.bugs.browser.widgets.bug import BugTagsFrozenSetWidget
@@ -22,23 +19,22 @@ from lp.bugs.enums import BugNotificationLevel
from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
from lp.services.helpers import english_list
from lp.services.propertycache import cachedproperty
-from lp.services.webapp.publisher import (
- canonical_url,
- LaunchpadView,
- )
+from lp.services.webapp.publisher import LaunchpadView, canonical_url
def bug_notification_level_description_mapping(displayname):
return {
BugNotificationLevel.LIFECYCLE: (
- "%s is fixed or re-opened." % displayname).capitalize(),
+ "%s is fixed or re-opened." % displayname
+ ).capitalize(),
BugNotificationLevel.METADATA: (
"Any change is made to %s, other than a new "
- "comment being added." % displayname),
+ "comment being added." % displayname
+ ),
BugNotificationLevel.COMMENTS: (
- "A change is made or a new comment is added to %s."
- % displayname),
- }
+ "A change is made or a new comment is added to %s." % displayname
+ ),
+ }
class BugSubscriptionFilterView(LaunchpadView):
@@ -72,9 +68,15 @@ class BugSubscriptionFilterView(LaunchpadView):
def _add_english_condition(self, conditions, variable, description):
if len(variable) > 0:
conditions.append(
- "the %s is %s" % (description, english_list(
- (kind.title for kind in sorted(variable)),
- conjunction="or")))
+ "the %s is %s"
+ % (
+ description,
+ english_list(
+ (kind.title for kind in sorted(variable)),
+ conjunction="or",
+ ),
+ )
+ )
@property
def conditions(self):
@@ -82,27 +84,34 @@ class BugSubscriptionFilterView(LaunchpadView):
conditions = []
bug_notification_level = self.context.bug_notification_level
if bug_notification_level < BugNotificationLevel.COMMENTS:
- mapping = bug_notification_level_description_mapping(
- 'the bug')
- conditions.append(
- mapping[bug_notification_level].lower()[:-1])
+ mapping = bug_notification_level_description_mapping("the bug")
+ conditions.append(mapping[bug_notification_level].lower()[:-1])
self._add_english_condition(
- conditions, self.context.statuses, 'status')
+ conditions, self.context.statuses, "status"
+ )
self._add_english_condition(
- conditions, self.context.importances, 'importance')
+ conditions, self.context.importances, "importance"
+ )
tags = self.context.tags
if len(tags) > 0:
conditions.append(
- "the bug is tagged with %s" % english_list(
- sorted(tags), conjunction=(
- "and" if self.context.find_all_tags else "or")))
+ "the bug is tagged with %s"
+ % english_list(
+ sorted(tags),
+ conjunction=(
+ "and" if self.context.find_all_tags else "or"
+ ),
+ )
+ )
self._add_english_condition(
- conditions, self.context.information_types, 'information type')
+ conditions, self.context.information_types, "information type"
+ )
return conditions
-class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
- AdvancedSubscriptionMixin):
+class BugSubscriptionFilterEditViewBase(
+ LaunchpadEditFormView, AdvancedSubscriptionMixin
+):
"""Base class for edit or create views of `IBugSubscriptionFilter`."""
schema = IBugSubscriptionFilter
@@ -113,15 +122,17 @@ class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
"information_types",
"tags",
"find_all_tags",
- )
+ )
custom_widget_description = CustomWidgetFactory(
- TextWidget, displayWidth=50)
+ TextWidget, displayWidth=50
+ )
custom_widget_statuses = LabeledMultiCheckBoxWidget
custom_widget_importances = LabeledMultiCheckBoxWidget
custom_widget_information_types = LabeledMultiCheckBoxWidget
custom_widget_tags = CustomWidgetFactory(
- BugTagsFrozenSetWidget, displayWidth=35)
+ BugTagsFrozenSetWidget, displayWidth=35
+ )
# Define in concrete subclass to be the target of the
# structural subscription that we are modifying.
@@ -133,7 +144,8 @@ class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
@cachedproperty
def _bug_notification_level_descriptions(self):
return bug_notification_level_description_mapping(
- 'a bug in %s' % self.target.displayname)
+ "a bug in %s" % self.target.displayname
+ )
def setUpFields(self):
"""Set up fields for form.
@@ -145,14 +157,12 @@ class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
@property
def next_url(self):
"""Return to the user's structural subscriptions page."""
- return canonical_url(
- self.user, view_name="+structural-subscriptions")
+ return canonical_url(self.user, view_name="+structural-subscriptions")
cancel_url = next_url
-class BugSubscriptionFilterEditView(
- BugSubscriptionFilterEditViewBase):
+class BugSubscriptionFilterEditView(BugSubscriptionFilterEditViewBase):
"""Edit view for `IBugSubscriptionFilter`.
:ivar context: A provider of `IBugSubscriptionFilter`.
@@ -180,8 +190,7 @@ class BugSubscriptionFilterEditView(
return self.context.structural_subscription.target
-class BugSubscriptionFilterCreateView(
- BugSubscriptionFilterEditViewBase):
+class BugSubscriptionFilterCreateView(BugSubscriptionFilterEditViewBase):
"""View to create a new `IBugSubscriptionFilter`.
:ivar context: A provider of `IStructuralSubscription`.
diff --git a/lib/lp/bugs/browser/bugsupervisor.py b/lib/lp/bugs/browser/bugsupervisor.py
index 44040f2..731eb69 100644
--- a/lib/lp/bugs/browser/bugsupervisor.py
+++ b/lib/lp/bugs/browser/bugsupervisor.py
@@ -4,16 +4,13 @@
"""Browser view for bug supervisor."""
__all__ = [
- 'BugSupervisorEditView',
- ]
+ "BugSupervisorEditView",
+]
from lazr.restful.interface import copy_field
from zope.interface import Interface
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadEditFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadEditFormView, action
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
from lp.services.webapp.escaping import structured
from lp.services.webapp.publisher import canonical_url
@@ -25,20 +22,22 @@ class BugSupervisorEditSchema(Interface):
This is necessary to make an editable field for bug supervisor as it is
defined as read-only in the interface to prevent setting it directly.
"""
+
bug_supervisor = copy_field(
- IHasBugSupervisor['bug_supervisor'], readonly=False)
+ IHasBugSupervisor["bug_supervisor"], readonly=False
+ )
class BugSupervisorEditView(LaunchpadEditFormView):
"""Browser view class for editing the bug supervisor."""
schema = BugSupervisorEditSchema
- field_names = ['bug_supervisor']
+ field_names = ["bug_supervisor"]
@property
def label(self):
"""The form label."""
- return 'Edit bug supervisor for %s' % self.context.displayname
+ return "Edit bug supervisor for %s" % self.context.displayname
@property
def page_title(self):
@@ -57,14 +56,15 @@ class BugSupervisorEditView(LaunchpadEditFormView):
cancel_url = next_url
- @action('Change', name='change')
+ @action("Change", name="change")
def change_action(self, action, data):
"""Redirect to the target page with a success message."""
self.updateContextFromData(data)
if self.context.bug_supervisor is None:
message = (
"Successfully cleared the bug supervisor. "
- "You can set the bug supervisor again at any time.")
+ "You can set the bug supervisor again at any time."
+ )
else:
- message = structured('Bug supervisor privilege granted.')
+ message = structured("Bug supervisor privilege granted.")
self.request.response.addNotification(message)
diff --git a/lib/lp/bugs/browser/bugtarget.py b/lib/lp/bugs/browser/bugtarget.py
index 1802a0e..5633d0a 100644
--- a/lib/lp/bugs/browser/bugtarget.py
+++ b/lib/lp/bugs/browser/bugtarget.py
@@ -15,16 +15,13 @@ __all__ = [
"ProductConfigureBugTrackerView",
"ProjectGroupFileBugGuidedView",
"product_to_productbugconfiguration",
- ]
+]
+import http.client
from datetime import datetime
from functools import partial
-import http.client
from io import BytesIO
-from urllib.parse import (
- quote,
- urlencode,
- )
+from urllib.parse import quote, urlencode
from lazr.restful.interface import copy_field
from lazr.restful.interfaces import IJSONRequestCache
@@ -35,15 +32,8 @@ from zope.component import getUtility
from zope.formlib.form import Fields
from zope.formlib.interfaces import InputErrors
from zope.formlib.widget import CustomWidgetFactory
-from zope.formlib.widgets import (
- TextAreaWidget,
- TextWidget,
- )
-from zope.interface import (
- alsoProvides,
- implementer,
- Interface,
- )
+from zope.formlib.widgets import TextAreaWidget, TextWidget
+from zope.interface import Interface, alsoProvides, implementer
from zope.publisher.interfaces import NotFound
from zope.publisher.interfaces.browser import IBrowserPublisher
from zope.schema import Choice
@@ -52,28 +42,25 @@ from zope.schema.vocabulary import SimpleVocabulary
from zope.security.proxy import removeSecurityProxy
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
+ action,
safe_action,
- )
+)
from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.enums import (
- InformationType,
PRIVATE_INFORMATION_TYPES,
PUBLIC_INFORMATION_TYPES,
+ InformationType,
ServiceUsage,
- )
-from lp.app.errors import (
- NotFoundError,
- UnexpectedFormData,
- )
+)
+from lp.app.errors import NotFoundError, UnexpectedFormData
from lp.app.interfaces.launchpad import (
IHeadingContext,
ILaunchpadCelebrities,
ILaunchpadUsage,
- )
+)
from lp.app.utilities import json_dump_information_types
from lp.app.validators.name import valid_name_pattern
from lp.app.vocabularies import InformationTypeVocabulary
@@ -82,14 +69,11 @@ from lp.app.widgets.product import (
GhostCheckBoxWidget,
GhostWidget,
ProductBugTrackerWidget,
- )
+)
from lp.bugs.browser.structuralsubscription import (
expose_structural_subscription_data_to_js,
- )
-from lp.bugs.browser.widgets.bug import (
- BugTagsWidget,
- LargeBugTagsWidget,
- )
+)
+from lp.bugs.browser.widgets.bug import BugTagsWidget, LargeBugTagsWidget
from lp.bugs.browser.widgets.bugtask import FileBugSourcePackageNameWidget
from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource
from lp.bugs.interfaces.bug import (
@@ -98,30 +82,30 @@ from lp.bugs.interfaces.bug import (
IBugAddForm,
IBugSet,
IProjectGroupBugAddForm,
- )
+)
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
from lp.bugs.interfaces.bugtarget import (
IBugTarget,
IOfficialBugTagTargetPublic,
IOfficialBugTagTargetRestricted,
- )
+)
from lp.bugs.interfaces.bugtask import (
+ UNRESOLVED_BUGTASK_STATUSES,
BugTaskImportance,
BugTaskStatus,
IBugTaskSet,
- UNRESOLVED_BUGTASK_STATUSES,
- )
+)
from lp.bugs.interfaces.bugtracker import IBugTracker
from lp.bugs.model.bugtask import BugTask
from lp.bugs.model.structuralsubscription import (
get_structural_subscriptions_for_target,
- )
+)
from lp.bugs.utilities.filebugdataparser import FileBugData
from lp.registry.browser.product import ProductConfigureBase
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
- )
+)
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPerson
@@ -135,43 +119,43 @@ 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 (
- canonical_url,
- LaunchpadView,
- urlappend,
- )
+from lp.services.webapp import LaunchpadView, canonical_url, urlappend
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.batching import BatchNavigator
from lp.services.webapp.escaping import structured
-
# A simple vocabulary for the subscribe_to_existing_bug form field.
SUBSCRIBE_TO_BUG_VOCABULARY = SimpleVocabulary.fromItems(
- [('yes', True), ('no', False)])
+ [("yes", True), ("no", False)]
+)
class IProductBugConfiguration(Interface):
"""A composite schema for editing bug app configuration."""
bug_supervisor = copy_field(
- IHasBugSupervisor['bug_supervisor'], readonly=False)
- official_malone = copy_field(ILaunchpadUsage['official_malone'])
+ IHasBugSupervisor["bug_supervisor"], readonly=False
+ )
+ official_malone = copy_field(ILaunchpadUsage["official_malone"])
enable_bug_expiration = copy_field(
- ILaunchpadUsage['enable_bug_expiration'])
- bugtracker = copy_field(IProduct['bugtracker'])
- remote_product = copy_field(IProduct['remote_product'])
+ ILaunchpadUsage["enable_bug_expiration"]
+ )
+ bugtracker = copy_field(IProduct["bugtracker"])
+ remote_product = copy_field(IProduct["remote_product"])
bug_reporting_guidelines = copy_field(
- IBugTarget['bug_reporting_guidelines'])
+ IBugTarget["bug_reporting_guidelines"]
+ )
bug_reported_acknowledgement = copy_field(
- IBugTarget['bug_reported_acknowledgement'])
+ IBugTarget["bug_reported_acknowledgement"]
+ )
enable_bugfiling_duplicate_search = copy_field(
- IBugTarget['enable_bugfiling_duplicate_search'])
+ IBugTarget["enable_bugfiling_duplicate_search"]
+ )
def product_to_productbugconfiguration(product):
"""Adapts an `IProduct` into an `IProductBugConfiguration`."""
- alsoProvides(
- removeSecurityProxy(product), IProductBugConfiguration)
+ alsoProvides(removeSecurityProxy(product), IProductBugConfiguration)
return product
@@ -196,7 +180,7 @@ class ProductConfigureBugTrackerView(ProductConfigureBase):
"bug_reporting_guidelines",
"bug_reported_acknowledgement",
"enable_bugfiling_duplicate_search",
- ]
+ ]
if check_permission("launchpad.Edit", self.context):
field_names.append("bug_supervisor")
@@ -210,11 +194,11 @@ class ProductConfigureBugTrackerView(ProductConfigureBase):
# JavaScript fails to activate or run. Note that the bugtracker
# name : values are {'In Launchpad' : object, 'Somewhere else' : None
# 'In a registered bug tracker' : IBugTracker}.
- bugtracker = data.get('bugtracker', None)
+ bugtracker = data.get("bugtracker", None)
if bugtracker is None or IBugTracker.providedBy(bugtracker):
- data['enable_bug_expiration'] = False
+ data["enable_bug_expiration"] = False
- @action("Change", name='change')
+ @action("Change", name="change")
def change_action(self, action, data):
self.updateContextFromData(data)
@@ -227,7 +211,8 @@ class FileBugViewBase(LaunchpadFormView):
custom_widget_information_type = LaunchpadRadioWidgetWithDescription
custom_widget_comment = CustomWidgetFactory(
- TextAreaWidget, cssClass='comment-text')
+ TextAreaWidget, cssClass="comment-text"
+ )
custom_widget_packagename = FileBugSourcePackageNameWidget
extra_data_token = None
@@ -253,14 +238,18 @@ class FileBugViewBase(LaunchpadFormView):
# the form is rendered during LaunchpadFormView's initialize()
# when an action is invokved.
cache = IJSONRequestCache(self.request)
- cache.objects['bug_private_by_default'] = (
- self.context.pillar.getDefaultBugInformationType() in
- PRIVATE_INFORMATION_TYPES)
+ cache.objects["bug_private_by_default"] = (
+ self.context.pillar.getDefaultBugInformationType()
+ in PRIVATE_INFORMATION_TYPES
+ )
json_dump_information_types(
- cache, self.context.pillar.getAllowedBugInformationTypes())
+ cache, self.context.pillar.getAllowedBugInformationTypes()
+ )
bugtask_status_data = vocabulary_to_choice_edit_items(
- BugTaskStatus, include_description=True, css_class_prefix='status',
+ BugTaskStatus,
+ include_description=True,
+ css_class_prefix="status",
excluded_items=[
BugTaskStatus.UNKNOWN,
BugTaskStatus.EXPIRED,
@@ -268,95 +257,122 @@ class FileBugViewBase(LaunchpadFormView):
BugTaskStatus.OPINION,
BugTaskStatus.WONTFIX,
BugTaskStatus.INCOMPLETE,
- BugTaskStatus.DOESNOTEXIST])
- cache.objects['bugtask_status_data'] = bugtask_status_data
+ BugTaskStatus.DOESNOTEXIST,
+ ],
+ )
+ cache.objects["bugtask_status_data"] = bugtask_status_data
bugtask_importance_data = vocabulary_to_choice_edit_items(
- BugTaskImportance, include_description=True,
- css_class_prefix='importance',
- excluded_items=[BugTaskImportance.UNKNOWN])
- cache.objects['bugtask_importance_data'] = bugtask_importance_data
- cache.objects['enable_bugfiling_duplicate_search'] = (
- self.context.enable_bugfiling_duplicate_search)
+ BugTaskImportance,
+ include_description=True,
+ css_class_prefix="importance",
+ excluded_items=[BugTaskImportance.UNKNOWN],
+ )
+ cache.objects["bugtask_importance_data"] = bugtask_importance_data
+ cache.objects[
+ "enable_bugfiling_duplicate_search"
+ ] = self.context.enable_bugfiling_duplicate_search
super().initialize()
- if (self.extra_data_token is not None and
- not self.extra_data_to_process):
+ if (
+ self.extra_data_token is not None
+ and not self.extra_data_to_process
+ ):
# self.extra_data has been initialized in publishTraverse().
if self.extra_data.initial_summary:
- self.widgets['title'].setRenderedValue(
- self.extra_data.initial_summary)
+ self.widgets["title"].setRenderedValue(
+ self.extra_data.initial_summary
+ )
if self.extra_data.initial_tags:
- self.widgets['tags'].setRenderedValue(
- self.extra_data.initial_tags)
+ self.widgets["tags"].setRenderedValue(
+ self.extra_data.initial_tags
+ )
# XXX: Bjorn Tillenius 2006-01-15:
# We should include more details of what will be added
# to the bug report.
self.request.response.addNotification(
- 'Extra debug information will be added to the bug report'
- ' automatically.')
+ "Extra debug information will be added to the bug report"
+ " automatically."
+ )
@cachedproperty
def redirect_ubuntu_filebug(self):
if IDistribution.providedBy(self.context):
bug_supervisor = self.context.bug_supervisor
- elif (IDistributionSourcePackage.providedBy(self.context) or
- ISourcePackage.providedBy(self.context)):
+ elif IDistributionSourcePackage.providedBy(
+ self.context
+ ) or ISourcePackage.providedBy(self.context):
bug_supervisor = self.context.distribution.bug_supervisor
else:
bug_supervisor = None
# Work out whether the redirect should be overidden.
do_not_redirect = (
- self.request.form.get('no-redirect') is not None or
- [key for key in self.request.form.keys()
- if 'field.actions' in key] != [] or
- self.user.inTeam(bug_supervisor))
+ self.request.form.get("no-redirect") is not None
+ or [
+ key
+ for key in self.request.form.keys()
+ if "field.actions" in key
+ ]
+ != []
+ or self.user.inTeam(bug_supervisor)
+ )
return (
- config.malone.ubuntu_disable_filebug and
- self.targetIsUbuntu() and
- self.extra_data_token is None and
- not do_not_redirect)
+ config.malone.ubuntu_disable_filebug
+ and self.targetIsUbuntu()
+ and self.extra_data_token is None
+ and not do_not_redirect
+ )
@property
def field_names(self):
"""Return the list of field names to display."""
context = self.context
- field_names = ['title', 'comment', 'tags']
+ field_names = ["title", "comment", "tags"]
if self.is_bug_supervisor:
- field_names.append('information_type')
+ field_names.append("information_type")
else:
- field_names.append('security_related')
- field_names.extend([
- 'bug_already_reported_as', 'filecontent', 'patch',
- 'attachment_description', 'subscribe_to_existing_bug'])
- if (IDistribution.providedBy(context) or
- IDistributionSourcePackage.providedBy(context)):
- field_names.append('packagename')
+ field_names.append("security_related")
+ field_names.extend(
+ [
+ "bug_already_reported_as",
+ "filecontent",
+ "patch",
+ "attachment_description",
+ "subscribe_to_existing_bug",
+ ]
+ )
+ if IDistribution.providedBy(
+ context
+ ) or IDistributionSourcePackage.providedBy(context):
+ field_names.append("packagename")
if self.is_bug_supervisor:
field_names.extend(
- ['assignee', 'importance', 'milestone', 'status'])
+ ["assignee", "importance", "milestone", "status"]
+ )
return field_names
@property
def default_information_type(self):
value = self.context.pillar.getDefaultBugInformationType()
- if (self.extra_data
+ if (
+ self.extra_data
and self.extra_data.private
- and value in PUBLIC_INFORMATION_TYPES):
+ and value in PUBLIC_INFORMATION_TYPES
+ ):
value = InformationType.USERDATA
return value
@property
def initial_values(self):
"""Give packagename a default value, if applicable."""
- values = {'information_type': self.default_information_type}
+ values = {"information_type": self.default_information_type}
if IDistributionSourcePackage.providedBy(self.context):
- values['packagename'] = self.context.name
+ values["packagename"] = self.context.name
return values
@@ -373,39 +389,50 @@ class FileBugViewBase(LaunchpadFormView):
def getPackageNameFieldCSSClass(self):
"""Return the CSS class for the packagename field."""
if self.widget_errors.get("packagename"):
- return 'error'
+ return "error"
else:
- return ''
+ return ""
def validate(self, data):
"""Make sure the package name, if provided, exists in the distro."""
# The comment field is only required if filing a new bug.
if self.submit_bug_action.submitted():
- comment = data.get('comment')
+ comment = data.get("comment")
# The widget only exposes the error message. The private
# attr contains the real error.
- widget_error = self.widgets.get('comment')._error
+ widget_error = self.widgets.get("comment")._error
if widget_error and isinstance(widget_error.errors, TooLong):
- self.setFieldError('comment',
- 'The description is too long. If you have lots of '
- 'text to add, attach a file to the bug instead.')
+ self.setFieldError(
+ "comment",
+ "The description is too long. If you have lots of "
+ "text to add, attach a file to the bug instead.",
+ )
elif not comment or widget_error is not None:
self.setFieldError(
- 'comment', "Provide details about the issue.")
+ "comment", "Provide details about the issue."
+ )
# Check if there is an extra description, and if it is too long.
if self.extra_data.extra_description:
- if len("%s\n\n%s" % (
- comment, self.extra_data.extra_description)) > 50000:
+ if (
+ len(
+ "%s\n\n%s"
+ % (comment, self.extra_data.extra_description)
+ )
+ > 50000
+ ):
self.setFieldError(
- 'comment', 'The description and the additional '
- 'information is too long. If you have lots of text '
- 'to add, attach a file to the bug instead.')
+ "comment",
+ "The description and the additional "
+ "information is too long. If you have lots of text "
+ "to add, attach a file to the bug instead.",
+ )
# Check a bug has been selected when the user wants to
# subscribe to an existing bug.
elif self.this_is_my_bug_action.submitted():
- if not data.get('bug_already_reported_as'):
- self.setFieldError('bug_already_reported_as',
- "Please choose a bug.")
+ if not data.get("bug_already_reported_as"):
+ self.setFieldError(
+ "bug_already_reported_as", "Please choose a bug."
+ )
else:
# We only care about those two actions.
pass
@@ -424,7 +451,7 @@ class FileBugViewBase(LaunchpadFormView):
distribution = self.context.distribution
try:
- if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
+ if bool(getFeatureFlag("disclosure.dsp_picker.enabled")):
dsp_vocab = self.widgets.get("packagename").vocabulary
dsp_vocab.setDistribution(distribution)
dsp_vocab.getTermByToken(packagename)
@@ -433,7 +460,8 @@ class FileBugViewBase(LaunchpadFormView):
# vocabulary was used, so it needs secondary
# verification.
distribution.guessPublishedSourcePackageName(
- packagename)
+ packagename
+ )
except (LookupError, NotFoundError):
if distribution.series:
# If a distribution doesn't have any series,
@@ -443,20 +471,23 @@ class FileBugViewBase(LaunchpadFormView):
packagename_error = (
'"%s" does not exist in %s. Please choose a '
"different package. If you're unsure, please "
- 'select "I don\'t know"' % (
- packagename, distribution.displayname))
+ 'select "I don\'t know"'
+ % (packagename, distribution.displayname)
+ )
self.setFieldError("packagename", packagename_error)
else:
- self.setFieldError("packagename",
- "Please enter a package name")
+ self.setFieldError(
+ "packagename", "Please enter a package name"
+ )
def setUpWidgets(self):
"""Customize the onKeyPress event of the package name chooser."""
super().setUpWidgets()
if "packagename" in self.field_names:
- self.widgets["packagename"].onKeyPress = (
- "selectWidget('choose', event)")
+ self.widgets[
+ "packagename"
+ ].onKeyPress = "selectWidget('choose', event)"
def setUpFields(self):
"""Set up the form fields. See `LaunchpadFormView`."""
@@ -464,27 +495,33 @@ class FileBugViewBase(LaunchpadFormView):
if self.is_bug_supervisor:
info_type_vocab = InformationTypeVocabulary(
- types=self.context.pillar.getAllowedBugInformationTypes())
+ types=self.context.pillar.getAllowedBugInformationTypes()
+ )
information_type_field = copy_field(
- IBug['information_type'], readonly=False,
- vocabulary=info_type_vocab)
- self.form_fields = self.form_fields.omit('information_type')
+ IBug["information_type"],
+ readonly=False,
+ vocabulary=info_type_vocab,
+ )
+ self.form_fields = self.form_fields.omit("information_type")
self.form_fields += Fields(information_type_field)
else:
security_related_field = copy_field(
- IBug['security_related'], readonly=False)
- self.form_fields = self.form_fields.omit('security_related')
+ IBug["security_related"], readonly=False
+ )
+ self.form_fields = self.form_fields.omit("security_related")
self.form_fields += Fields(security_related_field)
# Override the vocabulary for the subscribe_to_existing_bug
# field.
subscribe_field = Choice(
- __name__='subscribe_to_existing_bug',
- title='Subscribe to this bug',
+ __name__="subscribe_to_existing_bug",
+ title="Subscribe to this bug",
vocabulary=SUBSCRIBE_TO_BUG_VOCABULARY,
- required=True, default=False)
+ required=True,
+ default=False,
+ )
- self.form_fields = self.form_fields.omit('subscribe_to_existing_bug')
+ self.form_fields = self.form_fields.omit("subscribe_to_existing_bug")
self.form_fields += Fields(subscribe_field)
def contextUsesMalone(self):
@@ -493,20 +530,23 @@ class FileBugViewBase(LaunchpadFormView):
return bug_tracking_usage == ServiceUsage.LAUNCHPAD
def contextAllowsNewBugs(self):
- return (self.contextUsesMalone() and
- self.getMainContext().getAllowedBugInformationTypes())
+ return (
+ self.contextUsesMalone()
+ and self.getMainContext().getAllowedBugInformationTypes()
+ )
def shouldSelectPackageName(self):
"""Should the radio button to select a package be selected?"""
- return (
- self.request.form.get("field.packagename") or
- self.initial_values.get("packagename"))
+ return self.request.form.get(
+ "field.packagename"
+ ) or self.initial_values.get("packagename")
def handleSubmitBugFailure(self, action, data, errors):
return self.showFileBugForm()
- @action("Submit Bug Report", name="submit_bug",
- failure=handleSubmitBugFailure)
+ @action(
+ "Submit Bug Report", name="submit_bug", failure=handleSubmitBugFailure
+ )
def submit_bug_action(self, action, data):
"""Add a bug to this IBugTarget."""
title = data["title"]
@@ -532,16 +572,21 @@ class FileBugViewBase(LaunchpadFormView):
if IDistributionSourcePackage.providedBy(context):
context = context.distribution
- linkified_ack = structured(FormattersAPI(
- self.getAcknowledgementMessage(self.context)).text_to_html(
- last_paragraph_class="last"))
+ linkified_ack = structured(
+ FormattersAPI(
+ self.getAcknowledgementMessage(self.context)
+ ).text_to_html(last_paragraph_class="last")
+ )
notifications = [linkified_ack]
params = CreateBugParams(
- title=title, comment=comment, owner=self.user,
+ title=title,
+ comment=comment,
+ owner=self.user,
information_type=information_type,
- tags=data.get('tags'))
+ tags=data.get("tags"),
+ )
if IDistribution.providedBy(context) and packagename:
- if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
+ if bool(getFeatureFlag("disclosure.dsp_picker.enabled")):
context = packagename
else:
# We don't know if the package name we got was a source or
@@ -550,88 +595,105 @@ class FileBugViewBase(LaunchpadFormView):
packagename = str(packagename.name)
try:
sourcepackagename = (
- context.guessPublishedSourcePackageName(packagename))
+ context.guessPublishedSourcePackageName(packagename)
+ )
except NotFoundError:
notifications.append(
"The package %s is not published in %s; the "
"bug was targeted only to the distribution."
- % (packagename, context.displayname))
+ % (packagename, context.displayname)
+ )
params.comment += (
"\r\n\r\nNote: the original reporter indicated "
"the bug was in package %r; however, that package "
- "was not published in %s." % (
- packagename, context.displayname))
+ "was not published in %s."
+ % (packagename, context.displayname)
+ )
else:
context = context.getSourcePackage(sourcepackagename.name)
extra_data = self.extra_data
if extra_data.extra_description:
params.comment = "%s\n\n%s" % (
- params.comment, extra_data.extra_description)
+ params.comment,
+ extra_data.extra_description,
+ )
notifications.append(
- 'Additional information was added to the bug description.')
+ "Additional information was added to the bug description."
+ )
# Apply any extra options given by privileged users.
if self.is_bug_supervisor:
- if 'assignee' in data:
- params.assignee = data['assignee']
- if 'status' in data:
- params.status = data['status']
- if 'importance' in data:
- params.importance = data['importance']
- if 'milestone' in data:
- params.milestone = data['milestone']
+ if "assignee" in data:
+ params.assignee = data["assignee"]
+ if "status" in data:
+ params.status = data["status"]
+ if "importance" in data:
+ params.importance = data["importance"]
+ if "milestone" in data:
+ params.milestone = data["milestone"]
self.added_bug = bug = context.createBug(params)
for comment in extra_data.comments:
bug.newMessage(self.user, bug.followup_subject(), comment)
notifications.append(
- 'A comment with additional information was added to the'
- ' bug report.')
+ "A comment with additional information was added to the"
+ " bug report."
+ )
# XXX 2007-01-19 gmb:
# We need to have a proper FileUpload widget rather than
# this rather hackish solution.
- attachment = self.request.form.get(self.widgets['filecontent'].name)
+ attachment = self.request.form.get(self.widgets["filecontent"].name)
if attachment or extra_data.attachments:
# Attach all the comments to a single empty comment.
attachment_comment = bug.newMessage(
- owner=self.user, subject=bug.followup_subject(), content=None,
- send_notifications=False)
+ owner=self.user,
+ subject=bug.followup_subject(),
+ content=None,
+ send_notifications=False,
+ )
# Deal with attachments added in the filebug form.
if attachment:
# We convert slashes in filenames to hyphens to avoid
# problems.
- filename = attachment.filename.replace('/', '-')
+ filename = attachment.filename.replace("/", "-")
# If the user hasn't entered a description for the
# attachment we use its name.
file_description = None
- if 'attachment_description' in data:
- file_description = data['attachment_description']
+ if "attachment_description" in data:
+ file_description = data["attachment_description"]
if file_description is None:
file_description = filename
bug.addAttachment(
- owner=self.user, data=BytesIO(data['filecontent']),
- filename=filename, description=file_description,
- comment=attachment_comment, is_patch=data['patch'])
+ owner=self.user,
+ data=BytesIO(data["filecontent"]),
+ filename=filename,
+ description=file_description,
+ comment=attachment_comment,
+ is_patch=data["patch"],
+ )
notifications.append(
- 'The file "%s" was attached to the bug report.'
- % filename)
+ 'The file "%s" was attached to the bug report.' % filename
+ )
for attachment in extra_data.attachments:
bug.linkAttachment(
- owner=self.user, file_alias=attachment['file_alias'],
- description=attachment['description'],
+ owner=self.user,
+ file_alias=attachment["file_alias"],
+ description=attachment["description"],
comment=attachment_comment,
- send_notifications=False)
+ send_notifications=False,
+ )
notifications.append(
'The file "%s" was attached to the bug report.'
- % attachment['file_alias'].filename)
+ % attachment["file_alias"].filename
+ )
if extra_data.subscribers:
# Subscribe additional subscribers to this bug
@@ -639,7 +701,8 @@ class FileBugViewBase(LaunchpadFormView):
valid_person_vocabulary = ValidPersonOrTeamVocabulary()
try:
person = valid_person_vocabulary.getTermByToken(
- subscriber).value
+ subscriber
+ ).value
except LookupError:
# We cannot currently pass this error up to the user, so
# we'll just ignore it.
@@ -647,8 +710,9 @@ class FileBugViewBase(LaunchpadFormView):
else:
bug.subscribe(person, self.user)
notifications.append(
- '%s has been subscribed to this bug.' %
- person.displayname)
+ "%s has been subscribed to this bug."
+ % person.displayname
+ )
# Give the user some feedback on the bug just opened.
for notification in notifications:
@@ -656,44 +720,55 @@ class FileBugViewBase(LaunchpadFormView):
if bug.information_type == InformationType.PRIVATESECURITY:
self.request.response.addNotification(
structured(
- 'Security-related bugs are by default private '
- '(visible only to their direct subscribers). '
- 'You may choose to <a href="+secrecy">publicly '
- 'disclose</a> this bug.'))
+ "Security-related bugs are by default private "
+ "(visible only to their direct subscribers). "
+ 'You may choose to <a href="+secrecy">publicly '
+ "disclose</a> this bug."
+ )
+ )
elif bug.information_type in PRIVATE_INFORMATION_TYPES:
self.request.response.addNotification(
structured(
- 'This bug report has been marked private '
- '(visible only to its direct subscribers). '
- 'You may choose to <a href="+secrecy">change this</a>.'))
+ "This bug report has been marked private "
+ "(visible only to its direct subscribers). "
+ 'You may choose to <a href="+secrecy">change this</a>.'
+ )
+ )
self.request.response.redirect(canonical_url(bug.bugtasks[0]))
- @action("Yes, this is the bug I'm trying to report",
- name="this_is_my_bug", failure=handleSubmitBugFailure)
+ @action(
+ "Yes, this is the bug I'm trying to report",
+ name="this_is_my_bug",
+ failure=handleSubmitBugFailure,
+ )
def this_is_my_bug_action(self, action, data):
"""Subscribe to the bug suggested."""
- bug = data.get('bug_already_reported_as')
- subscribe = data.get('subscribe_to_existing_bug')
+ bug = data.get("bug_already_reported_as")
+ subscribe = data.get("subscribe_to_existing_bug")
if bug.isUserAffected(self.user):
self.request.response.addNotification(
- "This bug is already marked as affecting you.")
+ "This bug is already marked as affecting you."
+ )
else:
bug.markUserAffected(self.user)
self.request.response.addNotification(
- "This bug has been marked as affecting you.")
+ "This bug has been marked as affecting you."
+ )
# If the user wants to be subscribed, subscribe them, unless
# they're already subscribed.
if subscribe:
if bug.isSubscribed(self.user):
self.request.response.addNotification(
- "You are already subscribed to this bug.")
+ "You are already subscribed to this bug."
+ )
else:
bug.subscribe(self.user, self.user)
self.request.response.addNotification(
- "You have subscribed to this bug report.")
+ "You have subscribed to this bug report."
+ )
self.next_url = canonical_url(bug.bugtasks[0])
@@ -708,7 +783,7 @@ class FileBugViewBase(LaunchpadFormView):
If a token was passed to this view, it will be passed through
to the inline bug filing form via the returned URL.
"""
- url = canonical_url(self.context, view_name='+filebug-inline-form')
+ url = canonical_url(self.context, view_name="+filebug-inline-form")
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
@@ -716,7 +791,7 @@ class FileBugViewBase(LaunchpadFormView):
@property
def duplicate_search_url(self):
"""Return the URL to the inline duplicate search view."""
- url = canonical_url(self.context, view_name='+filebug-show-similar')
+ url = canonical_url(self.context, view_name="+filebug-show-similar")
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
@@ -759,7 +834,7 @@ class FileBugViewBase(LaunchpadFormView):
# required=True. Instead it's validated in
# FileBugViewBase.validate. So... we need to suppress the
# "(Optional)" marker.
- if field_name == 'comment':
+ if field_name == "comment":
return False
else:
return LaunchpadFormView.showOptionalMarker(self, field_name)
@@ -809,11 +884,13 @@ class FileBugViewBase(LaunchpadFormView):
# bug_reported_acknowledgement from their IDistribution, so we
# don't need to look up this property in IDistribution.
# IDistribution and IProjectGroup don't have any parents.
- elif (IDistribution.providedBy(context) or
- IProjectGroup.providedBy(context) or
- IDistroSeries.providedBy(context) or
- ISourcePackage.providedBy(context) or
- IOCIProject.providedBy(context)):
+ elif (
+ IDistribution.providedBy(context)
+ or IProjectGroup.providedBy(context)
+ or IDistroSeries.providedBy(context)
+ or ISourcePackage.providedBy(context)
+ or IOCIProject.providedBy(context)
+ ):
pass
else:
raise TypeError("Unexpected bug target: %r" % context)
@@ -831,7 +908,8 @@ class FileBugViewBase(LaunchpadFormView):
try:
return getUtility(IProcessApportBlobJobSource).getByBlobUUID(
- self.extra_data_token)
+ self.extra_data_token
+ )
except NotFoundError:
return None
@@ -856,10 +934,12 @@ class FileBugViewBase(LaunchpadFormView):
guidelines = []
content = self.context.bug_reporting_guidelines
if content is not None and len(content) > 0:
- guidelines.append({
- "source": self.context.bugtargetdisplayname,
- "content": content,
- })
+ guidelines.append(
+ {
+ "source": self.context.bugtargetdisplayname,
+ "content": content,
+ }
+ )
# Distribution source packages are shown with both their
# own reporting guidelines and those of their
# distribution.
@@ -867,10 +947,12 @@ class FileBugViewBase(LaunchpadFormView):
distribution = self.context.distribution
content = distribution.bug_reporting_guidelines
if content is not None and len(content) > 0:
- guidelines.append({
- "source": distribution.bugtargetdisplayname,
- "content": content,
- })
+ guidelines.append(
+ {
+ "source": distribution.bugtargetdisplayname,
+ "content": content,
+ }
+ )
return guidelines
def getMainContext(self):
@@ -881,10 +963,11 @@ class FileBugViewBase(LaunchpadFormView):
@cachedproperty
def is_bug_supervisor(self):
- """ Return True if the logged in user is a bug supervisor."""
+ """Return True if the logged in user is a bug supervisor."""
context = self.getMainContext()
return BugTask.userHasBugSupervisorPrivilegesContext(
- context, self.user)
+ context, self.user
+ )
class FileBugAdvancedView(LaunchpadView):
@@ -895,15 +978,18 @@ class FileBugAdvancedView(LaunchpadView):
def initialize(self):
filebug_url = canonical_url(
- self.context, rootsite='bugs', view_name='+filebug')
+ self.context, rootsite="bugs", view_name="+filebug"
+ )
self.request.response.redirect(
- filebug_url, status=http.client.MOVED_PERMANENTLY)
+ filebug_url, status=http.client.MOVED_PERMANENTLY
+ )
class IDistroBugAddForm(IBugAddForm):
packagename = copy_field(
- IBugAddForm['packagename'], vocabularyName='DistributionSourcePackage')
+ IBugAddForm["packagename"], vocabularyName="DistributionSourcePackage"
+ )
class FilebugShowSimilarBugsView(FileBugViewBase):
@@ -915,7 +1001,7 @@ class FilebugShowSimilarBugsView(FileBugViewBase):
@property
def schema(self):
- if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
+ if bool(getFeatureFlag("disclosure.dsp_picker.enabled")):
return IDistroBugAddForm
else:
return IBugAddForm
@@ -936,7 +1022,7 @@ class FilebugShowSimilarBugsView(FileBugViewBase):
This enables better validation error handling,
since the form is always used inline on the +filebug page.
"""
- url = '%s/+filebug' % canonical_url(self.context)
+ url = "%s/+filebug" % canonical_url(self.context)
if self.extra_data_token is not None:
url = urlappend(url, self.extra_data_token)
return url
@@ -944,7 +1030,7 @@ class FilebugShowSimilarBugsView(FileBugViewBase):
@property
def search_text(self):
"""Return the search string entered by the user."""
- return self.request.get('title')
+ return self.request.get("title")
@cachedproperty
def similar_bugs(self):
@@ -953,20 +1039,24 @@ class FilebugShowSimilarBugsView(FileBugViewBase):
if not title:
return []
elif IProduct.providedBy(self.context):
- context_params = {'product': self.context}
+ context_params = {"product": self.context}
elif IDistribution.providedBy(self.context):
- context_params = {'distribution': self.context}
+ context_params = {"distribution": self.context}
else:
assert IDistributionSourcePackage.providedBy(self.context), (
- 'Unknown search context: %r' % self.context)
+ "Unknown search context: %r" % self.context
+ )
context_params = {
- 'distribution': self.context.distribution,
- 'sourcepackagename': self.context.sourcepackagename}
+ "distribution": self.context.distribution,
+ "sourcepackagename": self.context.sourcepackagename,
+ }
matching_bugtasks = getUtility(IBugTaskSet).findSimilar(
- self.user, title, **context_params)
+ self.user, title, **context_params
+ )
matching_bugs = getUtility(IBugSet).getDistinctBugsForBugTasks(
- matching_bugtasks, self.user, self._MATCHING_BUGS_LIMIT)
+ matching_bugtasks, self.user, self._MATCHING_BUGS_LIMIT
+ )
return matching_bugs
@property
@@ -979,21 +1069,25 @@ class FilebugShowSimilarBugsView(FileBugViewBase):
- There are no widget errors.
"""
return (
- self.contextUsesMalone() and
- len(self.similar_bugs) > 0 and
- len(self.widget_errors) == 0)
+ self.contextUsesMalone()
+ and len(self.similar_bugs) > 0
+ and len(self.widget_errors) == 0
+ )
class FileBugGuidedView(FilebugShowSimilarBugsView):
- page_title = 'Report a bug'
+ page_title = "Report a bug"
_SEARCH_FOR_DUPES = ViewPageTemplateFile(
- "../templates/bugtarget-filebug-search.pt")
+ "../templates/bugtarget-filebug-search.pt"
+ )
_PROJECTGROUP_SEARCH_FOR_DUPES = ViewPageTemplateFile(
- "../templates/projectgroup-filebug-search.pt")
+ "../templates/projectgroup-filebug-search.pt"
+ )
_FILEBUG_FORM = ViewPageTemplateFile(
- "../templates/bugtarget-filebug-submit-bug.pt")
+ "../templates/bugtarget-filebug-submit-bug.pt"
+ )
# XXX 2009-07-17 Graham Binns
# As above, this assignment to actions is to make sure that the
@@ -1002,7 +1096,7 @@ class FileBugGuidedView(FilebugShowSimilarBugsView):
actions = FilebugShowSimilarBugsView.actions
template = _SEARCH_FOR_DUPES
- focused_element_id = 'field.title'
+ focused_element_id = "field.title"
show_summary_in_results = True
def initialize(self):
@@ -1011,8 +1105,7 @@ class FileBugGuidedView(FilebugShowSimilarBugsView):
# The user is trying to file a new Ubuntu bug via the web
# interface and without using apport. Redirect to a page
# explaining the preferred bug-filing procedure.
- self.request.response.redirect(
- config.malone.ubuntu_bug_filing_url)
+ self.request.response.redirect(config.malone.ubuntu_bug_filing_url)
@safe_action
@action("Continue", name="search")
@@ -1027,15 +1120,14 @@ class FileBugGuidedView(FilebugShowSimilarBugsView):
def search_text(self):
"""Return the search string entered by the user."""
try:
- return self.widgets['title'].getInputValue()
+ return self.widgets["title"].getInputValue()
except InputErrors:
return None
def validate_no_dupe_found(self, action, data):
return ()
- @action("Continue", name="continue",
- validator="validate_no_dupe_found")
+ @action("Continue", name="continue", validator="validate_no_dupe_found")
def continue_action(self, action, data):
"""The same action as no-dupe-found, with a different label."""
return self.showFileBugForm()
@@ -1047,7 +1139,7 @@ class FileBugGuidedView(FilebugShowSimilarBugsView):
class ProjectGroupFileBugGuidedView(LaunchpadFormView):
"""Guided filebug pages for IProjectGroup."""
- page_title = 'Report a bug'
+ page_title = "Report a bug"
schema = IProjectGroupBugAddForm
@@ -1058,13 +1150,15 @@ class ProjectGroupFileBugGuidedView(LaunchpadFormView):
@property
def field_names(self):
- return ['product', 'title', 'tags']
+ return ["product", "title", "tags"]
@cachedproperty
def products_using_malone(self):
return [
- product for product in self.context.products
- if product.bug_tracking_usage == ServiceUsage.LAUNCHPAD]
+ product
+ for product in self.context.products
+ if product.bug_tracking_usage == ServiceUsage.LAUNCHPAD
+ ]
@property
def default_product(self):
@@ -1093,13 +1187,16 @@ class ProjectGroupFileBugGuidedView(LaunchpadFormView):
def projectgroup_search_action(self, action, data):
"""Redirect to the chosen product's form."""
base = canonical_url(
- data['product'], view_name='+filebug', rootsite='bugs')
- title = data['title'].encode('utf8')
- query = urlencode([
- ('field.title', title),
- ('field.tags', ' '.join(data['tags'])),
- ])
- url = '%s?%s' % (base, query)
+ data["product"], view_name="+filebug", rootsite="bugs"
+ )
+ title = data["title"].encode("utf8")
+ query = urlencode(
+ [
+ ("field.title", title),
+ ("field.tags", " ".join(data["tags"])),
+ ]
+ )
+ url = "%s?%s" % (base, query)
self.request.response.redirect(url)
@@ -1131,8 +1228,7 @@ class BugTargetBugListingView(LaunchpadView):
elif IProduct(self.context, None):
milestone_resultset = self.context.milestones
else:
- raise AssertionError(
- "milestones_list called with illegal context")
+ raise AssertionError("milestones_list called with illegal context")
return list(milestone_resultset)
@property
@@ -1146,6 +1242,7 @@ class BugTargetBugListingView(LaunchpadView):
"""
# Circular fail.
from lp.bugs.model.bugsummary import BugSummary
+
series_buglistings = []
bug_task_set = getUtility(IBugTaskSet)
series_list = self.series_list
@@ -1171,7 +1268,8 @@ class BugTargetBugListingView(LaunchpadView):
title=series.name,
url=canonical_url(series) + "/+bugs",
count=series_bug_count,
- ))
+ )
+ )
return series_buglistings
@property
@@ -1181,7 +1279,8 @@ class BugTargetBugListingView(LaunchpadView):
from lp.bugs.model.bugsummary import (
BugSummary,
CombineBugSummaryConstraint,
- )
+ )
+
milestone_buglistings = []
bug_task_set = getUtility(IBugTaskSet)
milestones = self.milestones_list
@@ -1190,10 +1289,12 @@ class BugTargetBugListingView(LaunchpadView):
# Note: this isn't totally optimal as a query, but its the simplest to
# code; we can iterate if needed to provide one complex context to
# countBugs.
- query_milestones = map(partial(
- CombineBugSummaryConstraint, self.context), milestones)
+ query_milestones = map(
+ partial(CombineBugSummaryConstraint, self.context), milestones
+ )
counts = bug_task_set.countBugs(
- self.user, query_milestones, (BugSummary.milestone_id,))
+ self.user, query_milestones, (BugSummary.milestone_id,)
+ )
for milestone in milestones:
milestone_bug_count = counts.get((milestone.id,), 0)
if milestone_bug_count > 0:
@@ -1202,7 +1303,8 @@ class BugTargetBugListingView(LaunchpadView):
title=milestone.name,
url=canonical_url(milestone),
count=milestone_bug_count,
- ))
+ )
+ )
return milestone_buglistings
@@ -1219,22 +1321,27 @@ class BugTargetBugTagsView(LaunchpadView):
"""The data for rendering a tags cloud"""
official_tags = self.context.official_bug_tags
tags = self.context.getUsedBugTagsWithOpenCounts(
- self.user, 10, official_tags)
+ self.user, 10, official_tags
+ )
return sorted(
- (dict(
- tag=tag,
- count=count,
- url=self._getSearchURL(tag),
+ (
+ dict(
+ tag=tag,
+ count=count,
+ url=self._getSearchURL(tag),
)
- for (tag, count) in tags.items()),
- key=lambda item: (-item['count'], item['tag']))
+ for (tag, count) in tags.items()
+ ),
+ key=lambda item: (-item["count"], item["tag"]),
+ )
@property
def show_manage_tags_link(self):
"""Should a link to a "manage official tags" page be shown?"""
- return (IOfficialBugTagTargetRestricted.providedBy(self.context) and
- check_permission('launchpad.BugSupervisor', self.context))
+ return IOfficialBugTagTargetRestricted.providedBy(
+ self.context
+ ) and check_permission("launchpad.BugSupervisor", self.context)
class OfficialBugTagsManageView(LaunchpadEditFormView):
@@ -1243,12 +1350,12 @@ class OfficialBugTagsManageView(LaunchpadEditFormView):
schema = IOfficialBugTagTargetPublic
custom_widget_official_bug_tags = LargeBugTagsWidget
- label = 'Manage official bug tags'
+ label = "Manage official bug tags"
- @action('Save', name='save')
+ @action("Save", name="save")
def save_action(self, action, data):
"""Action for saving new official bug tags."""
- self.context.official_bug_tags = data['official_bug_tags']
+ self.context.official_bug_tags = data["official_bug_tags"]
self.next_url = canonical_url(self.context)
@property
@@ -1257,7 +1364,8 @@ class OfficialBugTagsManageView(LaunchpadEditFormView):
# The model returns dict and list respectively but dumps blows up on
# security proxied objects.
used_tags = removeSecurityProxy(
- self.context.getUsedBugTagsWithOpenCounts(self.user))
+ self.context.getUsedBugTagsWithOpenCounts(self.user)
+ )
official_tags = removeSecurityProxy(self.context.official_bug_tags)
return """<script type="text/javascript">
var used_bug_tags = %s;
@@ -1265,9 +1373,10 @@ class OfficialBugTagsManageView(LaunchpadEditFormView):
var valid_name_pattern = %s;
</script>
""" % (
- dumps(used_tags),
- dumps(official_tags),
- dumps(valid_name_pattern.pattern))
+ dumps(used_tags),
+ dumps(official_tags),
+ dumps(valid_name_pattern.pattern),
+ )
@property
def cancel_url(self):
@@ -1278,7 +1387,7 @@ class OfficialBugTagsManageView(LaunchpadEditFormView):
class BugsPatchesView(LaunchpadView):
"""View list of patch attachments associated with bugs."""
- page_title = 'Patch attachments'
+ page_title = "Patch attachments"
@property
def label(self):
@@ -1286,7 +1395,7 @@ class BugsPatchesView(LaunchpadView):
if IHeadingContext.providedBy(self.context):
return self.page_title
else:
- return 'Patch attachments in %s' % self.context.displayname
+ return "Patch attachments in %s" % self.context.displayname
@property
def patch_task_orderings(self):
@@ -1299,11 +1408,13 @@ class BugsPatchesView(LaunchpadView):
("Importance", "-importance"),
...]
"""
- orderings = [("patch age", "-latest_patch_uploaded"),
- ("importance", "-importance"),
- ("status", "status"),
- ("oldest first", "datecreated"),
- ("newest first", "-datecreated")]
+ orderings = [
+ ("patch age", "-latest_patch_uploaded"),
+ ("importance", "-importance"),
+ ("status", "status"),
+ ("oldest first", "datecreated"),
+ ("newest first", "-datecreated"),
+ ]
targetname = self.targetName()
if targetname is not None:
# Lower case for consistency with the other orderings.
@@ -1315,13 +1426,19 @@ class BugsPatchesView(LaunchpadView):
orderby = self.request.get("orderby", "-latest_patch_uploaded")
if orderby not in [x[1] for x in self.patch_task_orderings]:
raise UnexpectedFormData(
- "Unexpected value for field 'orderby': '%s'" % orderby)
+ "Unexpected value for field 'orderby': '%s'" % orderby
+ )
return BatchNavigator(
self.context.searchTasks(
- None, user=self.user, order_by=orderby,
+ None,
+ user=self.user,
+ order_by=orderby,
status=UNRESOLVED_BUGTASK_STATUSES,
- omit_duplicates=True, has_patch=True),
- self.request)
+ omit_duplicates=True,
+ has_patch=True,
+ ),
+ self.request,
+ )
def targetName(self):
"""Return the name of the current context's target type, or None.
@@ -1331,11 +1448,13 @@ class BugsPatchesView(LaunchpadView):
name in a web page, for example. If no target type is
appropriate for the current context, then return None.
"""
- if (IDistribution.providedBy(self.context) or
- IDistroSeries.providedBy(self.context)):
+ if IDistribution.providedBy(self.context) or IDistroSeries.providedBy(
+ self.context
+ ):
return "Package"
- elif (IProjectGroup.providedBy(self.context) or
- IPerson.providedBy(self.context)):
+ elif IProjectGroup.providedBy(self.context) or IPerson.providedBy(
+ self.context
+ ):
# In the case of an IPerson, the target column can vary
# row-by-row, showing both packages and products. We
# decided to go with the table header "Project" for both,
@@ -1350,7 +1469,7 @@ class BugsPatchesView(LaunchpadView):
def patchAge(self, patch):
"""Return a timedelta object for the age of a patch attachment."""
- now = datetime.now(timezone('UTC'))
+ now = datetime.now(timezone("UTC"))
return now - patch.message.datecreated
def proxiedUrlForLibraryFile(self, patch):
@@ -1361,7 +1480,7 @@ class BugsPatchesView(LaunchpadView):
class TargetSubscriptionView(LaunchpadView):
"""A view to show all a person's structural subscriptions to a target."""
- page_title = 'Your subscriptions'
+ page_title = "Your subscriptions"
@property
def label(self):
@@ -1373,9 +1492,9 @@ class TargetSubscriptionView(LaunchpadView):
def initialize(self):
super().initialize()
expose_structural_subscription_data_to_js(
- self.context, self.request, self.user, self.subscriptions)
+ self.context, self.request, self.user, self.subscriptions
+ )
@property
def subscriptions(self):
- return get_structural_subscriptions_for_target(
- self.context, self.user)
+ return get_structural_subscriptions_for_target(self.context, self.user)
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index 7228fc7..af99675 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -4,37 +4,35 @@
"""IBugTask-related browser views."""
__all__ = [
- 'bugtarget_renderer',
- 'BugTargetTraversalMixin',
- 'BugTaskBreadcrumb',
- 'BugTaskContextMenu',
- 'BugTaskCreateQuestionView',
- 'BugTaskDeletionView',
- 'BugTaskEditView',
- 'BugTaskNavigation',
- 'BugTaskPrivacyAdapter',
- 'BugTaskRemoveQuestionView',
- 'BugTasksNominationsView',
- 'BugTasksTableView',
- 'BugTaskTableRowView',
- 'BugTaskTextView',
- 'BugTaskView',
- 'can_add_package_task_to_bug',
- 'can_add_project_task_to_bug',
- 'get_comments_for_bugtask',
- 'get_visible_comments',
- ]
+ "bugtarget_renderer",
+ "BugTargetTraversalMixin",
+ "BugTaskBreadcrumb",
+ "BugTaskContextMenu",
+ "BugTaskCreateQuestionView",
+ "BugTaskDeletionView",
+ "BugTaskEditView",
+ "BugTaskNavigation",
+ "BugTaskPrivacyAdapter",
+ "BugTaskRemoveQuestionView",
+ "BugTasksNominationsView",
+ "BugTasksTableView",
+ "BugTaskTableRowView",
+ "BugTaskTextView",
+ "BugTaskView",
+ "can_add_package_task_to_bug",
+ "can_add_project_task_to_bug",
+ "get_comments_for_bugtask",
+ "get_visible_comments",
+]
+import re
from collections import defaultdict
-from datetime import (
- datetime,
- timedelta,
- )
+from datetime import datetime, timedelta
from itertools import groupby
from operator import attrgetter
-import re
from urllib.parse import quote
+import transaction
from lazr.delegates import delegate_to
from lazr.lifecycle.event import ObjectModifiedEvent
from lazr.lifecycle.snapshot import Snapshot
@@ -44,31 +42,19 @@ from lazr.restful.interfaces import (
IJSONRequestCache,
IReference,
IWebServiceClientRequest,
- )
+)
from lazr.restful.utils import smartquote
from pytz import utc
from simplejson import dumps
-import transaction
from zope import formlib
from zope.browserpage import ViewPageTemplateFile
-from zope.component import (
- adapter,
- getAdapter,
- getMultiAdapter,
- getUtility,
- )
+from zope.component import adapter, getAdapter, getMultiAdapter, getUtility
from zope.event import notify
from zope.formlib.widget import CustomWidgetFactory
-from zope.interface import (
- implementer,
- providedBy,
- )
+from zope.interface import implementer, providedBy
from zope.interface.interfaces import ComponentLookupError
from zope.schema import Choice
-from zope.schema.vocabulary import (
- getVocabularyRegistry,
- SimpleVocabulary,
- )
+from zope.schema.vocabulary import SimpleVocabulary, getVocabularyRegistry
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
@@ -76,16 +62,16 @@ from zope.traversing.interfaces import IPathAdapter
from lp import _
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
ReturnToReferrerMixin,
- )
+ action,
+)
from lp.app.browser.lazrjs import (
TextAreaEditorWidget,
TextLineEditorWidget,
vocabulary_to_choice_edit_items,
- )
+)
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.browser.tales import ObjectImageDisplayAPI
from lp.app.browser.vocabulary import vocabulary_filters
@@ -93,15 +79,11 @@ from lp.app.enums import PROPRIETARY_INFORMATION_TYPES
from lp.app.errors import NotFoundError
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.archivepublisher.debversion import Version
-from lp.bugs.browser.bug import (
- BugContextMenu,
- BugTextView,
- BugViewMixin,
- )
+from lp.bugs.browser.bug import BugContextMenu, BugTextView, BugViewMixin
from lp.bugs.browser.bugcomment import (
build_comments_from_chunks,
group_comments_with_activity,
- )
+)
from lp.bugs.browser.widgets.bugtask import (
AssigneeDisplayWidget,
BugTaskAssigneeWidget,
@@ -109,21 +91,18 @@ from lp.bugs.browser.widgets.bugtask import (
BugTaskSourcePackageNameWidget,
BugTaskTargetWidget,
DBItemDisplayWidget,
- )
+)
from lp.bugs.enums import BugLockStatus
-from lp.bugs.interfaces.bug import (
- IBug,
- IBugSet,
- )
+from lp.bugs.interfaces.bug import IBug, IBugSet
from lp.bugs.interfaces.bugactivity import IBugActivity
from lp.bugs.interfaces.bugattachment import (
BugAttachmentType,
IBugAttachmentSet,
- )
+)
from lp.bugs.interfaces.bugnomination import (
BugNominationStatus,
IBugNominationSet,
- )
+)
from lp.bugs.interfaces.bugtarget import ISeriesBugTarget
from lp.bugs.interfaces.bugtask import (
BugTaskImportance,
@@ -135,24 +114,18 @@ from lp.bugs.interfaces.bugtask import (
IllegalTarget,
IRemoveQuestionFromBugTaskForm,
UserCannotEditBugTaskStatus,
- )
+)
from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
from lp.bugs.interfaces.bugtracker import BugTrackerType
from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
from lp.bugs.interfaces.cve import ICveSet
from lp.bugs.vocabularies import BugTaskMilestoneVocabulary
from lp.code.interfaces.branchcollection import IAllBranches
-from lp.registry.interfaces.distribution import (
- IDistribution,
- IDistributionSet,
- )
+from lp.registry.interfaces.distribution import IDistribution, IDistributionSet
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
- )
-from lp.registry.interfaces.distroseries import (
- IDistroSeries,
- IDistroSeriesSet,
- )
+)
+from lp.registry.interfaces.distroseries import IDistroSeries, IDistroSeriesSet
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProduct
@@ -165,29 +138,22 @@ from lp.services.feeds.browser import FeedsMixin
from lp.services.fields import PersonChoice
from lp.services.mail.notification import get_unified_diff
from lp.services.privacy.interfaces import IObjectPrivacy
-from lp.services.propertycache import (
- cachedproperty,
- get_property_cache,
- )
+from lp.services.propertycache import cachedproperty, get_property_cache
from lp.services.webapp import (
- canonical_url,
LaunchpadView,
Navigation,
+ canonical_url,
redirection,
stepthrough,
- )
+)
from lp.services.webapp.authorization import (
check_permission,
precache_permission_for_objects,
- )
+)
from lp.services.webapp.breadcrumb import Breadcrumb
-from lp.services.webapp.escaping import (
- html_escape,
- structured,
- )
+from lp.services.webapp.escaping import html_escape, structured
from lp.services.webapp.interfaces import ILaunchBag
-
vocabulary_registry = getVocabularyRegistry()
@@ -203,8 +169,10 @@ def bugtarget_renderer(context, field, request):
</span>""",
href=canonical_url(context.target),
css_class=ObjectImageDisplayAPI(context.target).sprite_css(),
- displayname=context.bugtargetdisplayname).escapedtext
+ displayname=context.bugtargetdisplayname,
+ ).escapedtext
return html
+
return render
@@ -215,13 +183,19 @@ def unique_title(title):
if title is None:
return None
title = title.lower()
- if title.startswith('re:'):
+ if title.startswith("re:"):
title = title[3:]
return title.strip()
-def get_comments_for_bugtask(bugtask, truncate=False, for_display=False,
- slice_info=None, show_spam_controls=False, user=None):
+def get_comments_for_bugtask(
+ bugtask,
+ truncate=False,
+ for_display=False,
+ slice_info=None,
+ show_spam_controls=False,
+ user=None,
+):
"""Return BugComments related to a bugtask.
This code builds a sorted list of BugComments in one shot,
@@ -233,9 +207,14 @@ def get_comments_for_bugtask(bugtask, truncate=False, for_display=False,
:param slice_info: If not None, defines a list of slices of the comments
to retrieve.
"""
- comments = build_comments_from_chunks(bugtask, truncate=truncate,
- slice_info=slice_info, show_spam_controls=show_spam_controls,
- user=user, hide_first=for_display)
+ comments = build_comments_from_chunks(
+ bugtask,
+ truncate=truncate,
+ slice_info=slice_info,
+ show_spam_controls=show_spam_controls,
+ user=user,
+ hide_first=for_display,
+ )
# TODO: further fat can be shaved off here by limiting the attachments we
# query to those that slice_info would include.
for comment in comments.values():
@@ -253,10 +232,10 @@ def get_comments_for_bugtask(bugtask, truncate=False, for_display=False,
comments = sorted(comments.values(), key=attrgetter("index"))
current_title = bugtask.bug.title
for comment in comments:
- if not ((unique_title(comment.title) ==
- unique_title(current_title)) or
- (unique_title(comment.title) ==
- unique_title(bugtask.bug.title))):
+ if not (
+ (unique_title(comment.title) == unique_title(current_title))
+ or (unique_title(comment.title) == unique_title(bugtask.bug.title))
+ ):
# this comment has a new title, so make that the rolling focus
current_title = comment.title
comment.display_title = True
@@ -273,9 +252,11 @@ def get_visible_comments(comments, user=None):
# double-submissions or user errors, and which don't add
# anything useful to the bug itself.
# Also omit comments with no body text or attachments to display.
- if (comment.isEmpty() or
- previous_comment and
- previous_comment.isIdenticalTo(comment)):
+ if (
+ comment.isEmpty()
+ or previous_comment
+ and previous_comment.isIdenticalTo(comment)
+ ):
continue
visible_comments.append(comment)
@@ -298,8 +279,9 @@ def get_visible_comments(comments, user=None):
role = PersonRoles(user)
strip_invisible = not (role.in_admin or role.in_registry_experts)
if strip_invisible:
- visible_comments = [c for c in visible_comments
- if c.visible or c.owner == user]
+ visible_comments = [
+ c for c in visible_comments if c.visible or c.owner == user
+ ]
return visible_comments
@@ -307,7 +289,7 @@ def get_visible_comments(comments, user=None):
class BugTargetTraversalMixin:
"""Mix-in in class that provides .../+bug/NNN traversal."""
- @stepthrough('+bug')
+ @stepthrough("+bug")
def traverse_bug(self, name):
"""Traverses +bug portions of URLs."""
return self._get_task_for_context(name)
@@ -328,7 +310,7 @@ class BugTargetTraversalMixin:
# Get out now if the user cannot view the bug. Continuing may
# reveal information about its context
- if not check_permission('launchpad.View', bug):
+ if not check_permission("launchpad.View", bug):
return None
# Loop through this bug's tasks to try and find the appropriate task
@@ -351,29 +333,31 @@ class BugTargetTraversalMixin:
if len(traversal_stack) > 0:
raise NotFoundError
return self.redirectSubTree(
- canonical_url(bug.default_bugtask, request=self.request))
+ canonical_url(bug.default_bugtask, request=self.request)
+ )
- @redirection('+bug')
+ @redirection("+bug")
def redirect_bug(self):
"""If +bug traversal fails, redirect to +bugs."""
- return '+bugs'
+ return "+bugs"
class BugTaskNavigation(Navigation):
"""Navigation for the `IBugTask`."""
+
usedfor = IBugTask
- @stepthrough('attachments')
+ @stepthrough("attachments")
def traverse_attachments(self, name):
"""traverse to an attachment by id."""
if name.isdigit():
attachment = getUtility(IBugAttachmentSet)[name]
if attachment is not None and attachment.bug == self.context.bug:
return self.redirectSubTree(
- canonical_url(attachment, request=self.request),
- status=301)
+ canonical_url(attachment, request=self.request), status=301
+ )
- @stepthrough('+attachment')
+ @stepthrough("+attachment")
def traverse_attachment(self, name):
"""traverse to an attachment by id."""
if name.isdigit():
@@ -381,7 +365,7 @@ class BugTaskNavigation(Navigation):
if attachment is not None and attachment.bug == self.context.bug:
return attachment
- @stepthrough('comments')
+ @stepthrough("comments")
def traverse_comments(self, name):
"""Traverse to a comment by index."""
if not name.isdigit():
@@ -389,34 +373,40 @@ class BugTaskNavigation(Navigation):
index = int(name)
# Ask the DB to slice out just the comment that we need.
comments = get_comments_for_bugtask(
- self.context, slice_info=[slice(index, index + 1)])
+ self.context, slice_info=[slice(index, index + 1)]
+ )
# XXX cjwatson 2015-09-15: Unify with
# Bug.userCanSetCommentVisibility, which also allows
# project-privileged users.
user = getUtility(ILaunchBag).user
roles = PersonRoles(user) if user else None
- if (comments and (
- comments[0].visible
- or user and (
- comments[0].owner == user
- or roles.in_admin or roles.in_registry_experts))):
+ if comments and (
+ comments[0].visible
+ or user
+ and (
+ comments[0].owner == user
+ or roles.in_admin
+ or roles.in_registry_experts
+ )
+ ):
return comments[0]
return None
- @stepthrough('nominations')
+ @stepthrough("nominations")
def traverse_nominations(self, nomination_id):
"""Traverse to a nomination by id."""
if not nomination_id.isdigit():
return None
return getUtility(IBugNominationSet).get(int(nomination_id))
- @redirection('references')
+ @redirection("references")
def redirect_references(self):
- return '..'
+ return ".."
class BugTaskContextMenu(BugContextMenu):
"""Context menu of actions that can be performed upon an `IBugTask`."""
+
usedfor = IBugTask
@@ -443,8 +433,11 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
self.context = getUtility(ILaunchBag).bugtask
else:
self.context = context
- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- [self.context.bug.ownerID], need_validity=True))
+ list(
+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ [self.context.bug.ownerID], need_validity=True
+ )
+ )
@property
def page_title(self):
@@ -452,8 +445,10 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
@property
def label(self):
- heading = 'Bug #%s in %s' % (
- self.context.bug.id, self.context.bugtargetdisplayname)
+ heading = "Bug #%s in %s" % (
+ self.context.bug.id,
+ self.context.bugtargetdisplayname,
+ )
title = FormattersAPI(self.context.bug.title).obfuscate_email()
return smartquote('%s: "%s"') % (heading, title)
@@ -464,7 +459,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
@property
def next_url(self):
"""Provided so returning to the page they came from works."""
- referer = self.request.getHeader('referer')
+ referer = self.request.getHeader("referer")
if referer:
next_url = referer
else:
@@ -474,7 +469,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
@property
def cancel_url(self):
"""Provided so returning to the page they came from works."""
- referer = self.request.getHeader('referer')
+ referer = self.request.getHeader("referer")
if referer:
cancel_url = referer
else:
@@ -485,10 +480,10 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
def is_duplicate_active(self):
active = True
if self.context.bug.duplicateof is not None:
- naked_duplicate = removeSecurityProxy(
- self.context.bug.duplicateof)
+ naked_duplicate = removeSecurityProxy(self.context.bug.duplicateof)
active = getattr(
- naked_duplicate.default_bugtask.target, 'active', True)
+ naked_duplicate.default_bugtask.target, "active", True
+ )
return active
@cachedproperty
@@ -497,7 +492,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
@cachedproperty
def recommended_canonical_url(self):
- return canonical_url(self.context.bug, rootsite='bugs')
+ return canonical_url(self.context.bug, rootsite="bugs")
@property
def information_type(self):
@@ -507,26 +502,34 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
"""Set up the needed widgets."""
bug = self.context.bug
cache = IJSONRequestCache(self.request)
- cache.objects['bug'] = bug
+ cache.objects["bug"] = bug
subscribers_url_data = {
- 'web_link': canonical_url(bug, rootsite='bugs'),
- 'self_link': absoluteURL(bug, self.api_request),
- }
- cache.objects['subscribers_portlet_url_data'] = subscribers_url_data
- cache.objects['total_comments_and_activity'] = (
- self.total_comments + self.total_activity)
- cache.objects['initial_comment_batch_offset'] = (
- self.visible_initial_comments + 1)
- cache.objects['first visible_recent_comment'] = (
- self.total_comments - self.visible_recent_comments)
+ "web_link": canonical_url(bug, rootsite="bugs"),
+ "self_link": absoluteURL(bug, self.api_request),
+ }
+ cache.objects["subscribers_portlet_url_data"] = subscribers_url_data
+ cache.objects["total_comments_and_activity"] = (
+ self.total_comments + self.total_activity
+ )
+ cache.objects["initial_comment_batch_offset"] = (
+ self.visible_initial_comments + 1
+ )
+ cache.objects["first visible_recent_comment"] = (
+ self.total_comments - self.visible_recent_comments
+ )
# See render() for how this flag is used.
self._redirecting_to_bug_list = False
self.bug_title_edit_widget = TextLineEditorWidget(
- bug, IBug['title'], "Edit this summary", 'h1',
- edit_url=canonical_url(self.context, view_name='+edit'),
- max_width='95%', truncate_lines=6)
+ bug,
+ IBug["title"],
+ "Edit this summary",
+ "h1",
+ edit_url=canonical_url(self.context, view_name="+edit"),
+ max_width="95%",
+ truncate_lines=6,
+ )
# XXX 2010-10-05 gmb bug=655597:
# This line of code keeps the view's query count down,
@@ -539,9 +542,9 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
def userIsSubscribed(self):
"""Is the user subscribed to this bug?"""
- return (
- self.context.bug.isSubscribed(self.user) or
- self.context.bug.isSubscribedToDupes(self.user))
+ return self.context.bug.isSubscribed(
+ self.user
+ ) or self.context.bug.isSubscribedToDupes(self.user)
def render(self):
"""Render the bug list if the user has permission to see the bug."""
@@ -549,7 +552,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
# after unsubscribing from a private bug, because rendering the
# bug page would raise Unauthorized errors!
if self._redirecting_to_bug_list:
- return ''
+ return ""
else:
return LaunchpadView.render(self)
@@ -557,8 +560,9 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
"""Nominate the bug for the series and redirect to the bug page."""
self.context.bug.addNomination(self.user, series)
self.request.response.addInfoNotification(
- 'This bug has been nominated to be fixed in %s.' %
- series.bugtargetdisplayname)
+ "This bug has been nominated to be fixed in %s."
+ % series.bugtargetdisplayname
+ )
self.request.response.redirect(canonical_url(self.context))
@cachedproperty
@@ -570,47 +574,65 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
bug = self.context.bug
show_spam_controls = bug.userCanSetCommentVisibility(self.user)
return get_comments_for_bugtask(
- self.context, truncate=True, slice_info=slice_info,
- for_display=True, show_spam_controls=show_spam_controls,
- user=self.user)
+ self.context,
+ truncate=True,
+ slice_info=slice_info,
+ for_display=True,
+ show_spam_controls=show_spam_controls,
+ user=self.user,
+ )
@cachedproperty
def interesting_activity(self):
return self._getInterestingActivity()
- def _getInterestingActivity(self, earliest_activity_date=None,
- latest_activity_date=None):
+ def _getInterestingActivity(
+ self, earliest_activity_date=None, latest_activity_date=None
+ ):
"""A sequence of interesting bug activity."""
- if (earliest_activity_date is not None and
- latest_activity_date is not None):
+ if (
+ earliest_activity_date is not None
+ and latest_activity_date is not None
+ ):
# Only get the activity for the date range that we're
# interested in to save us from processing too much.
activity = self.context.bug.getActivityForDateRange(
start_date=earliest_activity_date,
- end_date=latest_activity_date)
+ end_date=latest_activity_date,
+ )
else:
activity = self.context.bug.activity
bug_change_re = (
- 'affects|description|security vulnerability|information type|'
- 'summary|tags|visibility|bug task deleted|lock status|lock reason')
+ "affects|description|security vulnerability|information type|"
+ "summary|tags|visibility|bug task deleted|lock status|lock reason"
+ )
bugtask_change_re = (
- r'[a-z0-9][a-z0-9\+\.\-]+( \([A-Za-z0-9\s]+\))?: '
- r'(assignee|importance explanation|importance|milestone|'
- r'status explanation|status)')
+ r"[a-z0-9][a-z0-9\+\.\-]+( \([A-Za-z0-9\s]+\))?: "
+ r"(assignee|importance explanation|importance|milestone|"
+ r"status explanation|status)"
+ )
interesting_match = re.compile(
- "^(%s|%s)$" % (bug_change_re, bugtask_change_re)).match
+ "^(%s|%s)$" % (bug_change_re, bugtask_change_re)
+ ).match
activity_items = [
- activity_item for activity_item in activity
- if interesting_match(activity_item.whatchanged) is not None]
+ activity_item
+ for activity_item in activity
+ if interesting_match(activity_item.whatchanged) is not None
+ ]
# Pre-load the doers of the activities in one query.
person_ids = {
- activity_item.person_id for activity_item in activity_items}
- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- person_ids, need_validity=True))
+ activity_item.person_id for activity_item in activity_items
+ }
+ list(
+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ person_ids, need_validity=True
+ )
+ )
interesting_activity = tuple(
- BugActivityItem(activity_item) for activity_item in activity_items)
+ BugActivityItem(activity_item) for activity_item in activity_items
+ )
# This is a bit kludgy but it means that interesting_activity is
# populated correctly for all subsequent calls.
@@ -619,12 +641,16 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
def _getEventGroups(self, batch_size=None, offset=None):
# Ensure truncation results in < max_length comments as expected
- assert(config.malone.comments_list_truncate_oldest_to
- + config.malone.comments_list_truncate_newest_to
- < config.malone.comments_list_max_length)
-
- if (not self.visible_comments_truncated_for_display and
- batch_size is None):
+ assert (
+ config.malone.comments_list_truncate_oldest_to
+ + config.malone.comments_list_truncate_newest_to
+ < config.malone.comments_list_max_length
+ )
+
+ if (
+ not self.visible_comments_truncated_for_display
+ and batch_size is None
+ ):
comments = self.comments
elif batch_size is not None:
# If we're limiting to a given set of comments, we work on
@@ -632,8 +658,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
# on processing time a bit.
if offset is None:
offset = self.visible_initial_comments
- comments = self._getComments([
- slice(offset, offset + batch_size)])
+ comments = self._getComments([slice(offset, offset + batch_size)])
else:
# the comment function takes 0-offset counts where comment 0 is
# the initial description, so we need to add one to the limits
@@ -643,24 +668,23 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
slice_info = [
slice(None, oldest_count),
slice(new_count, None),
- ]
+ ]
comments = self._getComments(slice_info)
- visible_comments = get_visible_comments(
- comments, user=self.user)
+ visible_comments = get_visible_comments(comments, user=self.user)
if len(visible_comments) > 0 and batch_size is not None:
first_comment = visible_comments[0]
last_comment = visible_comments[-1]
- interesting_activity = (
- self._getInterestingActivity(
- earliest_activity_date=first_comment.datecreated,
- latest_activity_date=last_comment.datecreated))
+ interesting_activity = self._getInterestingActivity(
+ earliest_activity_date=first_comment.datecreated,
+ latest_activity_date=last_comment.datecreated,
+ )
else:
interesting_activity = self.interesting_activity
event_groups = group_comments_with_activity(
- comments=visible_comments,
- activities=interesting_activity)
+ comments=visible_comments, activities=interesting_activity
+ )
return event_groups
@cachedproperty
@@ -694,15 +718,20 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
def activity_sort_key(activity):
target = activity.target
return (
- activity.datechanged, 0 if target is None else 1, target,
- activity.attribute)
+ activity.datechanged,
+ 0 if target is None else 1,
+ target,
+ activity.attribute,
+ )
def group_activities_by_target(activities):
activities = sorted(activities, key=activity_sort_key)
return [
{"target": target, "activity": list(activity)}
for target, activity in groupby(
- activities, attrgetter("target"))]
+ activities, attrgetter("target")
+ )
+ ]
def comment_event_dict(comment):
actors = {activity.person for activity in comment.activity}
@@ -715,7 +744,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
"comment": comment,
"date": min(dates),
"person": actors.pop(),
- }
+ }
def activity_event_dict(activities):
actors = {activity.person for activity in activities}
@@ -725,7 +754,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
"activity": group_activities_by_target(activities),
"date": min(dates),
"person": actors.pop(),
- }
+ }
def event_dict(event_group):
if isinstance(event_group, list):
@@ -758,9 +787,9 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
# not 2 (their difference).
num_hidden = abs(comment.index - prev_comment.index) - 1
separator = {
- 'date': prev_comment.datecreated,
- 'num_hidden': num_hidden,
- }
+ "date": prev_comment.datecreated,
+ "num_hidden": num_hidden,
+ }
events.insert(index, separator)
index += 1
prev_comment = comment
@@ -780,7 +809,7 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
@cachedproperty
def visible_comments_truncated_for_display(self):
"""Whether the visible comment list is truncated for display."""
- show_all = (self.request.form_ng.getOne('comments') == 'all')
+ show_all = self.request.form_ng.getOne("comments") == "all"
if show_all:
return False
max_comments = config.malone.comments_list_max_length
@@ -800,28 +829,39 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
def wasDescriptionModified(self):
"""Return a boolean indicating whether the description was modified"""
- return (self.context.bug._indexed_messages(
- include_content=True, include_parents=False)[0].text_contents !=
- self.context.bug.description)
+ return (
+ self.context.bug._indexed_messages(
+ include_content=True, include_parents=False
+ )[0].text_contents
+ != self.context.bug.description
+ )
@cachedproperty
def linked_branches(self):
"""Filter out the bug_branch links to non-visible private branches."""
linked_branches = list(
self.context.bug.getVisibleLinkedBranches(
- self.user, eager_load=True))
+ self.user, eager_load=True
+ )
+ )
# This is an optimization for when we look at the merge proposals.
if linked_branches:
- list(getUtility(IAllBranches).getMergeProposals(
- for_branches=[link.branch for link in linked_branches],
- eager_load=True))
+ list(
+ getUtility(IAllBranches).getMergeProposals(
+ for_branches=[link.branch for link in linked_branches],
+ eager_load=True,
+ )
+ )
return linked_branches
@cachedproperty
def linked_merge_proposals(self):
"""Filter out the links to non-visible private MPs."""
- return list(self.context.bug.getVisibleLinkedMergeProposals(
- self.user, eager_load=True))
+ return list(
+ self.context.bug.getVisibleLinkedMergeProposals(
+ self.user, eager_load=True
+ )
+ )
@property
def days_to_expiration(self):
@@ -853,11 +893,14 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
# We should always display a positive number to the user,
# whether we're talking about the past or the future.
days_to_expiration = -days_to_expiration
- message = ("This bug report was marked for expiration %i days "
- "ago.")
+ message = (
+ "This bug report was marked for expiration %i days " "ago."
+ )
else:
- message = ("This bug report will be marked for expiration in %i "
- "days if no further activity occurs.")
+ message = (
+ "This bug report will be marked for expiration in %i "
+ "days if no further activity occurs."
+ )
return message % days_to_expiration
@@ -868,9 +911,20 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
links = []
for tag in self.context.bug.tags:
if tag in target_official_tags:
- links.append((tag, '%s?field.tag=%s' % (
- canonical_url(self.context.target, view_name='+bugs',
- force_local_path=True), quote(tag))))
+ links.append(
+ (
+ tag,
+ "%s?field.tag=%s"
+ % (
+ canonical_url(
+ self.context.target,
+ view_name="+bugs",
+ force_local_path=True,
+ ),
+ quote(tag),
+ ),
+ )
+ )
return links
@property
@@ -880,9 +934,20 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
links = []
for tag in self.context.bug.tags:
if tag not in target_official_tags:
- links.append((tag, '%s?field.tag=%s' % (
- canonical_url(self.context.target, view_name='+bugs',
- force_local_path=True), quote(tag))))
+ links.append(
+ (
+ tag,
+ "%s?field.tag=%s"
+ % (
+ canonical_url(
+ self.context.target,
+ view_name="+bugs",
+ force_local_path=True,
+ ),
+ quote(tag),
+ ),
+ )
+ )
return links
@property
@@ -896,29 +961,29 @@ class BugTaskView(LaunchpadView, BugViewMixin, FeedsMixin):
# Unwrap the security proxy. - official_tags is a security proxy
# wrapped list.
available_tags = list(self.context.bug.official_tags)
- return 'var available_official_tags = %s;' % dumps(available_tags)
+ return "var available_official_tags = %s;" % dumps(available_tags)
@property
def user_is_admin(self):
"""Is the user a Launchpad admin?"""
- return check_permission('launchpad.Admin', self.context)
+ return check_permission("launchpad.Admin", self.context)
@property
def bug_description_html(self):
"""The bug's description as HTML."""
bug = self.context.bug
- description = IBug['description']
+ description = IBug["description"]
title = "Bug Description"
- edit_url = canonical_url(self.context, view_name='+edit')
- return TextAreaEditorWidget(
- bug, description, title, edit_url=edit_url)
+ edit_url = canonical_url(self.context, view_name="+edit")
+ return TextAreaEditorWidget(bug, description, title, edit_url=edit_url)
@property
def bug_heat_html(self):
"""HTML representation of the bug heat."""
return (
'<span><a href="/+help-bugs/bug-heat.html" target="help" '
- 'class="sprite flame">%d</a></span>' % self.context.bug.heat)
+ 'class="sprite flame">%d</a></span>' % self.context.bug.heat
+ )
class BugTaskBatchedCommentsAndActivityView(BugTaskView):
@@ -930,7 +995,7 @@ class BugTaskBatchedCommentsAndActivityView(BugTaskView):
@property
def offset(self):
try:
- return int(self.request.form_ng.getOne('offset'))
+ return int(self.request.form_ng.getOne("offset"))
except TypeError:
# We return visible_initial_comments + 1, since otherwise we'd
# end up repeating comments that are already visible on the
@@ -942,15 +1007,17 @@ class BugTaskBatchedCommentsAndActivityView(BugTaskView):
@property
def batch_size(self):
try:
- return int(self.request.form_ng.getOne('batch_size'))
+ return int(self.request.form_ng.getOne("batch_size"))
except TypeError:
return config.malone.comments_list_default_batch_size
@property
def next_batch_url(self):
return "%s?offset=%s&batch_size=%s" % (
- canonical_url(self.context, view_name='+batched-comments'),
- self.next_offset, self.batch_size)
+ canonical_url(self.context, view_name="+batched-comments"),
+ self.next_offset,
+ self.batch_size,
+ )
@property
def next_offset(self):
@@ -960,8 +1027,10 @@ class BugTaskBatchedCommentsAndActivityView(BugTaskView):
def _event_groups(self):
"""See `BugTaskView`."""
batch_size = self.batch_size
- if (batch_size > (self.total_comments) or
- not self.has_more_comments_and_activity):
+ if (
+ batch_size > (self.total_comments)
+ or not self.has_more_comments_and_activity
+ ):
# If the batch size is big enough to encompass all the
# remaining comments and activity, trim it so that we don't
# re-show things.
@@ -970,18 +1039,19 @@ class BugTaskBatchedCommentsAndActivityView(BugTaskView):
else:
offset_to_remove = self.offset
batch_size = (
- self.total_comments - self.visible_recent_comments -
+ self.total_comments
+ - self.visible_recent_comments
+ -
# This last bit is to make sure that _getEventGroups()
# doesn't accidentally inflate the batch size later on.
- offset_to_remove)
- return self._getEventGroups(
- batch_size=batch_size, offset=self.offset)
+ offset_to_remove
+ )
+ return self._getEventGroups(batch_size=batch_size, offset=self.offset)
@cachedproperty
def has_more_comments_and_activity(self):
"""Return True if there are more camments and activity to load."""
- return (
- self.next_offset < (self.total_comments + self.total_activity))
+ return self.next_offset < (self.total_comments + self.total_activity)
def get_prefix(bugtask):
@@ -1002,15 +1072,15 @@ def get_prefix(bugtask):
if bugtask.sourcepackagename is not None:
parts.append(bugtask.sourcepackagename.name)
- return '_'.join(parts)
+ return "_".join(parts)
def get_assignee_vocabulary_info(context):
"""The vocabulary of bug task assignees the current user can set."""
if context.userCanSetAnyAssignee(getUtility(ILaunchBag).user):
- vocab_name = 'ValidAssignee'
+ vocab_name = "ValidAssignee"
else:
- vocab_name = 'AllUserTeamsParticipation'
+ vocab_name = "AllUserTeamsParticipation"
vocab = vocabulary_registry.get(None, vocab_name)
return vocab_name, vocab
@@ -1030,29 +1100,30 @@ class BugTaskBugWatchMixin:
error_message_mapping = {
BugWatchActivityStatus.BUG_NOT_FOUND: "%(bugtracker)s bug #"
- "%(bug)s appears not to exist. Check that the bug "
- "number is correct.",
+ "%(bug)s appears not to exist. Check that the bug "
+ "number is correct.",
BugWatchActivityStatus.CONNECTION_ERROR: "Launchpad couldn't "
- "connect to %(bugtracker)s.",
+ "connect to %(bugtracker)s.",
BugWatchActivityStatus.INVALID_BUG_ID: "Bug ID %(bug)s isn't "
- "valid on %(bugtracker)s. Check that the bug ID is "
- "correct.",
+ "valid on %(bugtracker)s. Check that the bug ID is "
+ "correct.",
BugWatchActivityStatus.TIMEOUT: "Launchpad's connection to "
- "%(bugtracker)s timed out.",
+ "%(bugtracker)s timed out.",
BugWatchActivityStatus.UNKNOWN: "Launchpad couldn't import bug "
- "#%(bug)s from " "%(bugtracker)s.",
+ "#%(bug)s from "
+ "%(bugtracker)s.",
BugWatchActivityStatus.UNPARSABLE_BUG: "Launchpad couldn't "
- "extract a status from %(bug)s on %(bugtracker)s.",
+ "extract a status from %(bug)s on %(bugtracker)s.",
BugWatchActivityStatus.UNPARSABLE_BUG_TRACKER: "Launchpad "
- "couldn't determine the version of %(bugtrackertype)s "
- "running on %(bugtracker)s.",
+ "couldn't determine the version of %(bugtrackertype)s "
+ "running on %(bugtracker)s.",
BugWatchActivityStatus.UNSUPPORTED_BUG_TRACKER: "Launchpad "
- "doesn't support importing bugs from %(bugtrackertype)s"
- " bug trackers.",
+ "doesn't support importing bugs from %(bugtrackertype)s"
+ " bug trackers.",
BugWatchActivityStatus.PRIVATE_REMOTE_BUG: "The bug is marked as "
- "private on the remote bug tracker. Launchpad cannot import "
- "the status of private remote bugs.",
- }
+ "private on the remote bug tracker. Launchpad cannot import "
+ "the status of private remote bugs.",
+ }
if bug_watch.last_error_type in error_message_mapping:
message = error_message_mapping[bug_watch.last_error_type]
@@ -1060,20 +1131,22 @@ class BugTaskBugWatchMixin:
message = bug_watch.last_error_type.description
error_data = {
- 'bug': bug_watch.remotebug,
- 'bugtracker': bug_watch.bugtracker.title,
- 'bugtrackertype': bug_watch.bugtracker.bugtrackertype.title}
+ "bug": bug_watch.remotebug,
+ "bugtracker": bug_watch.bugtracker.title,
+ "bugtrackertype": bug_watch.bugtracker.bugtrackertype.title,
+ }
return {
- 'message': message % error_data,
- 'help_url': '%s#%s' % (
+ "message": message % error_data,
+ "help_url": "%s#%s"
+ % (
canonical_url(bug_watch, view_name="+error-help"),
- bug_watch.last_error_type.name),
- }
+ bug_watch.last_error_type.name,
+ ),
+ }
class BugTaskPrivilegeMixin:
-
@cachedproperty
def user_has_privileges(self):
"""Is the user privileged? That is, an admin, pillar owner, driver
@@ -1087,18 +1160,20 @@ class BugTaskPrivilegeMixin:
class IBugTaskEditForm(IBugTask):
sourcepackagename = copy_field(
- IBugTask['sourcepackagename'],
- vocabularyName='DistributionSourcePackage')
+ IBugTask["sourcepackagename"],
+ vocabularyName="DistributionSourcePackage",
+ )
-class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
- BugTaskPrivilegeMixin):
+class BugTaskEditView(
+ LaunchpadEditFormView, BugTaskBugWatchMixin, BugTaskPrivilegeMixin
+):
"""The view class used for the task +editstatus page."""
@property
def schema(self):
"""See `LaunchpadFormView`."""
- if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
+ if bool(getFeatureFlag("disclosure.dsp_picker.enabled")):
return IBugTaskEditForm
else:
return IBugTask
@@ -1110,15 +1185,20 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
milestone_source = None
user_is_subscribed = None
- edit_form = ViewPageTemplateFile('../templates/bugtask-edit-form.pt')
+ edit_form = ViewPageTemplateFile("../templates/bugtask-edit-form.pt")
_next_url_override = None
# The field names that we use by default. This list will be mutated
# depending on the current context and the permissions of the user viewing
# the form.
- default_field_names = ['assignee', 'bugwatch', 'importance', 'milestone',
- 'status']
+ default_field_names = [
+ "assignee",
+ "bugwatch",
+ "importance",
+ "milestone",
+ "status",
+ ]
custom_widget_target = BugTaskTargetWidget
custom_widget_sourcepackagename = BugTaskSourcePackageNameWidget
custom_widget_bugwatch = BugTaskBugWatchWidget
@@ -1130,7 +1210,7 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
self.user_is_subscribed = self.context.bug.isSubscribed(self.user)
super().initialize()
- page_title = 'Edit status'
+ page_title = "Edit status"
@property
def show_target_widget(self):
@@ -1142,8 +1222,9 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
# SourcePackage tasks can have only their sourcepackagename changed.
# Conjoinment means we can't rely on editing the
# DistributionSourcePackage task for this :(
- return (IDistroSeries.providedBy(self.context.target) or
- ISourcePackage.providedBy(self.context.target))
+ return IDistroSeries.providedBy(
+ self.context.target
+ ) or ISourcePackage.providedBy(self.context.target)
@cachedproperty
def field_names(self):
@@ -1165,34 +1246,38 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
# Don't edit self.field_names directly, because it's shared by all
# BugTaskEditView instances.
editable_field_names = set(self.default_field_names)
- editable_field_names.discard('bugwatch')
+ editable_field_names.discard("bugwatch")
# XXX: Brad Bollenbach 2006-09-29 bug=63000: Permission checking
# doesn't belong here!
if not self.user_has_privileges:
- if 'milestone' in editable_field_names:
+ if "milestone" in editable_field_names:
editable_field_names.remove("milestone")
- if 'importance' in editable_field_names:
+ if "importance" in editable_field_names:
editable_field_names.remove("importance")
else:
- editable_field_names = {'bugwatch'}
+ editable_field_names = {"bugwatch"}
if self.context.bugwatch is None:
- editable_field_names.update(('status', 'assignee'))
- if ('importance' in self.default_field_names
- and self.user_has_privileges):
- editable_field_names.add('importance')
+ editable_field_names.update(("status", "assignee"))
+ if (
+ "importance" in self.default_field_names
+ and self.user_has_privileges
+ ):
+ editable_field_names.add("importance")
else:
bugtracker = self.context.bugwatch.bugtracker
if bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS:
- editable_field_names.add('status')
- if ('importance' in self.default_field_names
- and self.user_has_privileges):
- editable_field_names.add('importance')
+ editable_field_names.add("status")
+ if (
+ "importance" in self.default_field_names
+ and self.user_has_privileges
+ ):
+ editable_field_names.add("importance")
if self.show_target_widget:
- editable_field_names.add('target')
+ editable_field_names.add("target")
elif self.show_sourcepackagename_widget:
- editable_field_names.add('sourcepackagename')
+ editable_field_names.add("sourcepackagename")
# To help with caching, return an immutable object.
return frozenset(editable_field_names)
@@ -1241,24 +1326,29 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
super().setUpFields()
read_only_field_names = self._getReadOnlyFieldNames()
- if 'target' in self.editable_field_names:
- self.form_fields = self.form_fields.omit('target')
- target_field = copy_field(IBugTask['target'], readonly=False)
+ if "target" in self.editable_field_names:
+ self.form_fields = self.form_fields.omit("target")
+ target_field = copy_field(IBugTask["target"], readonly=False)
self.form_fields += formlib.form.Fields(target_field)
# The status field is a special case because we alter the vocabulary
# it uses based on the permissions of the user viewing form.
- if 'status' in self.editable_field_names:
+ if "status" in self.editable_field_names:
if self.user is None:
status_noshow = set(BugTaskStatus.items)
else:
status_noshow = {
- BugTaskStatus.UNKNOWN, BugTaskStatus.EXPIRED,
- BugTaskStatus.DOESNOTEXIST}
+ BugTaskStatus.UNKNOWN,
+ BugTaskStatus.EXPIRED,
+ BugTaskStatus.DOESNOTEXIST,
+ }
status_noshow.update(
- status for status in BugTaskStatus.items
+ status
+ for status in BugTaskStatus.items
if not self.context.canTransitionToStatus(
- status, self.user))
+ status, self.user
+ )
+ )
if self.context.status in status_noshow:
# The user has to be able to see the current value.
@@ -1270,13 +1360,17 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
# by EnumeratedType have their name as the token and here we need
# the title as the token for backwards compatibility.
status_items = [
- (item.title, item) for item in BugTaskStatus.items
- if item not in status_noshow]
+ (item.title, item)
+ for item in BugTaskStatus.items
+ if item not in status_noshow
+ ]
status_field = Choice(
- __name__='status', title=self.schema['status'].title,
- vocabulary=SimpleVocabulary.fromItems(status_items))
+ __name__="status",
+ title=self.schema["status"].title,
+ vocabulary=SimpleVocabulary.fromItems(status_items),
+ )
- self.form_fields = self.form_fields.omit('status')
+ self.form_fields = self.form_fields.omit("status")
self.form_fields += formlib.form.Fields(status_field)
# If we have a milestone vocabulary already, create a new field
@@ -1284,14 +1378,15 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
if self.milestone_source is not None:
milestone_source = self.milestone_source
milestone_field = Choice(
- __name__='milestone',
- title=self.schema['milestone'].title,
- source=milestone_source, required=False)
+ __name__="milestone",
+ title=self.schema["milestone"].title,
+ source=milestone_source,
+ required=False,
+ )
else:
- milestone_field = copy_field(
- IBugTask['milestone'], readonly=False)
+ milestone_field = copy_field(IBugTask["milestone"], readonly=False)
- self.form_fields = self.form_fields.omit('milestone')
+ self.form_fields = self.form_fields.omit("milestone")
self.form_fields += formlib.form.Fields(milestone_field)
for field in read_only_field_names:
@@ -1299,42 +1394,58 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
# In cases where the status or importance fields are read only we give
# them a custom widget so that they are rendered correctly.
- for field in ['status', 'importance']:
+ for field in ["status", "importance"]:
if field in read_only_field_names:
self.form_fields[field].custom_widget = CustomWidgetFactory(
- DBItemDisplayWidget)
+ DBItemDisplayWidget
+ )
- if 'importance' not in read_only_field_names:
+ if "importance" not in read_only_field_names:
# Users shouldn't be able to set a bugtask's importance to
# `UNKNOWN`, only bug watches do that.
importance_vocab_items = [
- item for item in BugTaskImportance.items.items
- if item != BugTaskImportance.UNKNOWN]
- self.form_fields = self.form_fields.omit('importance')
+ item
+ for item in BugTaskImportance.items.items
+ if item != BugTaskImportance.UNKNOWN
+ ]
+ self.form_fields = self.form_fields.omit("importance")
self.form_fields += formlib.form.Fields(
- Choice(__name__='importance',
- title=_('Importance'),
- values=importance_vocab_items,
- default=BugTaskImportance.UNDECIDED))
+ Choice(
+ __name__="importance",
+ title=_("Importance"),
+ values=importance_vocab_items,
+ default=BugTaskImportance.UNDECIDED,
+ )
+ )
if self.context.pillar.official_malone:
- self.form_fields = self.form_fields.omit('bugwatch')
+ self.form_fields = self.form_fields.omit("bugwatch")
- elif (self.context.bugwatch is not None and
- self.form_fields.get('assignee', False)):
- self.form_fields['assignee'].custom_widget = CustomWidgetFactory(
- AssigneeDisplayWidget)
+ elif self.context.bugwatch is not None and self.form_fields.get(
+ "assignee", False
+ ):
+ self.form_fields["assignee"].custom_widget = CustomWidgetFactory(
+ AssigneeDisplayWidget
+ )
- if (self.context.bugwatch is None and
- self.form_fields.get('assignee', False)):
+ if self.context.bugwatch is None and self.form_fields.get(
+ "assignee", False
+ ):
# Make the assignee field editable
- self.form_fields = self.form_fields.omit('assignee')
+ self.form_fields = self.form_fields.omit("assignee")
vocabulary, ignored = get_assignee_vocabulary_info(self.context)
- self.form_fields += formlib.form.Fields(PersonChoice(
- __name__='assignee', title=_('Assigned to'), required=False,
- vocabulary=vocabulary, readonly=False))
- self.form_fields['assignee'].custom_widget = CustomWidgetFactory(
- BugTaskAssigneeWidget)
+ self.form_fields += formlib.form.Fields(
+ PersonChoice(
+ __name__="assignee",
+ title=_("Assigned to"),
+ required=False,
+ vocabulary=vocabulary,
+ readonly=False,
+ )
+ )
+ self.form_fields["assignee"].custom_widget = CustomWidgetFactory(
+ BugTaskAssigneeWidget
+ )
def _getReadOnlyFieldNames(self):
"""Return the names of fields that will be rendered read only."""
@@ -1347,33 +1458,36 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
else:
editable_field_names = self.editable_field_names
read_only_field_names = [
- field_name for field_name in self.field_names
- if field_name not in editable_field_names]
+ field_name
+ for field_name in self.field_names
+ if field_name not in editable_field_names
+ ]
return read_only_field_names
def validate(self, data):
- if self.show_sourcepackagename_widget and 'sourcepackagename' in data:
- data['target'] = self.context.distroseries
- spn_or_dsp = data.get('sourcepackagename')
+ if self.show_sourcepackagename_widget and "sourcepackagename" in data:
+ data["target"] = self.context.distroseries
+ spn_or_dsp = data.get("sourcepackagename")
if spn_or_dsp:
if IDistributionSourcePackage.providedBy(spn_or_dsp):
spn = spn_or_dsp.sourcepackagename
else:
spn = spn_or_dsp
- data['target'] = data['target'].getSourcePackage(spn)
- del data['sourcepackagename']
- error_field = 'sourcepackagename'
+ data["target"] = data["target"].getSourcePackage(spn)
+ del data["sourcepackagename"]
+ error_field = "sourcepackagename"
else:
- error_field = 'target'
+ error_field = "target"
- new_target = data.get('target')
+ new_target = data.get("target")
if new_target and new_target != self.context.target:
try:
# The validity of the source package has already been checked
# by the bug target widget.
self.context.validateTransitionToTarget(
- new_target, check_source_package=False)
+ new_target, check_source_package=False
+ )
except IllegalTarget as e:
self.setFieldError(error_field, e.args[0])
@@ -1388,10 +1502,11 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
context = self.context
bugtask = context
- if self.request.form.get('subscribe', False):
+ if self.request.form.get("subscribe", False):
bugtask.bug.subscribe(self.user, self.user)
self.request.response.addNotification(
- "You have subscribed to this bug report.")
+ "You have subscribed to this bug report."
+ )
# Save the field names we extract from the form in a separate
# list, because we modify this list of names later if the
@@ -1401,7 +1516,8 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
data_to_apply = data.copy()
bugtask_before_modification = Snapshot(
- bugtask, providing=providedBy(bugtask))
+ bugtask, providing=providedBy(bugtask)
+ )
# If the user is reassigning an upstream task to a different
# product, we'll clear out the milestone value, to avoid
@@ -1414,15 +1530,17 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
milestone_ignored = False
missing = object()
new_target = new_values.pop("target", missing)
- if (new_target is not missing and
- bugtask.target.pillar != new_target.pillar):
+ if (
+ new_target is not missing
+ and bugtask.target.pillar != new_target.pillar
+ ):
# We clear the milestone value if one was already set. We ignore
# the milestone value if it was currently None, and the user tried
# to set a milestone value while also changing the product. This
# allows us to provide slightly clearer feedback messages.
if bugtask.milestone:
milestone_cleared = bugtask.milestone
- elif new_values.get('milestone') is not None:
+ elif new_values.get("milestone") is not None:
milestone_ignored = True
# Regardless of the user's permission, the milestone
@@ -1432,21 +1550,23 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
# whose changes we want to apply, because we don't want
# the form machinery to try and set this value back to
# what it was!
- data_to_apply.pop('milestone', None)
+ data_to_apply.pop("milestone", None)
# We special case setting target, status and assignee, because
# there's a workflow associated with changes to these fields.
- for manual_field in ('target', 'status', 'assignee'):
+ for manual_field in ("target", "status", "assignee"):
data_to_apply.pop(manual_field, None)
# We grab the comment_on_change field before we update bugtask so as
# to avoid problems accessing the field if the user has changed the
# product of the BugTask.
comment_on_change = self.request.form.get(
- "%s.comment_on_change" % self.prefix)
+ "%s.comment_on_change" % self.prefix
+ )
changed = formlib.form.applyChanges(
- bugtask, self.form_fields, data_to_apply, self.adapters)
+ bugtask, self.form_fields, data_to_apply, self.adapters
+ )
# Set the "changed" flag properly, just in case status and/or assignee
# happen to be the only values that changed. We explicitly verify that
@@ -1464,20 +1584,21 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
if milestone_cleared:
self.request.response.addWarningNotification(
"The %s milestone setting has been removed because "
- "you reassigned the bug to %s." % (
- milestone_cleared.displayname,
- bugtask.bugtargetdisplayname))
+ "you reassigned the bug to %s."
+ % (milestone_cleared.displayname, bugtask.bugtargetdisplayname)
+ )
elif milestone_ignored:
self.request.response.addWarningNotification(
"The milestone setting was ignored because "
- "you reassigned the bug to %s." %
- bugtask.bugtargetdisplayname)
+ "you reassigned the bug to %s." % bugtask.bugtargetdisplayname
+ )
if comment_on_change:
bugtask.bug.newMessage(
owner=getUtility(ILaunchBag).user,
subject=bugtask.bug.followup_subject(),
- content=comment_on_change)
+ content=comment_on_change,
+ )
new_status = new_values.pop("status", missing)
new_assignee = new_values.pop("assignee", missing)
@@ -1490,34 +1611,38 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
# since other changes may have been made.
transaction.abort()
self.setFieldError(
- 'status',
+ "status",
"Only the Bug Supervisor for %s can set the bug's "
- "status to %s" %
- (bugtask.target.displayname, new_status.title))
+ "status to %s"
+ % (bugtask.target.displayname, new_status.title),
+ )
return
if new_assignee is not missing and bugtask.assignee != new_assignee:
if new_assignee is not None and new_assignee != self.user:
is_contributor = new_assignee.isBugContributorInTarget(
- user=self.user, target=bugtask.pillar)
+ user=self.user, target=bugtask.pillar
+ )
if not is_contributor:
# If we have a new assignee who isn't a bug
# contributor in this pillar, we display a warning
# to the user, in case they made a mistake.
self.request.response.addWarningNotification(
structured(
- """<a href="%s">%s</a>
+ """<a href="%s">%s</a>
did not previously have any assigned bugs in
<a href="%s">%s</a>.
<br /><br />
If this bug was assigned by mistake,
you may <a href="%s/+editstatus"
>change the assignment</a>.""",
- canonical_url(new_assignee),
- new_assignee.displayname,
- canonical_url(bugtask.pillar),
- bugtask.pillar.title,
- canonical_url(bugtask)))
+ canonical_url(new_assignee),
+ new_assignee.displayname,
+ canonical_url(bugtask.pillar),
+ bugtask.pillar.title,
+ canonical_url(bugtask),
+ )
+ )
changed = True
bugtask.transitionToAssignee(new_assignee)
@@ -1527,21 +1652,21 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
# Reset the status and importance to the default values,
# since Unknown isn't selectable in the UI.
bugtask.transitionToStatus(
- IBugTask['status'].default, bug_importer)
+ IBugTask["status"].default, bug_importer
+ )
bugtask.transitionToImportance(
- IBugTask['importance'].default, bug_importer)
+ IBugTask["importance"].default, bug_importer
+ )
else:
- #XXX: Bjorn Tillenius 2006-03-01:
+ # XXX: Bjorn Tillenius 2006-03-01:
# Reset the bug task's status information. The right
# thing would be to convert the bug watch's status to a
# Launchpad status, but it's not trivial to do at the
# moment. I will fix this later.
- bugtask.transitionToStatus(
- BugTaskStatus.UNKNOWN,
- bug_importer)
+ bugtask.transitionToStatus(BugTaskStatus.UNKNOWN, bug_importer)
bugtask.transitionToImportance(
- BugTaskImportance.UNKNOWN,
- bug_importer)
+ BugTaskImportance.UNKNOWN, bug_importer
+ )
bugtask.transitionToAssignee(None)
if changed:
@@ -1549,7 +1674,9 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
ObjectModifiedEvent(
object=bugtask,
object_before_modification=bugtask_before_modification,
- edited_fields=field_names))
+ edited_fields=field_names,
+ )
+ )
# We clear the known views cache because the bug may not be
# viewable anymore by the current user. If the bug is not
@@ -1560,23 +1687,25 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
self.request.response.addWarningNotification(
"The bug you have just updated is now a private bug for "
"%s. You do not have permission to view such bugs."
- % bugtask.pillar.displayname)
+ % bugtask.pillar.displayname
+ )
self._next_url_override = canonical_url(
- new_target.pillar, rootsite='bugs')
+ new_target.pillar, rootsite="bugs"
+ )
- if (bugtask.sourcepackagename and (
- self.widgets.get('target') or
- self.widgets.get('sourcepackagename'))):
+ if bugtask.sourcepackagename and (
+ self.widgets.get("target") or self.widgets.get("sourcepackagename")
+ ):
real_package_name = bugtask.sourcepackagename.name
# We get entered_package_name directly from the form here, since
# validating the sourcepackagename field mutates its value in to
# the one already in real_package_name, which makes our comparison
# of the two below useless.
- if self.widgets.get('sourcepackagename'):
- field_name = self.widgets['sourcepackagename'].name
+ if self.widgets.get("sourcepackagename"):
+ field_name = self.widgets["sourcepackagename"].name
else:
- field_name = self.widgets['target'].package_widget.name
+ field_name = self.widgets["target"].package_widget.name
entered_package_name = self.request.form.get(field_name)
if real_package_name != entered_package_name:
@@ -1585,11 +1714,14 @@ class BugTaskEditView(LaunchpadEditFormView, BugTaskBugWatchMixin,
self.request.response.addNotification(
"'%(entered_package)s' is a binary package. This bug has"
" been assigned to its source package '%(real_package)s'"
- " instead." %
- {'entered_package': entered_package_name,
- 'real_package': real_package_name})
+ " instead."
+ % {
+ "entered_package": entered_package_name,
+ "real_package": real_package_name,
+ }
+ )
- @action('Save Changes', name='save')
+ @action("Save Changes", name="save")
def save_action(self, action, data):
"""Update the bugtask with the form data."""
self.updateContextFromData(data)
@@ -1601,7 +1733,7 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
schema = IBugTask
field_names = []
- label = 'Remove bug task'
+ label = "Remove bug task"
page_title = label
@property
@@ -1611,13 +1743,14 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
return self._next_url or self._return_url
return None
- @action('Delete', name='delete_bugtask')
+ @action("Delete", name="delete_bugtask")
def delete_bugtask_action(self, action, data):
bugtask = self.context
bug = bugtask.bug
- deleted_bugtask_url = canonical_url(self.context, rootsite='bugs')
- success_message = ("This bug no longer affects %s."
- % bugtask.bugtargetdisplayname)
+ deleted_bugtask_url = canonical_url(self.context, rootsite="bugs")
+ success_message = (
+ "This bug no longer affects %s." % bugtask.bugtargetdisplayname
+ )
error_message = None
# We set the next_url here before the bugtask is deleted since later
# the bugtask will not be available if required to construct the url.
@@ -1631,8 +1764,9 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
self.request.response.addErrorNotification(error_message)
if self.request.is_ajax:
if error_message:
- self.request.response.setHeader('Content-type',
- 'application/json')
+ self.request.response.setHeader(
+ "Content-type", "application/json"
+ )
return dumps(None)
launchbag = getUtility(ILaunchBag)
launchbag.add(bug.default_bugtask)
@@ -1642,15 +1776,15 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
# We can't do the redirect here since the XHR caller won't see it
# so we return the URL to go to and let the caller do it.
if self._return_url == deleted_bugtask_url:
- next_url = canonical_url(
- bug.default_bugtask, rootsite='bugs')
- self.request.response.setHeader('Content-type',
- 'application/json')
+ next_url = canonical_url(bug.default_bugtask, rootsite="bugs")
+ self.request.response.setHeader(
+ "Content-type", "application/json"
+ )
return dumps(dict(bugtask_url=next_url))
# No redirect required so return the new bugtask table HTML.
view = getMultiAdapter(
- (bug, self.request),
- name='+bugtasks-and-nominations-table')
+ (bug, self.request), name="+bugtasks-and-nominations-table"
+ )
view.initialize()
return view.render()
@@ -1664,23 +1798,42 @@ def bugtask_sort_key(bugtask):
key = (None, bugtask.target.displayname, None, None, None, None)
elif IDistroSeries.providedBy(bugtask.target):
key = (
- None, bugtask.target.distribution.displayname,
- bugtask.target.name, None, None, None)
+ None,
+ bugtask.target.distribution.displayname,
+ bugtask.target.name,
+ None,
+ None,
+ None,
+ )
elif IDistributionSourcePackage.providedBy(bugtask.target):
key = (
bugtask.target.sourcepackagename.name,
- bugtask.target.distribution.displayname, None, None, None, None)
+ bugtask.target.distribution.displayname,
+ None,
+ None,
+ None,
+ None,
+ )
elif ISourcePackage.providedBy(bugtask.target):
key = (
bugtask.target.sourcepackagename.name,
bugtask.target.distribution.displayname,
- Version(bugtask.target.distroseries.version), None, None, None)
+ Version(bugtask.target.distroseries.version),
+ None,
+ None,
+ None,
+ )
elif IProduct.providedBy(bugtask.target):
key = (None, None, None, bugtask.target.displayname, None, None)
elif IProductSeries.providedBy(bugtask.target):
key = (
- None, None, None, bugtask.target.product.displayname,
- bugtask.target.name, None)
+ None,
+ None,
+ None,
+ bugtask.target.product.displayname,
+ bugtask.target.name,
+ None,
+ )
elif IOCIProject.providedBy(bugtask.target):
ociproject = bugtask.target
pillar = ociproject.pillar
@@ -1715,9 +1868,11 @@ class BugTasksNominationsView(LaunchpadView):
def displayAlsoAffectsLinks(self):
"""Return True if the Also Affects links should be displayed."""
return (
- check_permission('launchpad.Edit', self.context) and
+ check_permission("launchpad.Edit", self.context)
+ and
# Hide the links when the bug is viewed in a CVE context.
- self.request.getNearest(ICveSet) == (None, None))
+ self.request.getNearest(ICveSet) == (None, None)
+ )
@cachedproperty
def current_user_affected_status(self):
@@ -1729,17 +1884,16 @@ class BugTasksNominationsView(LaunchpadView):
"""A javascript literal indicating if the user is affected."""
affected = self.current_user_affected_status
if affected is None:
- return 'null'
+ return "null"
elif affected:
- return 'true'
+ return "true"
else:
- return 'false'
+ return "false"
@cachedproperty
def other_users_affected_count(self):
- """The number of other users affected by this bug.
- """
- if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
+ """The number of other users affected by this bug."""
+ if getFeatureFlag("bugs.affected_count_includes_dupes.disabled"):
if self.current_user_affected_status:
return self.context.users_affected_count - 1
else:
@@ -1753,7 +1907,7 @@ class BugTasksNominationsView(LaunchpadView):
Counting across duplicates may be disabled at run time.
"""
- if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
+ if getFeatureFlag("bugs.affected_count_includes_dupes.disabled"):
return self.context.users_affected_count
else:
return self.context.users_affected_count_with_dupes
@@ -1773,7 +1927,8 @@ class BugTasksNominationsView(LaunchpadView):
elif other_affected > 1:
return (
"This bug affects %d people. Does this bug "
- "affect you?" % other_affected)
+ "affect you?" % other_affected
+ )
else:
return "Does this bug affect you?"
elif me_affected is True:
@@ -1783,7 +1938,8 @@ class BugTasksNominationsView(LaunchpadView):
return "This bug affects you and 1 other person"
else:
return "This bug affects you and %d other people" % (
- other_affected)
+ other_affected
+ )
else:
if other_affected == 0:
return "This bug doesn't affect you"
@@ -1791,7 +1947,8 @@ class BugTasksNominationsView(LaunchpadView):
return "This bug affects 1 person, but not you"
elif other_affected > 1:
return "This bug affects %d people, but not you" % (
- other_affected)
+ other_affected
+ )
@cachedproperty
def anon_affected_statement(self):
@@ -1881,10 +2038,14 @@ class BugTasksTableView(LaunchpadView):
# bug, hence they can see any assignees.
# The security adaptor will do the job also but we don't want or need
# the expense of running several complex SQL queries.
- authorised_people = [task.assignee for task in self.bugtasks
- if task.assignee is not None]
+ authorised_people = [
+ task.assignee
+ for task in self.bugtasks
+ if task.assignee is not None
+ ]
precache_permission_for_objects(
- self.request, 'launchpad.LimitedView', authorised_people)
+ self.request, "launchpad.LimitedView", authorised_people
+ )
distro_packages = defaultdict(list)
distro_series_packages = defaultdict(list)
@@ -1892,20 +2053,25 @@ class BugTasksTableView(LaunchpadView):
target = bugtask.target
if IDistributionSourcePackage.providedBy(target):
distro_packages[target.distribution].append(
- target.sourcepackagename)
+ target.sourcepackagename
+ )
if ISourcePackage.providedBy(target):
distro_series_packages[target.distroseries].append(
- target.sourcepackagename)
+ target.sourcepackagename
+ )
distro_set = getUtility(IDistributionSet)
- self.target_releases = dict(distro_set.getCurrentSourceReleases(
- distro_packages))
+ self.target_releases = dict(
+ distro_set.getCurrentSourceReleases(distro_packages)
+ )
distro_series_set = getUtility(IDistroSeriesSet)
self.target_releases.update(
- distro_series_set.getCurrentSourceReleases(
- distro_series_packages))
+ distro_series_set.getCurrentSourceReleases(distro_series_packages)
+ )
ids = set()
- for release_person_ids in map(attrgetter('creatorID', 'maintainerID'),
- self.target_releases.values()):
+ for release_person_ids in map(
+ attrgetter("creatorID", "maintainerID"),
+ self.target_releases.values(),
+ ):
ids.update(release_person_ids)
ids.discard(None)
if ids:
@@ -1919,53 +2085,60 @@ class BugTasksTableView(LaunchpadView):
def milestones(self):
if self.bugtasks:
bugtask_set = getUtility(IBugTaskSet)
- return list(
- bugtask_set.getBugTaskTargetMilestones(self.bugtasks))
+ return list(bugtask_set.getBugTaskTargetMilestones(self.bugtasks))
else:
return []
def getTargetLinkTitle(self, target):
"""Return text to put as the title for the link to the target."""
- if not (IDistributionSourcePackage.providedBy(target) or
- ISourcePackage.providedBy(target)):
+ if not (
+ IDistributionSourcePackage.providedBy(target)
+ or ISourcePackage.providedBy(target)
+ ):
return None
current_release = self.target_releases.get(target)
if current_release is None:
return "No current release for this source package in %s" % (
- target.distribution.displayname)
+ target.distribution.displayname
+ )
uploader = current_release.creator
maintainer = current_release.maintainer
return (
"Latest release: %(version)s, uploaded to %(component)s"
" on %(date_uploaded)s by %(uploader)s,"
- " maintained by %(maintainer)s" % dict(
+ " maintained by %(maintainer)s"
+ % dict(
version=current_release.version,
component=current_release.component.name,
date_uploaded=current_release.dateuploaded,
uploader=uploader.unique_displayname,
maintainer=maintainer.unique_displayname,
- ))
+ )
+ )
- def _getTableRowView(self, context, is_converted_to_question,
- is_conjoined_replica):
+ def _getTableRowView(
+ self, context, is_converted_to_question, is_conjoined_replica
+ ):
"""Get the view for the context, and initialize it.
The view's is_conjoined_replica and is_converted_to_question
attributes are set, as well as the edit view.
"""
view = getMultiAdapter(
- (context, self.request),
- name='+bugtasks-and-nominations-table-row')
+ (context, self.request), name="+bugtasks-and-nominations-table-row"
+ )
view.is_converted_to_question = is_converted_to_question
view.is_conjoined_replica = is_conjoined_replica
view.edit_view = getMultiAdapter(
- (context, self.request), name='+edit-form')
+ (context, self.request), name="+edit-form"
+ )
view.milestone_source = self.caching_milestone_vocabulary
if IBugTask.providedBy(context):
view.target_link_title = self.getTargetLinkTitle(context.target)
- view.edit_view.milestone_source = (
- BugTaskMilestoneVocabulary(context, self.milestones))
+ view.edit_view.milestone_source = BugTaskMilestoneVocabulary(
+ context, self.milestones
+ )
view.edit_view.user_is_subscribed = self.user_is_subscribed
# Hint to optimize when there are many bugtasks.
view.many_bugtasks = self.many_bugtasks
@@ -1995,11 +2168,14 @@ class BugTasksTableView(LaunchpadView):
nominations = list(bug.getNominations())
# Eager load validity for all the persons we know of that will be
# displayed.
- ids = set(map(attrgetter('owner_id'), nominations))
+ ids = set(map(attrgetter("owner_id"), nominations))
ids.discard(None)
if ids:
- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- ids, need_validity=True))
+ list(
+ getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ ids, need_validity=True
+ )
+ )
# Build a cache we can pass on to getConjoinedPrimary(), so that
# it doesn't have to iterate over all the bug tasks in each loop
@@ -2025,31 +2201,40 @@ class BugTasksTableView(LaunchpadView):
bugtask_and_nomination_views.append(
getMultiAdapter(
(parent, self.request),
- name='+bugtasks-and-nominations-table-row'))
+ name="+bugtasks-and-nominations-table-row",
+ )
+ )
conjoined_primary = bugtask.getConjoinedPrimary(
- all_bugtasks, bugtasks_by_package)
+ all_bugtasks, bugtasks_by_package
+ )
view = self._getTableRowView(
- bugtask, is_converted_to_question,
- conjoined_primary is not None)
+ bugtask,
+ is_converted_to_question,
+ conjoined_primary is not None,
+ )
bugtask_and_nomination_views.append(view)
target = bugtask.product or bugtask.distribution
if not target:
continue
target_nominations = bug.getNominations(
- target, nominations=nominations)
+ target, nominations=nominations
+ )
bugtask_and_nomination_views.extend(
self._getTableRowView(
- nomination, is_converted_to_question, False)
+ nomination, is_converted_to_question, False
+ )
for nomination in target_nominations
- if nomination.status != BugNominationStatus.APPROVED)
+ if nomination.status != BugNominationStatus.APPROVED
+ )
return bugtask_and_nomination_views
-class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
- BugTaskPrivilegeMixin):
+class BugTaskTableRowView(
+ LaunchpadView, BugTaskBugWatchMixin, BugTaskPrivilegeMixin
+):
"""Browser class for rendering a bugtask row on the bug page."""
is_conjoined_replica = None
@@ -2058,7 +2243,8 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
many_bugtasks = False
template = ViewPageTemplateFile(
- '../templates/bugtask-tasks-and-nominations-table-row.pt')
+ "../templates/bugtask-tasks-and-nominations-table-row.pt"
+ )
def __init__(self, context, request):
super().__init__(context, request)
@@ -2072,8 +2258,9 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
super().initialize()
link = canonical_url(self.context)
task_link = edit_link = canonical_url(
- self.context, view_name='+editstatus')
- delete_link = canonical_url(self.context, view_name='+delete')
+ self.context, view_name="+editstatus"
+ )
+ delete_link = canonical_url(self.context, view_name="+delete")
bugtask_id = self.context.id
launchbag = getUtility(ILaunchBag)
is_primary = self.context.id == launchbag.bugtask.id
@@ -2082,9 +2269,10 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
# 150+ bugtasks, it can save three or four seconds of rendering
# time.
expandable=(
- not self.many_bugtasks and
- self.user_can_edit and
- self.canSeeTaskDetails()),
+ not self.many_bugtasks
+ and self.user_can_edit
+ and self.canSeeTaskDetails()
+ ),
indent_task=ISeriesBugTarget.providedBy(self.context.target),
is_conjoined_replica=self.is_conjoined_replica,
task_link=task_link,
@@ -2092,28 +2280,28 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
can_edit=self.user_can_edit,
link=link,
id=bugtask_id,
- row_id='tasksummary%d' % bugtask_id,
- form_row_id='task%d' % bugtask_id,
- row_css_class='highlight' if is_primary else None,
+ row_id="tasksummary%d" % bugtask_id,
+ form_row_id="task%d" % bugtask_id,
+ row_css_class="highlight" if is_primary else None,
target_link=canonical_url(self.context.target),
target_link_title=self.target_link_title,
user_can_delete=self.user_can_delete_bugtask,
delete_link=delete_link,
user_can_edit_importance=self.user_has_privileges,
- importance_css_class='importance' + self.context.importance.name,
+ importance_css_class="importance" + self.context.importance.name,
importance_title=self.context.importance.title,
# We always look up all milestones, so there's no harm
# using len on the list here and avoid the COUNT query.
target_has_milestones=len(self._visible_milestones) > 0,
user_can_edit_status=self.user_can_edit_status,
- )
+ )
if not self.many_bugtasks:
cache = IJSONRequestCache(self.request)
- bugtask_data = cache.objects.get('bugtask_data', None)
+ bugtask_data = cache.objects.get("bugtask_data", None)
if bugtask_data is None:
bugtask_data = dict()
- cache.objects['bugtask_data'] = bugtask_data
+ cache.objects["bugtask_data"] = bugtask_data
bugtask_data[bugtask_id] = self.bugtask_config()
def canSeeTaskDetails(self):
@@ -2124,15 +2312,19 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
It is independent of whether they can *change* the status; you
need to expand the details to see any milestone set.
"""
- assert self.is_conjoined_replica is not None, (
- 'is_conjoined_replica should be set before rendering the page.')
+ assert (
+ self.is_conjoined_replica is not None
+ ), "is_conjoined_replica should be set before rendering the page."
assert self.is_converted_to_question is not None, (
- 'is_converted_to_question should be set before rendering the'
- ' page.')
- return (self.displayEditForm() and
- not self.is_conjoined_replica and
- self.context.bug.duplicateof is None and
- not self.is_converted_to_question)
+ "is_converted_to_question should be set before rendering the"
+ " page."
+ )
+ return (
+ self.displayEditForm()
+ and not self.is_conjoined_replica
+ and self.context.bug.duplicateof is None
+ and not self.is_converted_to_question
+ )
def _getSeriesTargetNameHelper(self, bugtask):
"""Return the short name of bugtask's targeted series."""
@@ -2152,14 +2344,16 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
@property
def bugtask_icon(self):
"""Which icon should be shown for the task, if any?"""
- return getAdapter(self.context, IPathAdapter, 'image').sprite_css()
+ return getAdapter(self.context, IPathAdapter, "image").sprite_css()
def displayEditForm(self):
"""Return true if the BugTask edit form should be shown."""
return (
- check_permission('launchpad.Edit', self.context) and
+ check_permission("launchpad.Edit", self.context)
+ and
# Hide the edit form when the bug is viewed in a CVE context.
- self.request.getNearest(ICveSet) == (None, None))
+ self.request.getNearest(ICveSet) == (None, None)
+ )
@property
def status_widget_items(self):
@@ -2171,15 +2365,23 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
# by EnumeratedType have their name as the token and here we need
# the title as the token for backwards compatibility.
status_items = [
- (item.title, item) for item in BugTaskStatus.items
- if item not in (BugTaskStatus.UNKNOWN,
- BugTaskStatus.EXPIRED,
- BugTaskStatus.DOESNOTEXIST)
- or item == self.context.status]
+ (item.title, item)
+ for item in BugTaskStatus.items
+ if item
+ not in (
+ BugTaskStatus.UNKNOWN,
+ BugTaskStatus.EXPIRED,
+ BugTaskStatus.DOESNOTEXIST,
+ )
+ or item == self.context.status
+ ]
- disabled_items = [status for status in BugTaskStatus.items
+ disabled_items = [
+ status
+ for status in BugTaskStatus.items
if not self.context.canTransitionToStatus(status, self.user)
- and status != self.context.status]
+ and status != self.context.status
+ ]
else:
status_items = [(self.context.status.title, self.context.status)]
disabled_items = []
@@ -2187,8 +2389,9 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
items = vocabulary_to_choice_edit_items(
SimpleVocabulary.fromItems(status_items),
include_description=True,
- css_class_prefix='status',
- disabled_items=disabled_items)
+ css_class_prefix="status",
+ disabled_items=disabled_items,
+ )
return items
@@ -2202,15 +2405,18 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
# by EnumeratedType have their name as the token and here we need
# the title as the token for backwards compatibility.
importance_items = [
- (item.title, item) for item in BugTaskImportance.items
- if item != BugTaskImportance.UNKNOWN]
+ (item.title, item)
+ for item in BugTaskImportance.items
+ if item != BugTaskImportance.UNKNOWN
+ ]
items = vocabulary_to_choice_edit_items(
SimpleVocabulary.fromItems(importance_items),
include_description=True,
- css_class_prefix='importance')
+ css_class_prefix="importance",
+ )
else:
- items = '[]'
+ items = "[]"
return items
@@ -2226,13 +2432,14 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
items = vocabulary_to_choice_edit_items(
self._visible_milestones,
value_fn=lambda item: canonical_url(
- item, request=self.api_request))
- items.append({
- "name": "Remove milestone",
- "disabled": False,
- "value": None})
+ item, request=self.api_request
+ ),
+ )
+ items.append(
+ {"name": "Remove milestone", "disabled": False, "value": None}
+ )
else:
- items = '[]'
+ items = "[]"
return items
@@ -2243,7 +2450,7 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
@cachedproperty
def user_can_edit(self):
"""Can the user edit the task at all?"""
- return check_permission('launchpad.Edit', self.context)
+ return check_permission("launchpad.Edit", self.context)
@cachedproperty
def user_can_edit_importance(self):
@@ -2252,9 +2459,10 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
If yes, return True, otherwise return False.
"""
return (
- self.user_can_edit and
- self.user_can_edit_status and
- self.user_has_privileges)
+ self.user_can_edit
+ and self.user_can_edit_status
+ and self.user_has_privileges
+ )
@cachedproperty
def user_can_edit_status(self):
@@ -2269,7 +2477,8 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
if bugtask.bugwatch:
bugtracker = bugtask.bugwatch.bugtracker
edit_allowed = (
- bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS)
+ bugtracker.bugtrackertype == BugTrackerType.EMAILADDRESS
+ )
return edit_allowed
@property
@@ -2287,40 +2496,44 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
If yes, return True, otherwise return False.
"""
bugtask = self.context
- return (check_permission('launchpad.Delete', bugtask)
- and bugtask.canBeDeleted())
+ return (
+ check_permission("launchpad.Delete", bugtask)
+ and bugtask.canBeDeleted()
+ )
@property
def style_for_add_milestone(self):
if self.context.milestone is None:
- return ''
+ return ""
else:
- return 'hidden'
+ return "hidden"
@property
def style_for_edit_milestone(self):
if self.context.milestone is None:
- return 'hidden'
+ return "hidden"
else:
- return ''
+ return ""
def bugtask_config(self):
"""Configuration for the bugtask JS widgets on the row."""
- assignee_vocabulary_name, assignee_vocabulary = (
- get_assignee_vocabulary_info(self.context))
+ (
+ assignee_vocabulary_name,
+ assignee_vocabulary,
+ ) = get_assignee_vocabulary_info(self.context)
filter_details = vocabulary_filters(assignee_vocabulary)
# Display the search field only if the user can set any person
# or team
user = self.user
- hide_assignee_team_selection = (
- not self.context.userCanSetAnyAssignee(user) and
- (user is None or user.teams_participated_in.count() == 0))
+ hide_assignee_team_selection = not self.context.userCanSetAnyAssignee(
+ user
+ ) and (user is None or user.teams_participated_in.count() == 0)
cx = self.context
return dict(
id=cx.id,
- row_id=self.data['row_id'],
- form_row_id=self.data['form_row_id'],
- bugtask_path='/'.join([''] + self.data['link'].split('/')[3:]),
+ row_id=self.data["row_id"],
+ form_row_id=self.data["form_row_id"],
+ bugtask_path="/".join([""] + self.data["link"].split("/")[3:]),
prefix=get_prefix(cx),
targetname=cx.bugtargetdisplayname,
bug_title=cx.bug.title,
@@ -2331,7 +2544,7 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
hide_assignee_team_selection=hide_assignee_team_selection,
user_can_unassign=cx.userCanUnassign(user),
user_can_delete=self.user_can_delete_bugtask,
- delete_link=self.data['delete_link'],
+ delete_link=self.data["delete_link"],
target_is_product=IProduct.providedBy(cx.target),
status_widget_items=self.status_widget_items,
status_value=cx.status.title,
@@ -2339,15 +2552,15 @@ class BugTaskTableRowView(LaunchpadView, BugTaskBugWatchMixin,
importance_value=cx.importance.title,
milestone_widget_items=self.milestone_widget_items,
milestone_value=(
- canonical_url(
- cx.milestone,
- request=self.api_request)
- if cx.milestone else None),
+ canonical_url(cx.milestone, request=self.api_request)
+ if cx.milestone
+ else None
+ ),
user_can_edit_assignee=self.user_can_edit_assignee,
user_can_edit_milestone=self.user_has_privileges,
user_can_edit_status=self.user_can_edit_status,
user_can_edit_importance=self.user_has_privileges,
- )
+ )
@implementer(IObjectPrivacy)
@@ -2365,13 +2578,14 @@ class BugTaskPrivacyAdapter:
class BugTaskCreateQuestionView(LaunchpadFormView):
"""View for creating a question from a bug."""
+
schema = ICreateQuestionFromBugTaskForm
def setUpFields(self):
"""See `LaunchpadFormView`."""
LaunchpadFormView.setUpFields(self)
if not self.can_be_a_question:
- self.form_fields = self.form_fields.omit('comment')
+ self.form_fields = self.form_fields.omit("comment")
@property
def next_url(self):
@@ -2383,7 +2597,7 @@ class BugTaskCreateQuestionView(LaunchpadFormView):
"""Return True if this bug can become a question, otherwise False."""
return self.context.bug.canBeAQuestion()
- @action('Convert this Bug into a Question', name='create')
+ @action("Convert this Bug into a Question", name="create")
def create_action(self, action, data):
"""Create a question from this bug and set this bug to Invalid.
@@ -2396,26 +2610,28 @@ class BugTaskCreateQuestionView(LaunchpadFormView):
"""
if not self.context.bug.canBeAQuestion():
self.request.response.addNotification(
- 'This bug could not be converted into a question.')
+ "This bug could not be converted into a question."
+ )
return
- comment = data.get('comment', None)
+ comment = data.get("comment", None)
self.context.bug.convertToQuestion(self.user, comment=comment)
- label = 'Convert this bug to a question'
+ label = "Convert this bug to a question"
page_title = label
class BugTaskRemoveQuestionView(LaunchpadFormView):
"""View for creating a question from a bug."""
+
schema = IRemoveQuestionFromBugTaskForm
def setUpFields(self):
"""See `LaunchpadFormView`."""
LaunchpadFormView.setUpFields(self)
if not self.has_question:
- self.form_fields = self.form_fields.omit('comment')
+ self.form_fields = self.form_fields.omit("comment")
@property
def next_url(self):
@@ -2427,7 +2643,7 @@ class BugTaskRemoveQuestionView(LaunchpadFormView):
"""Return True if a question was created from this bug, or False."""
return self.context.bug.getQuestionCreatedFromBug() is not None
- @action('Convert Back to Bug', name='remove')
+ @action("Convert Back to Bug", name="remove")
def remove_action(self, action, data):
"""Remove a question from this bug.
@@ -2439,7 +2655,8 @@ class BugTaskRemoveQuestionView(LaunchpadFormView):
question = self.context.bug.getQuestionCreatedFromBug()
if question is None:
self.request.response.addNotification(
- 'This bug does not have a question to remove')
+ "This bug does not have a question to remove"
+ )
return
owner_is_subscribed = question.isSubscribed(self.context.bug.owner)
@@ -2453,24 +2670,29 @@ class BugTaskRemoveQuestionView(LaunchpadFormView):
'Removed Question #%s: <a href="%s">%s<a>.',
str(question.id),
canonical_url(question),
- question.title))
+ question.title,
+ )
+ )
- comment = data.get('comment', None)
+ comment = data.get("comment", None)
if comment is not None:
self.context.bug.newMessage(
owner=getUtility(ILaunchBag).user,
subject=self.context.bug.followup_subject(),
- content=comment)
+ content=comment,
+ )
@property
def label(self):
- return ('Bug #%i - Convert this question back to a bug'
- % self.context.bug.id)
+ return (
+ "Bug #%i - Convert this question back to a bug"
+ % self.context.bug.id
+ )
page_title = label
-@delegate_to(IBugActivity, context='activity')
+@delegate_to(IBugActivity, context="activity")
class BugActivityItem:
"""A decorated BugActivity."""
@@ -2495,42 +2717,44 @@ class BugActivityItem:
Some event names are more descriptive as data, but less relevant to
users, who are unfamiliar with the lp code."""
better_summaries = {
- 'bug task deleted': 'no longer affects',
- }
+ "bug task deleted": "no longer affects",
+ }
return better_summaries.get(summary, summary)
@property
def _formatted_tags_change(self):
"""Return a tags change as lists of added and removed tags."""
- assert self.whatchanged == 'tags', (
+ assert self.whatchanged == "tags", (
"Can't return a formatted tags change for a change in %s."
- % self.whatchanged)
+ % self.whatchanged
+ )
# Turn the strings of newvalue and oldvalue into sets so we
# can work out the differences.
- if self.newvalue != '':
- new_tags = set(re.split(r'\s+', self.newvalue))
+ if self.newvalue != "":
+ new_tags = set(re.split(r"\s+", self.newvalue))
else:
new_tags = set()
- if self.oldvalue != '':
- old_tags = set(re.split(r'\s+', self.oldvalue))
+ if self.oldvalue != "":
+ old_tags = set(re.split(r"\s+", self.oldvalue))
else:
old_tags = set()
added_tags = sorted(new_tags.difference(old_tags))
removed_tags = sorted(old_tags.difference(new_tags))
- return_string = ''
+ return_string = ""
if len(added_tags) > 0:
- return_string = "added: %s\n" % ' '.join(added_tags)
+ return_string = "added: %s\n" % " ".join(added_tags)
if len(removed_tags) > 0:
- return_string = (
- return_string + "removed: %s" % ' '.join(removed_tags))
+ return_string = return_string + "removed: %s" % " ".join(
+ removed_tags
+ )
# Trim any leading or trailing \ns and then convert the to
# <br />s so they're displayed correctly.
- return return_string.strip('\n')
+ return return_string.strip("\n")
@property
def change_details(self):
@@ -2538,49 +2762,50 @@ class BugActivityItem:
# Our default return dict. We may mutate this depending on
# what's changed.
return_dict = {
- 'old_value': self.oldvalue,
- 'new_value': self.newvalue,
- }
+ "old_value": self.oldvalue,
+ "new_value": self.newvalue,
+ }
attribute = self.attribute
- if attribute == 'title':
+ if attribute == "title":
# We display summary changes as a unified diff, replacing
# \ns with <br />s so that the lines are separated properly.
diff = html_escape(
- get_unified_diff(self.oldvalue, self.newvalue, 72))
+ get_unified_diff(self.oldvalue, self.newvalue, 72)
+ )
return diff.replace("\n", "<br />")
- elif attribute == 'description':
+ elif attribute == "description":
# Description changes can be quite long, so we just return
# 'updated' rather than returning the whole new description
# or a diff.
- return 'updated'
+ return "updated"
- elif attribute == 'tags':
+ elif attribute == "tags":
# We special-case tags because we can work out what's been
# added and what's been removed.
return html_escape(self._formatted_tags_change).replace(
- '\n', '<br />')
+ "\n", "<br />"
+ )
- elif attribute == 'assignee':
+ elif attribute == "assignee":
for key in return_dict:
if return_dict[key] is None:
- return_dict[key] = 'nobody'
+ return_dict[key] = "nobody"
else:
return_dict[key] = html_escape(return_dict[key])
- elif attribute == 'lock status':
+ elif attribute == "lock status":
if self.newvalue == str(BugLockStatus.COMMENT_ONLY):
reason = " "
detail = (
- 'Metadata changes locked{}and '
- 'limited to project staff'
+ "Metadata changes locked{}and " "limited to project staff"
)
if self.message:
reason = " ({}) ".format(html_escape(self.message))
return detail.format(reason)
else:
- return 'Metadata changes unlocked'
- elif attribute == 'lock reason':
+ return "Metadata changes unlocked"
+ elif attribute == "lock reason":
if self.newvalue != "unset" and self.oldvalue != "unset":
# return a proper message with old and new values
return "{} → {}".format(
@@ -2591,14 +2816,14 @@ class BugActivityItem:
return "{}".format(self.newvalue)
else:
return "Unset"
- elif attribute == 'milestone':
+ elif attribute == "milestone":
for key in return_dict:
if return_dict[key] is None:
- return_dict[key] = 'none'
+ return_dict[key] = "none"
else:
return_dict[key] = html_escape(return_dict[key])
- elif attribute == 'bug task deleted':
+ elif attribute == "bug task deleted":
return self.oldvalue
else:
@@ -2631,4 +2856,4 @@ class BugTaskBreadcrumb(Breadcrumb):
def detail(self):
bug = self.context.bug
title = smartquote('"%s"' % bug.title)
- return '%s %s' % (bug.displayname, title)
+ return "%s %s" % (bug.displayname, title)
diff --git a/lib/lp/bugs/browser/bugtracker.py b/lib/lp/bugs/browser/bugtracker.py
index 49cfb21..2002ba3 100644
--- a/lib/lp/bugs/browser/bugtracker.py
+++ b/lib/lp/bugs/browser/bugtracker.py
@@ -4,19 +4,19 @@
"""Bug tracker views."""
__all__ = [
- 'BugTrackerAddView',
- 'BugTrackerComponentGroupNavigation',
- 'BugTrackerEditView',
- 'BugTrackerEditComponentView',
- 'BugTrackerNavigation',
- 'BugTrackerNavigationMenu',
- 'BugTrackerSetBreadcrumb',
- 'BugTrackerSetContextMenu',
- 'BugTrackerSetNavigation',
- 'BugTrackerSetView',
- 'BugTrackerView',
- 'RemoteBug',
- ]
+ "BugTrackerAddView",
+ "BugTrackerComponentGroupNavigation",
+ "BugTrackerEditView",
+ "BugTrackerEditComponentView",
+ "BugTrackerNavigation",
+ "BugTrackerNavigationMenu",
+ "BugTrackerSetBreadcrumb",
+ "BugTrackerSetContextMenu",
+ "BugTrackerSetNavigation",
+ "BugTrackerSetView",
+ "BugTrackerView",
+ "RemoteBug",
+]
from itertools import chain
@@ -31,10 +31,10 @@ from zope.schema.vocabulary import SimpleVocabulary
from lp import _
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
- )
+ action,
+)
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.validators import LaunchpadValidationError
from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
@@ -47,48 +47,40 @@ from lp.bugs.interfaces.bugtracker import (
IBugTrackerComponentGroup,
IBugTrackerSet,
IRemoteBug,
- )
+)
from lp.services.database.sqlbase import flush_database_updates
-from lp.services.helpers import (
- english_list,
- shortlist,
- )
+from lp.services.helpers import english_list, shortlist
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
- canonical_url,
ContextMenu,
GetitemNavigation,
LaunchpadView,
Link,
Navigation,
+ canonical_url,
redirection,
stepthrough,
structured,
- )
+)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.batching import (
ActiveBatchNavigator,
BatchNavigator,
InactiveBatchNavigator,
- )
+)
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.interfaces import ILaunchBag
from lp.services.webapp.menu import NavigationMenu
-
# A set of bug tracker types for which there can only ever be one bug
# tracker.
-SINGLE_INSTANCE_TRACKERS = (
- BugTrackerType.DEBBUGS,
- )
+SINGLE_INSTANCE_TRACKERS = (BugTrackerType.DEBBUGS,)
# A set of bug tracker types that we should not allow direct creation
# of.
-NO_DIRECT_CREATION_TRACKERS = (
- SINGLE_INSTANCE_TRACKERS + (
- BugTrackerType.EMAILADDRESS,
- )
- )
+NO_DIRECT_CREATION_TRACKERS = SINGLE_INSTANCE_TRACKERS + (
+ BugTrackerType.EMAILADDRESS,
+)
class BugTrackerSetNavigation(GetitemNavigation):
@@ -100,11 +92,11 @@ class BugTrackerSetContextMenu(ContextMenu):
usedfor = IBugTrackerSet
- links = ['newbugtracker']
+ links = ["newbugtracker"]
def newbugtracker(self):
- text = 'Register another bug tracker'
- return Link('+newbugtracker', text, icon='add')
+ text = "Register another bug tracker"
+ return Link("+newbugtracker", text, icon="add")
class BugTrackerAddView(LaunchpadFormView):
@@ -112,40 +104,54 @@ class BugTrackerAddView(LaunchpadFormView):
page_title = "Register an external bug tracker"
schema = IBugTracker
label = page_title
- field_names = ['bugtrackertype', 'name', 'title', 'baseurl', 'summary',
- 'contactdetails']
+ field_names = [
+ "bugtrackertype",
+ "name",
+ "title",
+ "baseurl",
+ "summary",
+ "contactdetails",
+ ]
def setUpWidgets(self, context=None):
# We only show those bug tracker types for which there can be
# multiple instances in the bugtrackertype Choice widget.
vocab_items = [
- item for item in BugTrackerType.items.items
- if item not in NO_DIRECT_CREATION_TRACKERS]
+ item
+ for item in BugTrackerType.items.items
+ if item not in NO_DIRECT_CREATION_TRACKERS
+ ]
fields = []
for field_name in self.field_names:
- if field_name == 'bugtrackertype':
- fields.append(form.FormField(
- Choice(__name__='bugtrackertype',
- title=_('Bug Tracker Type'),
- values=vocab_items,
- default=BugTrackerType.BUGZILLA)))
+ if field_name == "bugtrackertype":
+ fields.append(
+ form.FormField(
+ Choice(
+ __name__="bugtrackertype",
+ title=_("Bug Tracker Type"),
+ values=vocab_items,
+ default=BugTrackerType.BUGZILLA,
+ )
+ )
+ )
else:
fields.append(self.form_fields[field_name])
self.form_fields = form.Fields(*fields)
super().setUpWidgets(context=context)
- @action(_('Add'), name='add')
+ @action(_("Add"), name="add")
def add(self, action, data):
"""Create the IBugTracker."""
btset = getUtility(IBugTrackerSet)
bugtracker = btset.ensureBugTracker(
- name=data['name'],
- bugtrackertype=data['bugtrackertype'],
- title=data['title'],
- summary=data['summary'],
- baseurl=data['baseurl'],
- contactdetails=data['contactdetails'],
- owner=getUtility(ILaunchBag).user)
+ name=data["name"],
+ bugtrackertype=data["bugtrackertype"],
+ title=data["title"],
+ summary=data["summary"],
+ baseurl=data["baseurl"],
+ contactdetails=data["contactdetails"],
+ owner=getUtility(ILaunchBag).user,
+ )
self.next_url = canonical_url(bugtracker)
@property
@@ -165,7 +171,8 @@ class BugTrackerSetView(LaunchpadView):
# bug watch counts per tracker. However the batching makes
# the inefficiency tolerable for now. Robert Collins 20100919.
self._pillar_cache = self.context.getPillarsForBugtrackers(
- list(self.context.getAllTrackers()), self.user)
+ list(self.context.getAllTrackers()), self.user
+ )
@property
def inactive_tracker_count(self):
@@ -175,14 +182,14 @@ class BugTrackerSetView(LaunchpadView):
def active_trackers(self):
results = self.context.getAllTrackers(active=True)
navigator = ActiveBatchNavigator(results, self.request)
- navigator.setHeadings('tracker', 'trackers')
+ navigator.setHeadings("tracker", "trackers")
return navigator
@cachedproperty
def inactive_trackers(self):
results = self.context.getAllTrackers(active=False)
navigator = InactiveBatchNavigator(results, self.request)
- navigator.setHeadings('tracker', 'trackers')
+ navigator.setHeadings("tracker", "trackers")
return navigator
def getPillarData(self, bugtracker):
@@ -202,8 +209,9 @@ class BugTrackerSetView(LaunchpadView):
else:
has_more_pillars = False
return {
- 'pillars': pillars[:self.pillar_limit],
- 'has_more_pillars': has_more_pillars}
+ "pillars": pillars[: self.pillar_limit],
+ "has_more_pillars": has_more_pillars,
+ }
class BugTrackerView(LaunchpadView):
@@ -213,7 +221,8 @@ class BugTrackerView(LaunchpadView):
@property
def page_title(self):
return smartquote(
- 'The "%s" bug tracker in Launchpad' % self.context.title)
+ 'The "%s" bug tracker in Launchpad' % self.context.title
+ )
def initialize(self):
self.batchnav = BatchNavigator(self.context.watches, self.request)
@@ -235,7 +244,8 @@ class BugTrackerView(LaunchpadView):
BUG_TRACKER_ACTIVE_VOCABULARY = SimpleVocabulary.fromItems(
- [('On', True), ('Off', False)])
+ [("On", True), ("Off", False)]
+)
class BugTrackerEditView(LaunchpadEditFormView):
@@ -243,30 +253,33 @@ class BugTrackerEditView(LaunchpadEditFormView):
schema = IBugTracker
custom_widget_summary = CustomWidgetFactory(
- TextAreaWidget, width=30, height=5)
+ TextAreaWidget, width=30, height=5
+ )
custom_widget_aliases = CustomWidgetFactory(DelimitedListWidget, height=3)
custom_widget_active = CustomWidgetFactory(
- LaunchpadRadioWidget, orientation='vertical')
+ LaunchpadRadioWidget, orientation="vertical"
+ )
@property
def page_title(self):
return smartquote(
- 'Change details for the "%s" bug tracker' % self.context.title)
+ 'Change details for the "%s" bug tracker' % self.context.title
+ )
@cachedproperty
def field_names(self):
field_names = [
- 'name',
- 'title',
- 'bugtrackertype',
- 'summary',
- 'baseurl',
- 'aliases',
- 'contactdetails',
- ]
+ "name",
+ "title",
+ "bugtrackertype",
+ "summary",
+ "baseurl",
+ "aliases",
+ "contactdetails",
+ ]
if check_permission("launchpad.Admin", self.context):
- field_names.append('active')
+ field_names.append("active")
return field_names
@@ -283,48 +296,56 @@ class BugTrackerEditView(LaunchpadEditFormView):
# If we're displaying the 'active' field we need to swap it out
# and replace it with a field that uses our custom vocabulary.
- if 'active' in self.field_names:
+ if "active" in self.field_names:
active_field = Choice(
- __name__='active',
- title=_('Updates for this bug tracker are'),
+ __name__="active",
+ title=_("Updates for this bug tracker are"),
vocabulary=BUG_TRACKER_ACTIVE_VOCABULARY,
- required=True, default=self.context.active)
+ required=True,
+ default=self.context.active,
+ )
- self.form_fields = self.form_fields.omit('active')
+ self.form_fields = self.form_fields.omit("active")
self.form_fields += form.Fields(active_field)
def validate(self, data):
# Normalise aliases to an empty list if it's None.
- if data.get('aliases') is None:
- data['aliases'] = []
+ if data.get("aliases") is None:
+ data["aliases"] = []
# If aliases has an error, unwrap the Dantean exception from
# Zope so that we can tell the user something useful.
- if self.getFieldError('aliases'):
+ if self.getFieldError("aliases"):
# XXX: wgrant 2008-04-02 bug=210901: The error
# messages may have already been escaped by
# LaunchpadValidationError, so wrap them in structured() to
# avoid double-escaping them. It's possible that non-LVEs
# could also be escaped, but I can't think of any cases so
# let's just escape them anyway.
- aliases_errors = self.widgets['aliases']._error.errors.args[0]
+ aliases_errors = self.widgets["aliases"]._error.errors.args[0]
maybe_structured_errors = [
structured(error)
- if isinstance(error, LaunchpadValidationError) else error
- for error in aliases_errors]
- self.setFieldError('aliases', structured(
- '<br />'.join(['%s'] * len(maybe_structured_errors)),
- *maybe_structured_errors))
-
- @action('Change', name='change')
+ if isinstance(error, LaunchpadValidationError)
+ else error
+ for error in aliases_errors
+ ]
+ self.setFieldError(
+ "aliases",
+ structured(
+ "<br />".join(["%s"] * len(maybe_structured_errors)),
+ *maybe_structured_errors,
+ ),
+ )
+
+ @action("Change", name="change")
def change_action(self, action, data):
# If the baseurl is going to change, save the current baseurl
# as an alias. Users attempting to use this URL, which is
# presumably incorrect or out-of-date, will be captured.
current_baseurl = self.context.baseurl
- requested_baseurl = data['baseurl']
+ requested_baseurl = data["baseurl"]
if requested_baseurl != current_baseurl:
- data['aliases'].append(current_baseurl)
+ data["aliases"].append(current_baseurl)
self.updateContextFromData(data)
self.next_url = canonical_url(self.context)
@@ -348,12 +369,15 @@ class BugTrackerEditView(LaunchpadEditFormView):
# Check that no products or projects use this bugtracker.
pillars = (
- getUtility(IBugTrackerSet).getPillarsForBugtrackers(
- [self.context]).get(self.context, []))
+ getUtility(IBugTrackerSet)
+ .getPillarsForBugtrackers([self.context])
+ .get(self.context, [])
+ )
if len(pillars) > 0:
reasons.append(
- 'This is the bug tracker for %s.' % english_list(
- sorted(pillar.title for pillar in pillars)))
+ "This is the bug tracker for %s."
+ % english_list(sorted(pillar.title for pillar in pillars))
+ )
# Only admins and registry experts can delete bug watches en
# masse.
@@ -364,30 +388,32 @@ class BugTrackerEditView(LaunchpadEditFormView):
break
else:
reasons.append(
- 'There are linked bug watches and only members of %s '
- 'can delete them en masse.' % english_list(
- sorted(team.title for team in admin_teams)))
+ "There are linked bug watches and only members of %s "
+ "can delete them en masse."
+ % english_list(sorted(team.title for team in admin_teams))
+ )
# Bugtrackers with imported messages cannot be deleted.
if not self.context.imported_bug_messages.is_empty():
reasons.append(
- 'Bug comments have been imported via this bug tracker.')
+ "Bug comments have been imported via this bug tracker."
+ )
# If the bugtracker is a celebrity then we protect it from
# deletion.
celebrities_set = {
getattr(celebrities, name)
- for name in ILaunchpadCelebrities.names()}
+ for name in ILaunchpadCelebrities.names()
+ }
if self.context in celebrities_set:
- reasons.append(
- 'This bug tracker is protected from deletion.')
+ reasons.append("This bug tracker is protected from deletion.")
return reasons
def delete_condition(self, action):
return len(self.delete_not_possible_reasons) == 0
- @action('Delete', name='delete', condition=delete_condition)
+ @action("Delete", name="delete", condition=delete_condition)
def delete_action(self, action, data):
# First unlink bug watches from all bugtasks, flush updates,
# then delete the watches themselves.
@@ -395,9 +421,10 @@ class BugTrackerEditView(LaunchpadEditFormView):
for bugtask in watch.bugtasks:
if len(bugtask.bug.bugtasks) < 2:
raise AssertionError(
- 'There should be more than one bugtask for a bug '
- 'when one of them is linked to the original bug via '
- 'a bug watch.')
+ "There should be more than one bugtask for a bug "
+ "when one of them is linked to the original bug via "
+ "a bug watch."
+ )
bugtask.bugwatch = None
flush_database_updates()
for watch in self.context.watches:
@@ -409,7 +436,8 @@ class BugTrackerEditView(LaunchpadEditFormView):
# Hey, it worked! Tell the user.
self.request.response.addInfoNotification(
- '%s has been deleted.' % (self.context.title,))
+ "%s has been deleted." % (self.context.title,)
+ )
# Go back to the bug tracker listing.
self.next_url = canonical_url(getUtility(IBugTrackerSet))
@@ -421,18 +449,21 @@ class BugTrackerEditView(LaunchpadEditFormView):
def reschedule_action_condition(self, action):
"""Return True if the user can see the reschedule action."""
user_can_reset_watches = check_permission(
- "launchpad.Admin", self.context)
+ "launchpad.Admin", self.context
+ )
return user_can_reset_watches and not self.context.watches.is_empty()
@action(
- 'Reschedule all watches', name='reschedule',
- condition=reschedule_action_condition)
+ "Reschedule all watches",
+ name="reschedule",
+ condition=reschedule_action_condition,
+ )
def rescheduleAction(self, action, data):
"""Reschedule all the watches for the bugtracker."""
self.context.resetWatches()
self.request.response.addInfoNotification(
- "All bug watches on %s have been rescheduled." %
- self.context.title)
+ "All bug watches on %s have been rescheduled." % self.context.title
+ )
self.next_url = canonical_url(self.context)
@@ -464,24 +495,26 @@ class BugTrackerEditComponentView(LaunchpadEditFormView):
This class assumes that bug tracker components are always
linked to source packages in the Ubuntu distribution.
"""
+
schema = IBugTrackerComponent
custom_widget_source_package_name = UbuntuSourcePackageNameWidget
- field_names = ['source_package_name']
- page_title = 'Link component'
+ field_names = ["source_package_name"]
+ page_title = "Link component"
@property
def label(self):
return (
- 'Link a distribution source package to %s component' %
- self.context.name)
+ "Link a distribution source package to %s component"
+ % self.context.name
+ )
@property
def initial_values(self):
"""See `LaunchpadFormView.`"""
- field_values = dict(source_package_name='')
+ field_values = dict(source_package_name="")
dsp = self.context.distro_source_package
if dsp is not None:
- field_values['source_package_name'] = dsp.name
+ field_values["source_package_name"] = dsp.name
return field_values
@property
@@ -497,34 +530,45 @@ class BugTrackerEditComponentView(LaunchpadEditFormView):
look it up in Ubuntu to retrieve the distro_source_package
object, and link it to this component.
"""
- source_package_name = data['source_package_name']
- distribution = self.widgets['source_package_name'].getDistribution()
+ source_package_name = data["source_package_name"]
+ distribution = self.widgets["source_package_name"].getDistribution()
dsp = distribution.getSourcePackage(source_package_name)
bug_tracker = self.context.component_group.bug_tracker
# Has this source package already been assigned to a component?
component = bug_tracker.getRemoteComponentForDistroSourcePackageName(
- distribution, source_package_name)
+ distribution, source_package_name
+ )
if component is not None:
self.request.response.addNotification(
- "The %s source package is already linked to %s:%s in %s." % (
+ "The %s source package is already linked to %s:%s in %s."
+ % (
source_package_name.name,
component.component_group.name,
- component.name, distribution.name))
+ component.name,
+ distribution.name,
+ )
+ )
return
# The submitted component can be linked to the distro source package.
component = context or self.context
component.distro_source_package = dsp
if source_package_name is None:
self.request.response.addNotification(
- "%s:%s is now unlinked." % (
- component.component_group.name, component.name))
+ "%s:%s is now unlinked."
+ % (component.component_group.name, component.name)
+ )
else:
self.request.response.addNotification(
- "%s:%s is now linked to the %s source package in %s." % (
- component.component_group.name, component.name,
- source_package_name.name, distribution.name))
+ "%s:%s is now linked to the %s source package in %s."
+ % (
+ component.component_group.name,
+ component.name,
+ source_package_name.name,
+ distribution.name,
+ )
+ )
- @action('Save Changes', name='save')
+ @action("Save Changes", name="save")
def save_action(self, action, data):
"""Update the component with the form data."""
self.updateContextFromData(data)
@@ -559,8 +603,7 @@ class RemoteBug:
@property
def title(self):
- return 'Remote Bug #%s in %s' % (self.remotebug,
- self.bugtracker.title)
+ return "Remote Bug #%s in %s" % (self.remotebug, self.bugtracker.title)
class RemoteBugView(LaunchpadView):
@@ -574,9 +617,9 @@ class RemoteBugView(LaunchpadView):
class BugTrackerNavigationMenu(NavigationMenu):
usedfor = BugTrackerView
- facet = 'bugs'
- links = ['edit']
+ facet = "bugs"
+ links = ["edit"]
def edit(self):
- text = 'Change details'
- return Link('+edit', text, icon='edit')
+ text = "Change details"
+ return Link("+edit", text, icon="edit")
diff --git a/lib/lp/bugs/browser/bugwatch.py b/lib/lp/bugs/browser/bugwatch.py
index 516adad..4d67816 100644
--- a/lib/lp/bugs/browser/bugwatch.py
+++ b/lib/lp/bugs/browser/bugwatch.py
@@ -4,20 +4,17 @@
"""IBugWatch-related browser views."""
__all__ = [
- 'BugWatchSetNavigation',
- 'BugWatchActivityPortletView',
- 'BugWatchEditView',
- 'BugWatchView'
- ]
+ "BugWatchSetNavigation",
+ "BugWatchActivityPortletView",
+ "
Follow ups