launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28607
[Merge] ~cjwatson/launchpad:black-app into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:black-app into launchpad:master.
Commit message:
lp.app: Apply black
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/424957
--
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-app into launchpad:master.
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 21314b8..b57c076 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -56,3 +56,5 @@ afcfc15adcf3267d3fd07d7679df00231853c908
c606443bdb2f342593c9a7c9437cb70c01f85f29
# apply black to lp.answers
7ae201d4e317cc9db665a0edb28c2439797daff6
+# apply black to lp.app
+8fd124775592a33c3d2ce9ef8111a9a5f1a5e089
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c9d3f02..7d5c58e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -42,6 +42,7 @@ repos:
files: |
(?x)^lib/lp/(
answers
+ |app
)/
- repo: https://github.com/PyCQA/isort
rev: 5.9.2
@@ -60,6 +61,7 @@ repos:
exclude: |
(?x)^lib/lp/(
answers
+ |app
)/
- id: isort
alias: isort-black
@@ -68,6 +70,7 @@ repos:
files: |
(?x)^lib/lp/(
answers
+ |app
)/
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
diff --git a/lib/lp/app/__init__.py b/lib/lp/app/__init__.py
index 0276aec..69ebb95 100644
--- a/lib/lp/app/__init__.py
+++ b/lib/lp/app/__init__.py
@@ -19,5 +19,4 @@ from zope.formlib import itemswidgets
# for first page load.
import lp.app.versioninfo # noqa: F401
-
itemswidgets.EXPLICIT_EMPTY_SELECTION = False
diff --git a/lib/lp/app/browser/badge.py b/lib/lp/app/browser/badge.py
index 2dcf6ba..d3fd5a6 100644
--- a/lib/lp/app/browser/badge.py
+++ b/lib/lp/app/browser/badge.py
@@ -9,16 +9,13 @@ Badges are shown in two main places:
"""
__all__ = [
- 'Badge',
- 'HasBadgeBase',
- 'IHasBadges',
- 'STANDARD_BADGES',
- ]
+ "Badge",
+ "HasBadgeBase",
+ "IHasBadges",
+ "STANDARD_BADGES",
+]
-from zope.interface import (
- implementer,
- Interface,
- )
+from zope.interface import Interface, implementer
from lp.services.privacy.interfaces import IObjectPrivacy
@@ -30,8 +27,9 @@ class Badge:
views, and as larger images on the object content pages.
"""
- def __init__(self, icon_image=None, heading_image=None,
- alt='', title='', id=''):
+ def __init__(
+ self, icon_image=None, heading_image=None, alt="", title="", id=""
+ ):
self.small_image = icon_image
self.large_image = heading_image
self.alt = alt
@@ -39,17 +37,19 @@ class Badge:
self.id = id
def copy(self):
- return Badge(self.small_image, self.large_image, self.alt,
- self.title, self.id)
+ return Badge(
+ self.small_image, self.large_image, self.alt, self.title, self.id
+ )
def renderIconImage(self):
"""Render the small image as an HTML img tag."""
if self.small_image:
- return ('<img alt="%s" width="14" height="14" src="%s"'
- ' title="%s"/>'
- % (self.alt, self.small_image, self.title))
+ return (
+ '<img alt="%s" width="14" height="14" src="%s"'
+ ' title="%s"/>' % (self.alt, self.small_image, self.title)
+ )
else:
- return ''
+ return ""
def renderHeadingImage(self):
"""Render the large image as an HTML img tag."""
@@ -57,33 +57,56 @@ class Badge:
if self.id:
id_attribute = 'id="%s"' % self.id
else:
- id_attribute = ''
- return ('<img alt="%s" width="32" height="32" src="%s"'
- ' title="%s" %s/>' % (
- self.alt, self.large_image, self.title, id_attribute))
+ id_attribute = ""
+ return (
+ '<img alt="%s" width="32" height="32" src="%s"'
+ ' title="%s" %s/>'
+ % (self.alt, self.large_image, self.title, id_attribute)
+ )
else:
- return ''
+ return ""
STANDARD_BADGES = {
- 'bug': Badge('/@@/bug', '/@@/bug-large',
- 'bug', 'Linked to a bug', 'bugbadge'),
- 'blueprint': Badge('/@@/blueprint', None,
- '(Linked to a blueprint)', 'Linked to a blueprint'),
- 'branch': Badge('/@@/branch', '/@@/branch-large',
- '(Linked to a branch)', 'Linked to a branch',
- 'branchbadge'),
- 'private': Badge('/@@/private', '/@@/private-large',
- '(Private)', 'Private', 'privatebadge'),
- 'security': Badge('/@@/security', '/@@/security-large',
- '(Security vulnerability)', 'Security vulnerability',
- 'securitybadge'),
- 'mergeproposal': Badge('/@@/merge-proposal-icon',
- '/@@/merge-proposal-large',
- '(Has a merge proposal)', 'Has a merge proposal',
- 'mpbadge'),
- 'patch': Badge(None, None, '(Has a patch)', 'Has a patch', 'haspatch'),
- }
+ "bug": Badge(
+ "/@@/bug", "/@@/bug-large", "bug", "Linked to a bug", "bugbadge"
+ ),
+ "blueprint": Badge(
+ "/@@/blueprint",
+ None,
+ "(Linked to a blueprint)",
+ "Linked to a blueprint",
+ ),
+ "branch": Badge(
+ "/@@/branch",
+ "/@@/branch-large",
+ "(Linked to a branch)",
+ "Linked to a branch",
+ "branchbadge",
+ ),
+ "private": Badge(
+ "/@@/private",
+ "/@@/private-large",
+ "(Private)",
+ "Private",
+ "privatebadge",
+ ),
+ "security": Badge(
+ "/@@/security",
+ "/@@/security-large",
+ "(Security vulnerability)",
+ "Security vulnerability",
+ "securitybadge",
+ ),
+ "mergeproposal": Badge(
+ "/@@/merge-proposal-icon",
+ "/@@/merge-proposal-large",
+ "(Has a merge proposal)",
+ "Has a merge proposal",
+ "mpbadge",
+ ),
+ "patch": Badge(None, None, "(Has a patch)", "Has a patch", "haspatch"),
+}
class IHasBadges(Interface):
@@ -97,8 +120,7 @@ class IHasBadges(Interface):
"""
def getVisibleBadges():
- """Return a list of `Badge` objects that the logged in user can see.
- """
+ """Return a list of `Badge` objects that the logged in user can see."""
@implementer(IHasBadges)
@@ -113,7 +135,7 @@ class HasBadgeBase:
"""
# All private objects should show the private badge.
- badges = ('private',)
+ badges = ("private",)
# This class is now a default adapter for IHasBadges.
def __init__(self, context):
diff --git a/lib/lp/app/browser/folder.py b/lib/lp/app/browser/folder.py
index d38a5be..ad4c620 100644
--- a/lib/lp/app/browser/folder.py
+++ b/lib/lp/app/browser/folder.py
@@ -2,9 +2,9 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
- 'ExportedFolder',
- 'ExportedImageFolder',
- ]
+ "ExportedFolder",
+ "ExportedImageFolder",
+]
import errno
import os
@@ -27,7 +27,7 @@ class File:
def __init__(self, path, name):
self.path = path
- f = open(path, 'rb')
+ f = open(path, "rb")
self.data = f.read()
f.close()
self.content_type, enc = guess_content_type(path, self.data)
@@ -50,7 +50,7 @@ class ExportedFolder:
to True to change this.
"""
- rev_part_re = re.compile('rev[0-9a-f]+$')
+ rev_part_re = re.compile("rev[0-9a-f]+$")
export_subdirectories = False
@@ -68,7 +68,7 @@ class ExportedFolder:
if not names:
# Just the root directory, so make this a 404.
- raise NotFound(self, '')
+ raise NotFound(self, "")
elif len(names) > 1 and not self.export_subdirectories:
# Too many path elements, so make this a 404.
raise NotFound(self, self.names[-1])
@@ -78,7 +78,8 @@ class ExportedFolder:
# because the Zope name traversal will sanitize './' and '../'
# before setting the value of self.names.
return self.prepareDataForServing(
- os.path.join(self.folder, *names))
+ os.path.join(self.folder, *names)
+ )
def prepareDataForServing(self, filename):
"""Set the response headers and return the data for this resource."""
@@ -97,8 +98,8 @@ class ExportedFolder:
# TODO: Set an appropriate charset too. There may be zope code we
# can reuse for this.
response = self.request.response
- response.setHeader('Content-Type', fileobj.content_type)
- response.setHeader('Last-Modified', fileobj.lmh)
+ response.setHeader("Content-Type", fileobj.content_type)
+ response.setHeader("Last-Modified", fileobj.lmh)
setCacheControl(response)
return fileobj.data
@@ -110,8 +111,9 @@ class ExportedFolder:
"""Traverse to the given name."""
# The two following constraints are enforced by the publisher.
assert os.path.sep not in name, (
- 'traversed name contains os.path.sep: %s' % name)
- assert name != '..', 'traversing to ..'
+ "traversed name contains os.path.sep: %s" % name
+ )
+ assert name != "..", "traversing to .."
self.names.append(name)
return self
@@ -122,7 +124,8 @@ class ExportedFolder:
def folder(self):
raise (
NotImplementedError,
- 'Your subclass of ExportedFolder should have its own folder.')
+ "Your subclass of ExportedFolder should have its own folder.",
+ )
class ExportedImageFolder(ExportedFolder):
@@ -133,7 +136,7 @@ class ExportedImageFolder(ExportedFolder):
"""
# The extensions we consider.
- image_extensions = ('.png', '.gif')
+ image_extensions = (".png", ".gif")
def prepareDataForServing(self, filename):
"""Serve files without their extension.
@@ -142,7 +145,7 @@ class ExportedImageFolder(ExportedFolder):
the same base name and has an image extension, it will be served.
"""
root, ext = os.path.splitext(filename)
- if ext == '' and not os.path.exists(root):
+ if ext == "" and not os.path.exists(root):
for image_ext in self.image_extensions:
if os.path.exists(root + image_ext):
filename = filename + image_ext
diff --git a/lib/lp/app/browser/informationtype.py b/lib/lp/app/browser/informationtype.py
index f2848b4..034b6e3 100644
--- a/lib/lp/app/browser/informationtype.py
+++ b/lib/lp/app/browser/informationtype.py
@@ -2,8 +2,8 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
- 'InformationTypePortletMixin',
- ]
+ "InformationTypePortletMixin",
+]
from lazr.restful.interfaces import IJSONRequestCache
@@ -13,7 +13,6 @@ from lp.app.utilities import json_dump_information_types
class InformationTypePortletMixin:
-
def _getContext(self):
information_typed = IInformationType(self.context, None)
if information_typed is None:
@@ -25,8 +24,8 @@ class InformationTypePortletMixin:
if IInformationType.providedBy(context):
cache = IJSONRequestCache(self.request)
json_dump_information_types(
- cache,
- context.getAllowedInformationTypes(self.user))
+ cache, context.getAllowedInformationTypes(self.user)
+ )
@property
def information_type(self):
@@ -45,17 +44,21 @@ class InformationTypePortletMixin:
@property
def information_type_css(self):
context = self._getContext()
- if (IInformationType.providedBy(context) and
- context.information_type in PRIVATE_INFORMATION_TYPES):
- return 'sprite private'
+ if (
+ IInformationType.providedBy(context)
+ and context.information_type in PRIVATE_INFORMATION_TYPES
+ ):
+ return "sprite private"
else:
- return 'sprite public'
+ return "sprite public"
@property
def privacy_portlet_css(self):
context = self._getContext()
- if (IInformationType.providedBy(context) and
- context.information_type in PRIVATE_INFORMATION_TYPES):
- return 'portlet private'
+ if (
+ IInformationType.providedBy(context)
+ and context.information_type in PRIVATE_INFORMATION_TYPES
+ ):
+ return "portlet private"
else:
- return 'portlet public'
+ return "portlet public"
diff --git a/lib/lp/app/browser/launchpad.py b/lib/lp/app/browser/launchpad.py
index c887a24..b6f178e 100644
--- a/lib/lp/app/browser/launchpad.py
+++ b/lib/lp/app/browser/launchpad.py
@@ -4,32 +4,29 @@
"""Browser code for the launchpad application."""
__all__ = [
- 'AppFrontPageSearchView',
- 'ExceptionHierarchy',
- 'Hierarchy',
- 'IcingFolder',
- 'iter_view_registrations',
- 'LaunchpadImageFolder',
- 'LaunchpadRootNavigation',
- 'LinkView',
- 'LoginStatus',
- 'Macro',
- 'MaintenanceMessage',
- 'NavigationMenuTabs',
- 'SoftTimeoutView',
- 'get_launchpad_views',
- ]
+ "AppFrontPageSearchView",
+ "ExceptionHierarchy",
+ "Hierarchy",
+ "IcingFolder",
+ "iter_view_registrations",
+ "LaunchpadImageFolder",
+ "LaunchpadRootNavigation",
+ "LinkView",
+ "LoginStatus",
+ "Macro",
+ "MaintenanceMessage",
+ "NavigationMenuTabs",
+ "SoftTimeoutView",
+ "get_launchpad_views",
+]
-from datetime import timedelta
import operator
import os
import re
import time
-from urllib.parse import (
- parse_qs,
- urlencode,
- )
+from datetime import timedelta
+from urllib.parse import parse_qs, urlencode
from zope import i18n
from zope.component import (
@@ -37,53 +34,31 @@ from zope.component import (
getUtility,
queryAdapter,
queryUtility,
- )
-from zope.datetime import (
- DateTimeError,
- parseDatetimetz,
- )
+)
+from zope.datetime import DateTimeError, parseDatetimetz
from zope.i18nmessageid import Message
-from zope.interface import (
- alsoProvides,
- implementer,
- Interface,
- )
+from zope.interface import Interface, alsoProvides, implementer
from zope.publisher.defaultview import getDefaultViewName
from zope.publisher.interfaces import NotFound
from zope.publisher.interfaces.browser import IBrowserPublisher
from zope.publisher.interfaces.xmlrpc import IXMLRPCRequest
-from zope.schema import (
- Choice,
- TextLine,
- )
+from zope.schema import Choice, TextLine
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
-from zope.traversing.interfaces import (
- IPathAdapter,
- ITraversable,
- )
+from zope.traversing.interfaces import IPathAdapter, ITraversable
from lp import _
from lp.answers.interfaces.questioncollection import IQuestionSet
-from lp.app.browser.folder import (
- ExportedFolder,
- ExportedImageFolder,
- )
+from lp.app.browser.folder import ExportedFolder, ExportedImageFolder
from lp.app.browser.launchpadform import LaunchpadFormView
-from lp.app.browser.tales import (
- DurationFormatterAPI,
- MenuAPI,
- )
+from lp.app.browser.tales import DurationFormatterAPI, MenuAPI
from lp.app.errors import (
GoneError,
NameLookupFailed,
NotFoundError,
POSTToNonCanonicalURL,
- )
-from lp.app.interfaces.headings import (
- IHeadingBreadcrumb,
- IMajorHeadingView,
- )
+)
+from lp.app.interfaces.headings import IHeadingBreadcrumb, IMajorHeadingView
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.interfaces.services import IServiceFactory
from lp.app.widgets.project import ProjectScopeWidget
@@ -99,7 +74,7 @@ from lp.code.errors import (
CannotHaveLinkedBranch,
InvalidNamespace,
NoLinkedBranch,
- )
+)
from lp.code.interfaces.branch import IBranchSet
from lp.code.interfaces.branchlookup import IBranchLookup
from lp.code.interfaces.codehosting import IBazaarApplication
@@ -120,7 +95,7 @@ from lp.registry.interfaces.product import (
InvalidProductName,
IProduct,
IProductSet,
- )
+)
from lp.registry.interfaces.projectgroup import IProjectGroupSet
from lp.registry.interfaces.role import IPersonRoles
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
@@ -132,18 +107,18 @@ from lp.services.propertycache import cachedproperty
from lp.services.statistics.interfaces.statistic import ILaunchpadStatisticSet
from lp.services.temporaryblobstorage.interfaces import (
ITemporaryStorageManager,
- )
+)
from lp.services.utils import utc_now
from lp.services.verification.interfaces.logintoken import ILoginTokenSet
from lp.services.webapp import (
- canonical_name,
- canonical_url,
LaunchpadView,
Link,
Navigation,
StandardLaunchpadFacets,
+ canonical_name,
+ canonical_url,
stepto,
- )
+)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.escaping import structured
@@ -155,7 +130,7 @@ from lp.services.webapp.interfaces import (
ILaunchpadRoot,
IMultiFacetedBreadcrumb,
INavigationMenu,
- )
+)
from lp.services.webapp.menu import get_facet
from lp.services.webapp.publisher import RedirectionView
from lp.services.webapp.url import urlappend
@@ -172,7 +147,7 @@ from lp.testopenid.interfaces.server import ITestOpenIDApplication
from lp.translations.interfaces.translationgroup import ITranslationGroupSet
from lp.translations.interfaces.translationimportqueue import (
ITranslationImportQueue,
- )
+)
from lp.translations.interfaces.translations import IRosettaApplication
@@ -188,10 +163,14 @@ class NavigationMenuTabs(LaunchpadView):
def initialize(self):
menuapi = MenuAPI(self.context)
- self.links = sorted((
- link for link in menuapi.navigation.values()
- if (link.enabled or config.launchpad.devmode)),
- key=operator.attrgetter('sort_key'))
+ self.links = sorted(
+ (
+ link
+ for link in menuapi.navigation.values()
+ if (link.enabled or config.launchpad.devmode)
+ ),
+ key=operator.attrgetter("sort_key"),
+ )
self.title = None
if len(self.links) > 0:
facet = menuapi.selectedfacetname()
@@ -202,7 +181,7 @@ class NavigationMenuTabs(LaunchpadView):
def render(self):
if not self.links:
- return ''
+ return ""
else:
return self.template()
@@ -213,7 +192,8 @@ class LinkView(LaunchpadView):
The link is not rendered if it's not enabled and we are not in development
mode.
"""
- MODIFY_ICONS = ('edit', 'remove', 'trash-icon')
+
+ MODIFY_ICONS = ("edit", "remove", "trash-icon")
@property
def sprite_class(self):
@@ -222,9 +202,9 @@ class LinkView(LaunchpadView):
# The 3.0 UI design says these are displayed like other icons
# But they do not have the same use so we want to keep this rule
# separate.
- return 'sprite modify'
+ return "sprite modify"
else:
- return 'sprite'
+ return "sprite"
def render(self):
"""Render the menu link if it's enabled or we're in dev mode."""
@@ -235,19 +215,19 @@ class LinkView(LaunchpadView):
# at the end of an embedded template.
return self.template().strip()
else:
- return ''
+ return ""
@property
def css_class(self):
"""Return the CSS class."""
value = ["menu-link-%s" % self.context.name]
if not self.context.linked:
- value.append('nolink')
+ value.append("nolink")
if self.context.icon:
value.append(self.sprite_class)
value.append(self.context.icon)
if self.context.hidden:
- value.append('hidden')
+ value.append("hidden")
return " ".join(value)
@property
@@ -255,14 +235,14 @@ class LinkView(LaunchpadView):
"""Return the url if linked."""
if self.context.linked:
return self.context.url
- return ''
+ return ""
@property
def summary(self):
"""Return the summary if linked."""
if self.context.linked:
return self.context.summary
- return ''
+ return ""
class Hierarchy(LaunchpadView):
@@ -273,9 +253,13 @@ class Hierarchy(LaunchpadView):
"""The objects for which we want breadcrumbs."""
# Start the chain with the deepest object that has a breadcrumb.
try:
- objects = [next(
- obj for obj in reversed(self.request.traversed_objects)
- if IBreadcrumb(obj, None))]
+ objects = [
+ next(
+ obj
+ for obj in reversed(self.request.traversed_objects)
+ if IBreadcrumb(obj, None)
+ )
+ ]
except StopIteration:
return []
# Now iterate. If an object has a breadcrumb, it can override
@@ -318,13 +302,15 @@ class Hierarchy(LaunchpadView):
for idx, breadcrumb in reversed(list(enumerate(breadcrumbs))):
if IMultiFacetedBreadcrumb.providedBy(breadcrumb):
facet_crumb = Breadcrumb(
- breadcrumb.context, rootsite=facet.rootsite,
- text=facet.text)
+ breadcrumb.context,
+ rootsite=facet.rootsite,
+ text=facet.text,
+ )
alsoProvides(facet_crumb, IFacetBreadcrumb)
breadcrumbs.insert(idx + 1, facet_crumb)
# Ensure that all remaining breadcrumbs are
# themselves faceted.
- for remaining_crumb in breadcrumbs[idx + 1:]:
+ for remaining_crumb in breadcrumbs[idx + 1 :]:
remaining_crumb.rootsite_override = facet.rootsite
break
if len(breadcrumbs) > 0:
@@ -342,8 +328,9 @@ class Hierarchy(LaunchpadView):
"""
crumbs = []
for crumb in self.items:
- if (IHeadingBreadcrumb.providedBy(crumb)
- or IFacetBreadcrumb.providedBy(crumb)):
+ if IHeadingBreadcrumb.providedBy(
+ crumb
+ ) or IFacetBreadcrumb.providedBy(crumb):
crumbs = []
continue
crumbs.append(crumb)
@@ -383,13 +370,13 @@ class Hierarchy(LaunchpadView):
# Views may provide an additional breadcrumb to precede them.
# This is useful to have an add view link back to its
# collection despite its parent being the context of the collection.
- if hasattr(view, 'inside_breadcrumb'):
+ if hasattr(view, "inside_breadcrumb"):
crumbs.append(view.inside_breadcrumb)
- if hasattr(view, '__name__') and view.__name__ not in default_views:
- title = getattr(view, 'page_title', None)
+ if hasattr(view, "__name__") and view.__name__ not in default_views:
+ title = getattr(view, "page_title", None)
if title is None:
- title = getattr(view, 'label', None)
+ title = getattr(view, "label", None)
if isinstance(title, Message):
title = i18n.translate(title, context=self.request)
crumbs.append(Breadcrumb(None, url=url, text=title))
@@ -403,14 +390,17 @@ class Hierarchy(LaunchpadView):
# If the view is an IMajorHeadingView then we do not want
# to display breadcrumbs either.
has_major_heading = IMajorHeadingView.providedBy(
- self._naked_context_view)
+ self._naked_context_view
+ )
return len(self.items_for_body) > 1 and not has_major_heading
@property
def heading_breadcrumbs(self):
crumbs = [
- crumb for crumb in self.items
- if IHeadingBreadcrumb.providedBy(crumb)]
+ crumb
+ for crumb in self.items
+ if IHeadingBreadcrumb.providedBy(crumb)
+ ]
assert len(crumbs) <= 2
return crumbs
@@ -421,38 +411,42 @@ class Hierarchy(LaunchpadView):
H1, else an H2.
"""
# The title is static, but only the index view gets an H1.
- tag = 'h1' if IMajorHeadingView.providedBy(self.context) else 'h2'
+ tag = "h1" if IMajorHeadingView.providedBy(self.context) else "h2"
# If there is actually no root context, then it's a top-level
# context-less page so Launchpad.net is shown as the branding.
crumb_markups = []
for crumb in self.heading_breadcrumbs:
crumb_markups.append(
- structured('<a href="%s">%s</a>', crumb.url, crumb.detail))
+ structured('<a href="%s">%s</a>', crumb.url, crumb.detail)
+ )
if not crumb_markups:
- crumb_markups.append(structured('<span>Launchpad.net</span>'))
+ crumb_markups.append(structured("<span>Launchpad.net</span>"))
content = structured(
- '<br />'.join(['%s'] * len(crumb_markups)), *crumb_markups)
+ "<br />".join(["%s"] * len(crumb_markups)), *crumb_markups
+ )
return structured(
- '<%s id="watermark-heading">%s</%s>',
- tag, content, tag).escapedtext
+ '<%s id="watermark-heading">%s</%s>', tag, content, tag
+ ).escapedtext
def logo(self):
"""Return the logo image for the top header breadcrumb's context."""
logo_context = (
- self.heading_breadcrumbs[0].context if self.heading_breadcrumbs
- else None)
- adapter = queryAdapter(logo_context, IPathAdapter, 'image')
+ self.heading_breadcrumbs[0].context
+ if self.heading_breadcrumbs
+ else None
+ )
+ adapter = queryAdapter(logo_context, IPathAdapter, "image")
if logo_context is not None:
return structured(
'<a href="%s">%s</a>',
- canonical_url(logo_context, rootsite='mainsite'),
- structured(adapter.logo())).escapedtext
+ canonical_url(logo_context, rootsite="mainsite"),
+ structured(adapter.logo()),
+ ).escapedtext
else:
return adapter.logo()
class ExceptionHierarchy(Hierarchy):
-
@property
def objects(self):
"""Return an empty list because the traversal is not safe or sane."""
@@ -546,78 +540,78 @@ class MaintenanceMessage:
toomuchtime = timedelta(seconds=1800) # 30 minutes
def __call__(self):
- if os.path.exists('+maintenancetime.txt'):
- with open('+maintenancetime.txt') as f:
+ if os.path.exists("+maintenancetime.txt"):
+ with open("+maintenancetime.txt") as f:
message = f.read()
try:
maintenancetime = parseDatetimetz(message)
except DateTimeError:
# XXX SteveAlexander 2005-09-22: log a warning here.
- return ''
+ return ""
timeleft = maintenancetime - utc_now()
if timeleft > self.toomuchtime:
- return ''
+ return ""
elif timeleft < self.notmuchtime:
- self.timelefttext = 'very very soon'
+ self.timelefttext = "very very soon"
else:
- self.timelefttext = 'in %s' % (
- DurationFormatterAPI(timeleft).approximateduration())
- self.featuretext = getFeatureFlag('app.maintenance_message')
+ self.timelefttext = "in %s" % (
+ DurationFormatterAPI(timeleft).approximateduration()
+ )
+ self.featuretext = getFeatureFlag("app.maintenance_message")
if self.timelefttext or self.featuretext:
return self.index()
- return ''
+ return ""
class LaunchpadRootFacets(StandardLaunchpadFacets):
usedfor = ILaunchpadRoot
enable_only = [
- 'overview',
- 'branches',
- 'bugs',
- 'specifications',
- 'translations',
- 'answers',
- ]
+ "overview",
+ "branches",
+ "bugs",
+ "specifications",
+ "translations",
+ "answers",
+ ]
def overview(self):
- target = ''
- text = 'Launchpad Home'
- return Link(target, text, site='mainsite')
+ target = ""
+ text = "Launchpad Home"
+ return Link(target, text, site="mainsite")
def translations(self):
- text = 'Translations'
- target = 'translations' if self.mainsite_only else ''
- site = 'mainsite' if self.mainsite_only else 'translations'
+ text = "Translations"
+ target = "translations" if self.mainsite_only else ""
+ site = "mainsite" if self.mainsite_only else "translations"
return Link(target, text, site=site)
def bugs(self):
- text = 'Bugs'
- target = 'bugs' if self.mainsite_only else ''
- site = 'mainsite' if self.mainsite_only else 'bugs'
+ text = "Bugs"
+ target = "bugs" if self.mainsite_only else ""
+ site = "mainsite" if self.mainsite_only else "bugs"
return Link(target, text, site=site)
def answers(self):
- text = 'Answers'
- target = 'questions' if self.mainsite_only else ''
- site = 'mainsite' if self.mainsite_only else 'answers'
+ text = "Answers"
+ target = "questions" if self.mainsite_only else ""
+ site = "mainsite" if self.mainsite_only else "answers"
return Link(target, text, site=site)
def specifications(self):
- text = 'Blueprints'
- target = 'specs' if self.mainsite_only else ''
- site = 'mainsite' if self.mainsite_only else 'blueprints'
+ text = "Blueprints"
+ target = "specs" if self.mainsite_only else ""
+ site = "mainsite" if self.mainsite_only else "blueprints"
return Link(target, text, site=site)
def branches(self):
- text = 'Code'
- target = '+code' if self.mainsite_only else ''
- site = 'mainsite' if self.mainsite_only else 'code'
+ text = "Code"
+ target = "+code" if self.mainsite_only else ""
+ site = "mainsite" if self.mainsite_only else "code"
return Link(target, text, site=site)
class LoginStatus:
-
def __init__(self, context, request):
self.context = context
self.request = request
@@ -625,8 +619,7 @@ class LoginStatus:
@property
def login_shown(self):
- return (self.user is None and
- '+login' not in self.request['PATH_INFO'])
+ return self.user is None and "+login" not in self.request["PATH_INFO"]
@property
def logged_in(self):
@@ -634,18 +627,18 @@ class LoginStatus:
@property
def login_url(self):
- query_string = self.request.get('QUERY_STRING', '')
+ query_string = self.request.get("QUERY_STRING", "")
# If we have a query string, remove some things we don't want, and
# keep it around.
if query_string:
query_dict = parse_qs(query_string, keep_blank_values=True)
- query_dict.pop('loggingout', None)
+ query_dict.pop("loggingout", None)
query_string = urlencode(sorted(query_dict.items()), doseq=True)
# If we still have a query_string after things we don't want
# have been removed, add it onto the url.
if query_string:
- query_string = '?' + query_string
+ query_string = "?" + query_string
# The approach we're taking is to combine the application url with
# the path_info, taking out path steps that are to do with virtual
@@ -661,70 +654,73 @@ class LoginStatus:
# We're going to use PATH_INFO to remove any spurious '+index' at the
# end of the URL. But, PATH_INFO will contain virtual hosting
# configuration, if there is any.
- path_info = self.request['PATH_INFO']
+ path_info = self.request["PATH_INFO"]
# Remove any virtual hosting segments.
path_steps = []
in_virtual_hosting_section = False
- for step in path_info.split('/'):
- if step.startswith('++vh++'):
+ for step in path_info.split("/"):
+ if step.startswith("++vh++"):
in_virtual_hosting_section = True
continue
- if step == '++':
+ if step == "++":
in_virtual_hosting_section = False
continue
if not in_virtual_hosting_section:
path_steps.append(step)
- path = '/'.join(path_steps)
+ path = "/".join(path_steps)
# Make the URL stop at the end of path_info so that we don't get
# spurious '+index' at the end.
- full_url = '%s%s' % (application_url, path)
- if full_url.endswith('/'):
+ full_url = "%s%s" % (application_url, path)
+ if full_url.endswith("/"):
full_url = full_url[:-1]
- logout_url_end = '/+logout'
- openid_callback_url_end = '/+openid-callback'
+ logout_url_end = "/+logout"
+ openid_callback_url_end = "/+openid-callback"
if full_url.endswith(logout_url_end):
- full_url = full_url[:-len(logout_url_end)]
+ full_url = full_url[: -len(logout_url_end)]
elif full_url.endswith(openid_callback_url_end):
- full_url = full_url[:-len(openid_callback_url_end)]
+ full_url = full_url[: -len(openid_callback_url_end)]
else:
# No need to remove anything from full_url.
pass
- return '%s/+login%s' % (full_url, query_string)
+ return "%s/+login%s" % (full_url, query_string)
class LaunchpadRootNavigation(Navigation):
usedfor = ILaunchpadRoot
- @stepto('support')
+ @stepto("support")
def redirect_support(self):
"""Redirect /support to launchpad Answers site."""
target_url = canonical_url(
- getUtility(ILaunchpadCelebrities).launchpad, rootsite='answers')
+ getUtility(ILaunchpadCelebrities).launchpad, rootsite="answers"
+ )
return self.redirectSubTree(target_url, status=301)
- @stepto('legal')
+ @stepto("legal")
def redirect_legal(self):
"""Redirect /legal to help.launchpad.net/Legal site."""
return self.redirectSubTree(
- 'https://help.launchpad.net/Legal', status=301)
+ "https://help.launchpad.net/Legal", status=301
+ )
- @stepto('faq')
+ @stepto("faq")
def redirect_faq(self):
"""Redirect /faq to launchpad-project/+faqs."""
return self.redirectSubTree(
- 'https://answers.launchpad.net/launchpad-project/+faqs',
- status=301)
+ "https://answers.launchpad.net/launchpad-project/+faqs", status=301
+ )
- @stepto('feedback')
+ @stepto("feedback")
def redirect_feedback(self):
"""Redirect /feedback to help.launchpad.net/Feedback site."""
return self.redirectSubTree(
- 'https://help.launchpad.net/Feedback', status=301)
+ "https://help.launchpad.net/Feedback", status=301
+ )
- @stepto('+branch')
+ @stepto("+branch")
def redirect_branch(self):
"""Redirect /+branch/<foo> to the branch named 'foo'.
@@ -741,12 +737,12 @@ class LaunchpadRootNavigation(Navigation):
# (in the case that there is an error resolving the branch url).
# Note: the http referer may be None if someone has hacked a url
# directly rather than following a /+branch/<foo> link.
- target_url = self.request.getHeader('referer')
- path = '/'.join(self.request.stepstogo)
+ target_url = self.request.getHeader("referer")
+ path = "/".join(self.request.stepstogo)
try:
branch, trailing = getUtility(IBranchLookup).getByLPPath(path)
target_url = canonical_url(branch, request=self.request)
- if trailing != '':
+ if trailing != "":
target_url = urlappend(target_url, trailing)
except NoLinkedBranch:
# A valid ICanHasLinkedBranch target exists but there's no
@@ -759,9 +755,14 @@ class LaunchpadRootNavigation(Navigation):
if target_url is None:
raise NotFoundError
self.request.response.addNotification(
- "The target %s does not have a linked branch." % path)
- except (CannotHaveLinkedBranch, InvalidNamespace,
- InvalidProductName, NotFoundError) as e:
+ "The target %s does not have a linked branch." % path
+ )
+ except (
+ CannotHaveLinkedBranch,
+ InvalidNamespace,
+ InvalidProductName,
+ NotFoundError,
+ ) as e:
# If are aren't arriving at this invalid branch URL from another
# page then we just raise a NotFoundError to generate a 404,
# otherwise we end up in a bad recursion loop. The target url will
@@ -769,13 +770,13 @@ class LaunchpadRootNavigation(Navigation):
if target_url is None:
raise NotFoundError
error_msg = str(e)
- if error_msg == '':
+ if error_msg == "":
error_msg = "Invalid branch lp:%s." % path
self.request.response.addErrorNotification(error_msg)
return self.redirectSubTree(target_url)
- @stepto('+code')
+ @stepto("+code")
def redirect_branch_or_repo(self):
"""Redirect /+code/<foo> to the branch or repository named 'foo'.
@@ -788,7 +789,7 @@ class LaunchpadRootNavigation(Navigation):
Unlike +branch, this works for both git and bzr repositories/branches.
"""
target_url = None
- path = '/'.join(self.request.stepstogo)
+ path = "/".join(self.request.stepstogo)
# Try a Git repository lookup first, since the schema is simpler and
# so it's quicker.
@@ -798,8 +799,12 @@ class LaunchpadRootNavigation(Navigation):
target_url = canonical_url(repository, request=self.request)
if trailing:
target_url = urlappend(target_url, trailing)
- except (InvalidNamespace, InvalidProductName, NameLookupFailed,
- Unauthorized):
+ except (
+ InvalidNamespace,
+ InvalidProductName,
+ NameLookupFailed,
+ Unauthorized,
+ ):
# Either the git repository wasn't found, or it was found but we
# lack authority to access it. In either case, attempt a bzr lookup
# so don't set target_url
@@ -809,7 +814,7 @@ class LaunchpadRootNavigation(Navigation):
try:
branch, trailing = getUtility(IBranchLookup).getByLPPath(path)
bzr_url = canonical_url(branch, request=self.request)
- if trailing != '':
+ if trailing != "":
bzr_url = urlappend(bzr_url, trailing)
if target_url and branch.product is not None:
@@ -823,8 +828,13 @@ class LaunchpadRootNavigation(Navigation):
target_url = bzr_url
else:
target_url = bzr_url
- except (NoLinkedBranch, CannotHaveLinkedBranch, InvalidNamespace,
- InvalidProductName, NotFoundError):
+ except (
+ NoLinkedBranch,
+ CannotHaveLinkedBranch,
+ InvalidNamespace,
+ InvalidProductName,
+ NotFoundError,
+ ):
# No bzr branch found either.
pass
@@ -837,74 +847,76 @@ class LaunchpadRootNavigation(Navigation):
return self.redirectSubTree(target_url)
- @stepto('+builds')
+ @stepto("+builds")
def redirect_buildfarm(self):
"""Redirect old /+builds requests to new URL, /builders."""
- new_url = '/builders'
+ new_url = "/builders"
return self.redirectSubTree(
- urlappend(new_url, '/'.join(self.request.stepstogo)))
+ urlappend(new_url, "/".join(self.request.stepstogo))
+ )
# XXX cprov 2009-03-19 bug=345877: path segments starting with '+'
# should never correspond to a valid traversal, they confuse the
# hierarchical navigation model.
stepto_utilities = {
- '+announcements': IAnnouncementSet,
- 'archives': IArchiveSet,
- '+services': IServiceFactory,
- 'binarypackagenames': IBinaryPackageNameSet,
- 'branches': IBranchSet,
- 'bugs': IMaloneApplication,
- 'builders': IBuilderSet,
- '+charm-bases': ICharmBaseSet,
- '+charm-recipes': ICharmRecipeSet,
- '+code-index': IBazaarApplication,
- '+code-imports': ICodeImportSet,
- 'codeofconduct': ICodeOfConductSet,
- '+countries': ICountrySet,
- 'distros': IDistributionSet,
- '+git': IGitRepositorySet,
- 'karmaaction': IKarmaActionSet,
- '+imports': ITranslationImportQueue,
- '+languages': ILanguageSet,
- 'livefses': ILiveFSSet,
- '+nameblacklist': INameBlacklistSet,
- 'package-sets': IPackagesetSet,
- 'people': IPersonSet,
- 'pillars': IPillarNameSet,
- '+polls': IPollSet,
- '+processors': IProcessorSet,
- 'projects': IProductSet,
- 'projectgroups': IProjectGroupSet,
- '+snaps': ISnapSet,
- '+snap-bases': ISnapBaseSet,
- '+snappy-series': ISnappySeriesSet,
- 'sourcepackagenames': ISourcePackageNameSet,
- 'specs': ISpecificationSet,
- 'sprints': ISprintSet,
- '+statistics': ILaunchpadStatisticSet,
- 'token': ILoginTokenSet,
- '+groups': ITranslationGroupSet,
- 'translations': IRosettaApplication,
- 'testopenid': ITestOpenIDApplication,
- 'questions': IQuestionSet,
- 'temporary-blobs': ITemporaryStorageManager,
+ "+announcements": IAnnouncementSet,
+ "archives": IArchiveSet,
+ "+services": IServiceFactory,
+ "binarypackagenames": IBinaryPackageNameSet,
+ "branches": IBranchSet,
+ "bugs": IMaloneApplication,
+ "builders": IBuilderSet,
+ "+charm-bases": ICharmBaseSet,
+ "+charm-recipes": ICharmRecipeSet,
+ "+code-index": IBazaarApplication,
+ "+code-imports": ICodeImportSet,
+ "codeofconduct": ICodeOfConductSet,
+ "+countries": ICountrySet,
+ "distros": IDistributionSet,
+ "+git": IGitRepositorySet,
+ "karmaaction": IKarmaActionSet,
+ "+imports": ITranslationImportQueue,
+ "+languages": ILanguageSet,
+ "livefses": ILiveFSSet,
+ "+nameblacklist": INameBlacklistSet,
+ "package-sets": IPackagesetSet,
+ "people": IPersonSet,
+ "pillars": IPillarNameSet,
+ "+polls": IPollSet,
+ "+processors": IProcessorSet,
+ "projects": IProductSet,
+ "projectgroups": IProjectGroupSet,
+ "+snaps": ISnapSet,
+ "+snap-bases": ISnapBaseSet,
+ "+snappy-series": ISnappySeriesSet,
+ "sourcepackagenames": ISourcePackageNameSet,
+ "specs": ISpecificationSet,
+ "sprints": ISprintSet,
+ "+statistics": ILaunchpadStatisticSet,
+ "token": ILoginTokenSet,
+ "+groups": ITranslationGroupSet,
+ "translations": IRosettaApplication,
+ "testopenid": ITestOpenIDApplication,
+ "questions": IQuestionSet,
+ "temporary-blobs": ITemporaryStorageManager,
# These three have been renamed, and no redirects done, as the old
# urls now point to the product pages.
#'bazaar': IBazaarApplication,
#'malone': IMaloneApplication,
#'rosetta': IRosettaApplication,
- }
+ }
- @stepto('products')
+ @stepto("products")
def products(self):
return self.redirectSubTree(
- canonical_url(getUtility(IProductSet)), status=301)
+ canonical_url(getUtility(IProductSet)), status=301
+ )
def traverse(self, name):
if name in self.stepto_utilities:
return getUtility(self.stepto_utilities[name])
- if name == '~':
+ if name == "~":
person = getUtility(ILaunchBag).user
if person is None:
raise Unauthorized()
@@ -912,17 +924,20 @@ class LaunchpadRootNavigation(Navigation):
# bugs.l.n/~/+assignedbugs goes to the person's canonical
# assigned list.
return self.redirectSubTree(
- canonical_url(self.context) + "~"
+ canonical_url(self.context)
+ + "~"
+ canonical_name(person.name),
- status=302)
- elif name.startswith('~'): # Allow traversal to ~foo for People
+ status=302,
+ )
+ elif name.startswith("~"): # Allow traversal to ~foo for People
if canonical_name(name) != name:
# (for instance, uppercase username?)
- if self.request.method == 'POST':
+ if self.request.method == "POST":
raise POSTToNonCanonicalURL
return self.redirectSubTree(
canonical_url(self.context) + canonical_name(name),
- status=301)
+ status=301,
+ )
else:
person = getUtility(IPersonSet).getByName(name[1:])
if person is None:
@@ -930,16 +945,16 @@ class LaunchpadRootNavigation(Navigation):
# Check to see if this is a team, and if so, whether the
# logged in user is allowed to view the team, by virtue of
# team membership or Launchpad administration.
- if (person.is_team and
- not check_permission('launchpad.LimitedView', person)):
+ if person.is_team and not check_permission(
+ "launchpad.LimitedView", person
+ ):
return None
# Only admins are permitted to see suspended users.
if person.account_status == AccountStatus.SUSPENDED:
- if not check_permission('launchpad.Moderate', person):
- raise GoneError(
- 'User is suspended: %s' % name)
+ if not check_permission("launchpad.Moderate", person):
+ raise GoneError("User is suspended: %s" % name)
if person.account_status == AccountStatus.PLACEHOLDER:
- if not check_permission('launchpad.Moderate', person):
+ if not check_permission("launchpad.Moderate", person):
return None
return person
@@ -949,20 +964,24 @@ class LaunchpadRootNavigation(Navigation):
# will break much sooner than that) or updates sent to
# {dapper,edgy}-updates. Probably all irrelevant, as I suspect the
# number of people using the plugin in edgy and dapper is 0.
- if name == 'bazaar' and IXMLRPCRequest.providedBy(self.request):
+ if name == "bazaar" and IXMLRPCRequest.providedBy(self.request):
return getUtility(IBazaarApplication)
# account for common typing mistakes
if canonical_name(name) != name:
- if self.request.method == 'POST':
+ if self.request.method == "POST":
raise POSTToNonCanonicalURL
return self.redirectSubTree(
- (canonical_url(self.context, request=self.request) +
- canonical_name(name)),
- status=301)
+ (
+ canonical_url(self.context, request=self.request)
+ + canonical_name(name)
+ ),
+ status=301,
+ )
pillar = getUtility(IPillarNameSet).getByName(
- name, ignore_inactive=False)
+ name, ignore_inactive=False
+ )
if pillar is None:
return None
@@ -986,30 +1005,34 @@ class LaunchpadRootNavigation(Navigation):
if user is None:
return None
user = IPersonRoles(user)
- if (not user.in_commercial_admin and not user.in_admin and
- not user.in_registry_experts):
+ if (
+ not user.in_commercial_admin
+ and not user.in_admin
+ and not user.in_registry_experts
+ ):
return None
- if check_permission('launchpad.LimitedView', pillar):
+ if check_permission("launchpad.LimitedView", pillar):
if pillar.name != name:
# This pillar was accessed through one of its aliases, so we
# must redirect to its canonical URL.
return self.redirectSubTree(
- canonical_url(pillar, self.request), status=301)
+ canonical_url(pillar, self.request), status=301
+ )
return pillar
return None
def _getBetaRedirectionView(self):
# If the inhibit_beta_redirect cookie is set, don't redirect.
- if self.request.cookies.get('inhibit_beta_redirect', '0') == '1':
+ if self.request.cookies.get("inhibit_beta_redirect", "0") == "1":
return None
# If we are looking at the front page, don't redirect.
- if self.request['PATH_INFO'] == '/':
+ if self.request["PATH_INFO"] == "/":
return None
# If this is a HTTP POST, we don't want to issue a redirect.
# Doing so would go against the HTTP standard.
- if self.request.method == 'POST':
+ if self.request.method == "POST":
return None
# If this is a web service request, don't redirect.
@@ -1017,7 +1040,7 @@ class LaunchpadRootNavigation(Navigation):
return None
# If the request is for a bug then redirect straight to that bug.
- bug_match = re.match(r"/bugs/(\d+)$", self.request['PATH_INFO'])
+ bug_match = re.match(r"/bugs/(\d+)$", self.request["PATH_INFO"])
if bug_match:
bug_number = bug_match.group(1)
bug_set = getUtility(IBugSet)
@@ -1030,8 +1053,9 @@ class LaunchpadRootNavigation(Navigation):
# Empty the traversal stack, since we're redirecting.
self.request.setTraversalStack([])
# And perform a temporary redirect.
- return RedirectionView(canonical_url(bug.default_bugtask),
- self.request, status=303)
+ return RedirectionView(
+ canonical_url(bug.default_bugtask), self.request, status=303
+ )
# Explicit catchall - do not redirect.
return None
@@ -1043,19 +1067,19 @@ class LaunchpadRootNavigation(Navigation):
class SoftTimeoutView(LaunchpadView):
-
def __call__(self):
"""Generate a soft timeout by sleeping enough time."""
start_time = time.time()
celebrities = getUtility(ILaunchpadCelebrities)
- if (self.user is None or
- not self.user.inTeam(celebrities.launchpad_developers)):
+ if self.user is None or not self.user.inTeam(
+ celebrities.launchpad_developers
+ ):
raise Unauthorized
- self.request.response.setHeader('content-type', 'text/plain')
+ self.request.response.setHeader("content-type", "text/plain")
soft_timeout = intOrZero(config.database.soft_request_timeout)
if soft_timeout == 0:
- return 'No soft timeout threshold is set.'
+ return "No soft timeout threshold is set."
time.sleep(soft_timeout / 1000.0)
time_to_generate_page = (time.time() - start_time) * 1000
@@ -1065,8 +1089,9 @@ class SoftTimeoutView(LaunchpadView):
time.sleep(0.1)
time_to_generate_page = (time.time() - start_time) * 1000
return (
- 'Soft timeout threshold is set to %s ms. This page took'
- ' %s ms to render.' % (soft_timeout, time_to_generate_page))
+ "Soft timeout threshold is set to %s ms. This page took"
+ " %s ms to render." % (soft_timeout, time_to_generate_page)
+ )
class IcingFolder(ExportedFolder):
@@ -1074,16 +1099,13 @@ class IcingFolder(ExportedFolder):
export_subdirectories = True
- folder = os.path.join(
- config.root, 'lib/canonical/launchpad/icing/')
+ folder = os.path.join(config.root, "lib/canonical/launchpad/icing/")
class LaunchpadImageFolder(ExportedImageFolder):
- """Export the Launchpad images - supporting retrieval without extension.
- """
+ """Export the Launchpad images - supporting retrieval without extension."""
- folder = os.path.join(
- config.root, 'lib/canonical/launchpad/images/')
+ folder = os.path.join(config.root, "lib/canonical/launchpad/images/")
class LaunchpadTourFolder(ExportedFolder):
@@ -1093,7 +1115,8 @@ class LaunchpadTourFolder(ExportedFolder):
"""
folder = os.path.join(
- os.path.dirname(os.path.realpath(__file__)), '../tour/')
+ os.path.dirname(os.path.realpath(__file__)), "../tour/"
+ )
export_subdirectories = True
@@ -1103,16 +1126,21 @@ class LaunchpadTourFolder(ExportedFolder):
The source directory contains source material that we don't want
published over the web.
"""
- if name == 'source':
+ if name == "source":
raise NotFound(request, name)
return super().publishTraverse(request, name)
def browserDefault(self, request):
"""Redirect to index.html if the directory itself is requested."""
if len(self.names) == 0:
- return RedirectionView(
- "%s+tour/index" % canonical_url(self.context),
- self.request, status=302), ()
+ return (
+ RedirectionView(
+ "%s+tour/index" % canonical_url(self.context),
+ self.request,
+ status=302,
+ ),
+ (),
+ )
else:
return self, ()
@@ -1120,13 +1148,12 @@ class LaunchpadTourFolder(ExportedFolder):
class LaunchpadAPIDocFolder(ExportedFolder):
"""Export the API documentation."""
- folder = os.path.join(
- config.root, 'lib/canonical/launchpad/apidoc/')
+ folder = os.path.join(config.root, "lib/canonical/launchpad/apidoc/")
def browserDefault(self, request):
"""Traverse to index.html if the directory itself is requested."""
if len(self.names) == 0:
- return self, ('index.html', )
+ return self, ("index.html",)
else:
return self, ()
@@ -1134,10 +1161,13 @@ class LaunchpadAPIDocFolder(ExportedFolder):
class IAppFrontPageSearchForm(Interface):
"""Schema for the app-specific front page search question forms."""
- search_text = TextLine(title=_('Search text'), required=False)
+ search_text = TextLine(title=_("Search text"), required=False)
- scope = Choice(title=_('Search scope'), required=False,
- vocabulary='DistributionOrProductOrProjectGroup')
+ scope = Choice(
+ title=_("Search scope"),
+ required=False,
+ vocabulary="DistributionOrProductOrProjectGroup",
+ )
class AppFrontPageSearchView(LaunchpadFormView):
@@ -1149,14 +1179,14 @@ class AppFrontPageSearchView(LaunchpadFormView):
def scope_css_class(self):
"""The CSS class for used in the scope widget."""
if self.scope_error:
- return 'error'
+ return "error"
else:
return None
@property
def scope_error(self):
"""The error message for the scope widget."""
- return self.getFieldError('scope')
+ return self.getFieldError("scope")
def get_launchpad_views(cookies):
@@ -1166,13 +1196,13 @@ def get_launchpad_views(cookies):
:return: A dict of all the view states.
"""
views = {
- 'small_maps': True,
- }
- cookie = cookies.get('launchpad_views', '')
+ "small_maps": True,
+ }
+ cookie = cookies.get("launchpad_views", "")
if len(cookie) > 0:
- pairs = cookie.split('&')
+ pairs = cookie.split("&")
for pair in pairs:
- parts = pair.split('=')
+ parts = pair.split("=")
if len(parts) != 2:
# The cookie is malformed, possibly hacked.
continue
@@ -1182,7 +1212,7 @@ def get_launchpad_views(cookies):
continue
# 'false' is the value that the browser script sets to disable a
# part of a page. Any other value is considered to be 'true'.
- views[key] = value != 'false'
+ views[key] = value != "false"
return views
diff --git a/lib/lp/app/browser/launchpadform.py b/lib/lp/app/browser/launchpadform.py
index 220b013..201c948 100644
--- a/lib/lp/app/browser/launchpadform.py
+++ b/lib/lp/app/browser/launchpadform.py
@@ -5,43 +5,34 @@
"""
__all__ = [
- 'action',
- 'has_structured_doc',
- 'LaunchpadEditFormView',
- 'LaunchpadFormView',
- 'render_radio_widget_part',
- 'ReturnToReferrerMixin',
- 'safe_action',
- ]
+ "action",
+ "has_structured_doc",
+ "LaunchpadEditFormView",
+ "LaunchpadFormView",
+ "render_radio_widget_part",
+ "ReturnToReferrerMixin",
+ "safe_action",
+]
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
import simplejson
import transaction
+from lazr.lifecycle.event import ObjectModifiedEvent
+from lazr.lifecycle.snapshot import Snapshot
from zope.event import notify
from zope.formlib import form
+
# imported so it may be exported
from zope.formlib.form import action
-from zope.formlib.interfaces import (
- IInputWidget,
- IWidgetFactory,
- )
+from zope.formlib.interfaces import IInputWidget, IWidgetFactory
from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import (
CheckBoxWidget,
DropdownWidget,
RadioWidget,
TextAreaWidget,
- )
-from zope.interface import (
- classImplements,
- implementer,
- providedBy,
- )
-from zope.traversing.interfaces import (
- ITraversable,
- TraversalError,
- )
+)
+from zope.interface import classImplements, implementer, providedBy
+from zope.traversing.interfaces import ITraversable, TraversalError
from lp.services.webapp.escaping import html_escape
from lp.services.webapp.interfaces import (
@@ -50,12 +41,8 @@ from lp.services.webapp.interfaces import (
IMultiLineWidgetLayout,
INotificationResponse,
UnsafeFormGetSubmissionError,
- )
-from lp.services.webapp.publisher import (
- canonical_url,
- LaunchpadView,
- )
-
+)
+from lp.services.webapp.publisher import LaunchpadView, canonical_url
classImplements(CheckBoxWidget, ICheckBoxWidgetLayout)
classImplements(DropdownWidget, IAlwaysSubmittedWidget)
@@ -70,7 +57,7 @@ _first_widget_marker = object()
class LaunchpadFormView(LaunchpadView):
# The prefix used for all form inputs.
- prefix = 'field'
+ prefix = "field"
# The form schema
schema = None
@@ -88,7 +75,7 @@ class LaunchpadFormView(LaunchpadView):
# to disable setting of initial focus.
initial_focus_widget = _first_widget_marker
- label = ''
+ label = ""
actions = ()
@@ -114,7 +101,8 @@ class LaunchpadFormView(LaunchpadView):
data = {}
errors, form_action = form.handleSubmit(
- self.actions, data, self._validate)
+ self.actions, data, self._validate
+ )
# no action selected, so return
if form_action is None:
@@ -122,8 +110,8 @@ class LaunchpadFormView(LaunchpadView):
# Check to see if an attempt was made to submit a non-safe
# action with a GET query.
- is_safe = getattr(form_action, 'is_safe', False)
- if not is_safe and self.request.method != 'POST':
+ is_safe = getattr(form_action, "is_safe", False)
+ if not is_safe and self.request.method != "POST":
raise UnsafeFormGetSubmissionError(form_action.__name__)
if errors:
@@ -144,11 +132,14 @@ class LaunchpadFormView(LaunchpadView):
"""Add any notification messages to the response headers."""
if not INotificationResponse.providedBy(request.response):
return
- notifications = ([(notification.level, notification.message)
- for notification in request.response.notifications])
+ notifications = [
+ (notification.level, notification.message)
+ for notification in request.response.notifications
+ ]
if notifications:
request.response.setHeader(
- 'X-Lazr-Notifications', simplejson.dumps(notifications))
+ "X-Lazr-Notifications", simplejson.dumps(notifications)
+ )
def render(self):
"""Return the body of the response.
@@ -181,10 +172,14 @@ class LaunchpadFormView(LaunchpadView):
pass
def setUpFields(self):
- assert self.schema is not None, (
- "Schema must be set for LaunchpadFormView")
- self.form_fields = form.Fields(self.schema, for_input=self.for_input,
- render_context=self.render_context)
+ assert (
+ self.schema is not None
+ ), "Schema must be set for LaunchpadFormView"
+ self.form_fields = form.Fields(
+ self.schema,
+ for_input=self.for_input,
+ render_context=self.render_context,
+ )
self.extendFields()
if self.field_names is not None:
self.form_fields = self.form_fields.select(*self.field_names)
@@ -198,7 +193,8 @@ class LaunchpadFormView(LaunchpadView):
# important for some existing forms.
if field.custom_widget is None:
widget = getattr(
- self, 'custom_widget_%s' % field.__name__, None)
+ self, "custom_widget_%s" % field.__name__, None
+ )
if widget is not None:
if IWidgetFactory.providedBy(widget):
field.custom_widget = widget
@@ -208,9 +204,14 @@ class LaunchpadFormView(LaunchpadView):
if context is None:
context = self.context
self.widgets = form.setUpWidgets(
- self.form_fields, self.prefix, context, self.request,
- data=self.initial_values, adapters=self.adapters,
- ignore_request=False)
+ self.form_fields,
+ self.prefix,
+ context,
+ self.request,
+ data=self.initial_values,
+ adapters=self.adapters,
+ ignore_request=False,
+ )
for field_name, help_link in self.help_links.items():
self.widgets[field_name].help_link = help_link
@@ -329,7 +330,8 @@ class LaunchpadFormView(LaunchpadView):
for error in form.getWidgetsData(widgets, self.prefix, data):
self.errors.append(error)
for error in form.checkInvariants(
- self.form_fields, data, self.invariant_context):
+ self.form_fields, data, self.invariant_context
+ ):
self.addError(error)
return self.errors
@@ -355,11 +357,11 @@ class LaunchpadFormView(LaunchpadView):
count += 1
if count == 0:
- return ''
+ return ""
elif count == 1:
- return 'There is 1 error.'
+ return "There is 1 error."
else:
- return 'There are %d errors.' % count
+ return "There are %d errors." % count
def ajax_failure_handler(self, action, data, errors):
"""Called by the form if validate() finds any errors.
@@ -372,7 +374,7 @@ class LaunchpadFormView(LaunchpadView):
if not self.request.is_ajax:
return
self.request.response.setStatus(400, "Validation")
- self.request.response.setHeader('Content-type', 'application/json')
+ self.request.response.setHeader("Content-type", "application/json")
errors = {}
for widget in self.widgets:
widget_error = self.getFieldError(widget.context.getName())
@@ -381,7 +383,8 @@ class LaunchpadFormView(LaunchpadView):
return_data = dict(
form_wide_errors=self.form_wide_errors,
errors=errors,
- error_summary=self.error_count)
+ error_summary=self.error_count,
+ )
return simplejson.dumps(return_data)
def validate(self, data):
@@ -420,16 +423,16 @@ class LaunchpadFormView(LaunchpadView):
widget = None
if widget is None:
- return ''
+ return ""
else:
- return ("<!--\n"
- "setFocusByName('%s');\n"
- "// -->" % widget.name)
+ return "<!--\n" "setFocusByName('%s');\n" "// -->" % widget.name
def isSingleLineLayout(self, field_name):
widget = self.widgets[field_name]
- return not (IMultiLineWidgetLayout.providedBy(widget) or
- ICheckBoxWidgetLayout.providedBy(widget))
+ return not (
+ IMultiLineWidgetLayout.providedBy(widget)
+ or ICheckBoxWidgetLayout.providedBy(widget)
+ )
def isMultiLineLayout(self, field_name):
widget = self.widgets[field_name]
@@ -437,8 +440,9 @@ class LaunchpadFormView(LaunchpadView):
def isCheckBoxLayout(self, field_name):
widget = self.widgets[field_name]
- return (ICheckBoxWidgetLayout.providedBy(widget) and
- not IMultiLineWidgetLayout.providedBy(widget))
+ return ICheckBoxWidgetLayout.providedBy(
+ widget
+ ) and not IMultiLineWidgetLayout.providedBy(widget)
def showOptionalMarker(self, field_name):
"""Should the (Optional) marker be shown?"""
@@ -449,14 +453,15 @@ class LaunchpadFormView(LaunchpadView):
return False
# Do not show for readonly fields.
- context = getattr(widget, 'context', None)
- if getattr(context, 'readonly', None):
+ context = getattr(widget, "context", None)
+ if getattr(context, "readonly", None):
return False
# Do not show the marker for required widgets or always submitted
# widgets. Everything else gets the marker.
- return not (widget.required or
- IAlwaysSubmittedWidget.providedBy(widget))
+ return not (
+ widget.required or IAlwaysSubmittedWidget.providedBy(widget)
+ )
class LaunchpadEditFormView(LaunchpadFormView):
@@ -479,15 +484,21 @@ class LaunchpadEditFormView(LaunchpadFormView):
context = self.context
if notify_modified:
context_before_modification = Snapshot(
- context, providing=providedBy(context))
+ context, providing=providedBy(context)
+ )
- was_changed = form.applyChanges(context, self.form_fields,
- data, self.adapters)
+ was_changed = form.applyChanges(
+ context, self.form_fields, data, self.adapters
+ )
if was_changed and notify_modified:
- field_names = [form_field.__name__
- for form_field in self.form_fields]
- notify(ObjectModifiedEvent(
- context, context_before_modification, field_names))
+ field_names = [
+ form_field.__name__ for form_field in self.form_fields
+ ]
+ notify(
+ ObjectModifiedEvent(
+ context, context_before_modification, field_names
+ )
+ )
return was_changed
@@ -532,23 +543,27 @@ class ReturnToReferrerMixin:
"""See `LaunchpadFormView`."""
# The referer header we want is only available before the view's
# form submits to itself. This field is a hidden input in the form.
- referrer = self.request.form.get('_return_url')
+ referrer = self.request.form.get("_return_url")
returnNotChanged = True
if referrer is None:
# "referer" is misspelled in the HTTP specification.
- referrer = self.request.getHeader('referer')
+ referrer = self.request.getHeader("referer")
else:
- attribute_name = self.request.form.get('_return_attribute_name')
- attribute_value = self.request.form.get('_return_attribute_value')
- if (attribute_name is not None
+ attribute_name = self.request.form.get("_return_attribute_name")
+ attribute_value = self.request.form.get("_return_attribute_value")
+ if (
+ attribute_name is not None
and attribute_value is not None
- and getattr(self.context, attribute_name) != attribute_value):
+ and getattr(self.context, attribute_name) != attribute_value
+ ):
returnNotChanged = False
- if (referrer is not None
+ if (
+ referrer is not None
and returnNotChanged
and referrer.startswith(self.request.getApplicationURL())
- and referrer != self.request.getHeader('location')):
+ and referrer != self.request.getHeader("location")
+ ):
return referrer
else:
return canonical_url(self.context)
@@ -559,7 +574,7 @@ class ReturnToReferrerMixin:
def has_structured_doc(field):
"""Set an annotation to mark that the field's doc should be structured."""
- field.setTaggedValue('has_structured_doc', True)
+ field.setTaggedValue("has_structured_doc", True)
return field
@@ -575,13 +590,14 @@ class WidgetHasStructuredDoc:
self.widget = widget
def traverse(self, name, furtherPath):
- if name != 'has-structured-doc':
+ if name != "has-structured-doc":
raise TraversalError("Unknown query %r" % name)
if len(furtherPath) > 0:
raise TraversalError(
"There should be no further path segments after "
- "query:has-structured-doc")
- return self.widget.context.queryTaggedValue('has_structured_doc')
+ "query:has-structured-doc"
+ )
+ return self.widget.context.queryTaggedValue("has_structured_doc")
def render_radio_widget_part(widget, term_value, current_value, label=None):
@@ -599,5 +615,9 @@ def render_radio_widget_part(widget, term_value, current_value, label=None):
label = term.title
value = term.token
return render(
- index=term.value, text=label, value=value, name=widget.name,
- cssClass='')
+ index=term.value,
+ text=label,
+ value=value,
+ name=widget.name,
+ cssClass="",
+ )
diff --git a/lib/lp/app/browser/lazrjs.py b/lib/lp/app/browser/lazrjs.py
index c48cc09..23a6bc0 100644
--- a/lib/lp/app/browser/lazrjs.py
+++ b/lib/lp/app/browser/lazrjs.py
@@ -4,38 +4,32 @@
"""Wrappers for lazr-js widgets."""
__all__ = [
- 'BooleanChoiceWidget',
- 'EnumChoiceWidget',
- 'InlineEditPickerWidget',
- 'InlinePersonEditPickerWidget',
- 'InlineMultiCheckboxWidget',
- 'standard_text_html_representation',
- 'TextAreaEditorWidget',
- 'TextLineEditorWidget',
- 'vocabulary_to_choice_edit_items',
- ]
+ "BooleanChoiceWidget",
+ "EnumChoiceWidget",
+ "InlineEditPickerWidget",
+ "InlinePersonEditPickerWidget",
+ "InlineMultiCheckboxWidget",
+ "standard_text_html_representation",
+ "TextAreaEditorWidget",
+ "TextLineEditorWidget",
+ "vocabulary_to_choice_edit_items",
+]
+import simplejson
from lazr.enum import IEnumeratedType
from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
from lazr.restful.utils import get_current_browser_request
-import simplejson
from zope.browserpage import ViewPageTemplateFile
from zope.component import getUtility
-from zope.schema.interfaces import (
- ICollection,
- IVocabulary,
- )
+from zope.schema.interfaces import ICollection, IVocabulary
from zope.schema.vocabulary import getVocabularyRegistry
-from zope.security.checker import (
- canAccess,
- canWrite,
- )
+from zope.security.checker import canAccess, canWrite
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.browser.vocabulary import (
get_person_picker_entry_metadata,
vocabulary_filters,
- )
+)
from lp.services.propertycache import cachedproperty
from lp.services.webapp.interfaces import ILaunchBag
from lp.services.webapp.publisher import canonical_url
@@ -45,8 +39,15 @@ from lp.services.webapp.vocabulary import IHugeVocabulary
class WidgetBase:
"""Useful methods for all widgets."""
- def __init__(self, context, exported_field, content_box_id,
- edit_view, edit_url, edit_title):
+ def __init__(
+ self,
+ context,
+ exported_field,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ ):
self.context = context
self.exported_field = exported_field
@@ -62,7 +63,7 @@ class WidgetBase:
edit_url = canonical_url(self.context, view_name=edit_view)
self.edit_url = edit_url
if edit_title is None:
- edit_title = ''
+ edit_title = ""
self.edit_title = edit_title
# The mutator method name is used to determine whether or not the
@@ -75,8 +76,8 @@ class WidgetBase:
# about.
self.api_attribute = self.attribute_name
else:
- self.api_attribute = ws_stack['as']
- mutator_info = ws_stack.get('mutator_annotations')
+ self.api_attribute = ws_stack["as"]
+ mutator_info = ws_stack.get("mutator_annotations")
if mutator_info is not None:
mutator_method, mutator_extra = mutator_info
self.mutator_method_name = mutator_method.__name__
@@ -113,18 +114,31 @@ class WidgetBase:
class TextWidgetBase(WidgetBase):
"""Abstract base for the single and multiline text editor widgets."""
- def __init__(self, context, exported_field, title, content_box_id,
- edit_view, edit_url, edit_title):
+ def __init__(
+ self,
+ context,
+ exported_field,
+ title,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ ):
super().__init__(
- context, exported_field, content_box_id,
- edit_view, edit_url, edit_title)
+ context,
+ exported_field,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ )
self.accept_empty = simplejson.dumps(self.optional_field)
self.title = title
- self.widget_css_selector = simplejson.dumps('#' + self.content_box_id)
+ self.widget_css_selector = simplejson.dumps("#" + self.content_box_id)
@property
def json_attribute_uri(self):
- return simplejson.dumps(self.resource_uri + '/' + self.api_attribute)
+ return simplejson.dumps(self.resource_uri + "/" + self.api_attribute)
class DefinedTagMixin:
@@ -134,24 +148,40 @@ class DefinedTagMixin:
def open_tag(self):
if self.css_class:
return '<%s id="%s" class="%s">' % (
- self.tag, self.content_box_id, self.css_class)
+ self.tag,
+ self.content_box_id,
+ self.css_class,
+ )
else:
return '<%s id="%s">' % (self.tag, self.content_box_id)
@property
def close_tag(self):
- return '</%s>' % self.tag
+ return "</%s>" % self.tag
class TextLineEditorWidget(TextWidgetBase, DefinedTagMixin):
"""Wrapper for the lazr-js inlineedit/editor.js widget."""
- __call__ = ViewPageTemplateFile('../templates/text-line-editor.pt')
-
- def __init__(self, context, exported_field, title, tag, css_class=None,
- content_box_id=None, edit_view="+edit", edit_url=None,
- edit_title='', max_width=None, truncate_lines=0,
- default_text=None, initial_value_override=None, width=None):
+ __call__ = ViewPageTemplateFile("../templates/text-line-editor.pt")
+
+ def __init__(
+ self,
+ context,
+ exported_field,
+ title,
+ tag,
+ css_class=None,
+ content_box_id=None,
+ edit_view="+edit",
+ edit_url=None,
+ edit_title="",
+ max_width=None,
+ truncate_lines=0,
+ default_text=None,
+ initial_value_override=None,
+ width=None,
+ ):
"""Create a widget wrapper.
:param context: The object that is being edited.
@@ -178,8 +208,14 @@ class TextLineEditorWidget(TextWidgetBase, DefinedTagMixin):
:param width: Initial widget width.
"""
super().__init__(
- context, exported_field, title, content_box_id,
- edit_view, edit_url, edit_title)
+ context,
+ exported_field,
+ title,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ )
self.tag = tag
self.css_class = css_class
self.max_width = max_width
@@ -200,26 +236,35 @@ class TextLineEditorWidget(TextWidgetBase, DefinedTagMixin):
def text_css_class(self):
clazz = "yui3-editable_text-text"
if self.truncate_lines and self.truncate_lines > 0:
- clazz += ' ellipsis'
+ clazz += " ellipsis"
if self.truncate_lines == 1:
- clazz += ' single-line'
+ clazz += " single-line"
return clazz
@property
def text_css_style(self):
if self.max_width:
- return 'max-width: %s;' % self.max_width
- return ''
+ return "max-width: %s;" % self.max_width
+ return ""
class TextAreaEditorWidget(TextWidgetBase):
"""Wrapper for the multine-line lazr-js inlineedit/editor.js widget."""
- __call__ = ViewPageTemplateFile('../templates/text-area-editor.pt')
-
- def __init__(self, context, exported_field, title, content_box_id=None,
- edit_view="+edit", edit_url=None, edit_title='',
- hide_empty=True, linkify_text=True):
+ __call__ = ViewPageTemplateFile("../templates/text-area-editor.pt")
+
+ def __init__(
+ self,
+ context,
+ exported_field,
+ title,
+ content_box_id=None,
+ edit_view="+edit",
+ edit_url=None,
+ edit_title="",
+ hide_empty=True,
+ linkify_text=True,
+ ):
"""Create the widget wrapper.
:param context: The object that is being edited.
@@ -239,18 +284,24 @@ class TextAreaEditorWidget(TextWidgetBase):
things that look like links made into anchors.
"""
super().__init__(
- context, exported_field, title, content_box_id,
- edit_view, edit_url, edit_title)
+ context,
+ exported_field,
+ title,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ )
self.hide_empty = hide_empty
self.linkify_text = linkify_text
@property
def tag_class(self):
"""The CSS class for the widget."""
- classes = ['lazr-multiline-edit']
+ classes = ["lazr-multiline-edit"]
if self.hide_empty and not self.value:
- classes.append('hidden')
- return ' '.join(classes)
+ classes.append("hidden")
+ return " ".join(classes)
@cachedproperty
def value(self):
@@ -265,14 +316,22 @@ class InlineEditPickerWidget(WidgetBase):
VocabularyPickerWidget.
"""
- __call__ = ViewPageTemplateFile('../templates/inline-picker.pt')
-
- def __init__(self, context, exported_field, default_html,
- content_box_id=None, header='Select an item',
- step_title='Search',
- null_display_value='None',
- edit_view="+edit", edit_url=None, edit_title='',
- help_link=None):
+ __call__ = ViewPageTemplateFile("../templates/inline-picker.pt")
+
+ def __init__(
+ self,
+ context,
+ exported_field,
+ default_html,
+ content_box_id=None,
+ header="Select an item",
+ step_title="Search",
+ null_display_value="None",
+ edit_view="+edit",
+ edit_url=None,
+ edit_title="",
+ help_link=None,
+ ):
"""Create a widget wrapper.
:param context: The object that is being edited.
@@ -291,8 +350,13 @@ class InlineEditPickerWidget(WidgetBase):
:param edit_title: Used to set the title attribute of the anchor.
"""
super().__init__(
- context, exported_field, content_box_id,
- edit_view, edit_url, edit_title)
+ context,
+ exported_field,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ )
self.default_html = default_html
self.header = header
self.step_title = step_title
@@ -301,13 +365,14 @@ class InlineEditPickerWidget(WidgetBase):
# JSON encoded attributes.
self.json_content_box_id = simplejson.dumps(self.content_box_id)
- self.json_attribute = simplejson.dumps(self.api_attribute + '_link')
+ self.json_attribute = simplejson.dumps(self.api_attribute + "_link")
self.json_vocabulary_name = simplejson.dumps(
- self.exported_field.vocabularyName)
+ self.exported_field.vocabularyName
+ )
@property
def picker_type(self):
- return 'default'
+ return "default"
@property
def selected_value_metadata(self):
@@ -315,7 +380,7 @@ class InlineEditPickerWidget(WidgetBase):
@property
def selected_value(self):
- """ String representation of field value associated with the picker.
+ """String representation of field value associated with the picker.
Default implementation is to return the 'name' attribute.
"""
@@ -323,7 +388,7 @@ class InlineEditPickerWidget(WidgetBase):
return None
val = getattr(self.context, self.exported_field.__name__)
if val is not None:
- return getattr(val, 'name', None)
+ return getattr(val, "name", None)
return None
@property
@@ -333,12 +398,14 @@ class InlineEditPickerWidget(WidgetBase):
def getConfig(self):
return dict(
picker_type=self.picker_type,
- header=self.header, step_title=self.step_title,
+ header=self.header,
+ step_title=self.step_title,
selected_value=self.selected_value,
selected_value_metadata=self.selected_value_metadata,
null_display_value=self.null_display_value,
show_search_box=self.show_search_box,
- vocabulary_filters=self.vocabulary_filters)
+ vocabulary_filters=self.vocabulary_filters,
+ )
@property
def json_config(self):
@@ -347,8 +414,7 @@ class InlineEditPickerWidget(WidgetBase):
@cachedproperty
def vocabulary(self):
registry = getVocabularyRegistry()
- return registry.get(
- IVocabulary, self.exported_field.vocabularyName)
+ return registry.get(IVocabulary, self.exported_field.vocabularyName)
@cachedproperty
def vocabulary_filters(self):
@@ -360,15 +426,24 @@ class InlineEditPickerWidget(WidgetBase):
class InlinePersonEditPickerWidget(InlineEditPickerWidget):
- def __init__(self, context, exported_field, default_html,
- content_box_id=None, header='Select an item',
- step_title='Search', show_create_team=False,
- assign_me_text='Pick me',
- remove_person_text='Remove person',
- remove_team_text='Remove team',
- null_display_value='None',
- edit_view="+edit", edit_url=None, edit_title='',
- help_link=None):
+ def __init__(
+ self,
+ context,
+ exported_field,
+ default_html,
+ content_box_id=None,
+ header="Select an item",
+ step_title="Search",
+ show_create_team=False,
+ assign_me_text="Pick me",
+ remove_person_text="Remove person",
+ remove_team_text="Remove team",
+ null_display_value="None",
+ edit_view="+edit",
+ edit_url=None,
+ edit_title="",
+ help_link=None,
+ ):
"""Create a widget wrapper.
:param context: The object that is being edited.
@@ -391,9 +466,18 @@ class InlinePersonEditPickerWidget(InlineEditPickerWidget):
:param help_link: Used to set a link for help for the widget.
"""
super().__init__(
- context, exported_field, default_html, content_box_id, header,
- step_title, null_display_value,
- edit_view, edit_url, edit_title, help_link)
+ context,
+ exported_field,
+ default_html,
+ content_box_id,
+ header,
+ step_title,
+ null_display_value,
+ edit_view,
+ edit_url,
+ edit_title,
+ help_link,
+ )
self._show_create_team = show_create_team
self.assign_me_text = assign_me_text
@@ -402,7 +486,7 @@ class InlinePersonEditPickerWidget(InlineEditPickerWidget):
@property
def picker_type(self):
- return 'person'
+ return "person"
@property
def selected_value_metadata(self):
@@ -422,13 +506,16 @@ class InlinePersonEditPickerWidget(InlineEditPickerWidget):
def getConfig(self):
config = super().getConfig()
- config.update(dict(
- show_remove_button=self.optional_field,
- show_assign_me_button=self.show_assign_me_button,
- show_create_team=self.show_create_team,
- assign_me_text=self.assign_me_text,
- remove_person_text=self.remove_person_text,
- remove_team_text=self.remove_team_text))
+ config.update(
+ dict(
+ show_remove_button=self.optional_field,
+ show_assign_me_button=self.show_assign_me_button,
+ show_create_team=self.show_create_team,
+ assign_me_text=self.assign_me_text,
+ remove_person_text=self.remove_person_text,
+ remove_team_text=self.remove_team_text,
+ )
+ )
return config
@@ -436,15 +523,27 @@ class InlineMultiCheckboxWidget(WidgetBase):
"""Wrapper for the lazr-js multicheckbox widget."""
__call__ = ViewPageTemplateFile(
- '../templates/inline-multicheckbox-widget.pt')
-
- def __init__(self, context, exported_field,
- label, label_tag="span", attribute_type="default",
- vocabulary=None, header=None,
- empty_display_value="None", selected_items=list(),
- items_tag="span", items_style='',
- content_box_id=None, edit_view="+edit", edit_url=None,
- edit_title=''):
+ "../templates/inline-multicheckbox-widget.pt"
+ )
+
+ def __init__(
+ self,
+ context,
+ exported_field,
+ label,
+ label_tag="span",
+ attribute_type="default",
+ vocabulary=None,
+ header=None,
+ empty_display_value="None",
+ selected_items=list(),
+ items_tag="span",
+ items_style="",
+ content_box_id=None,
+ edit_view="+edit",
+ edit_url=None,
+ edit_title="",
+ ):
"""Create a widget wrapper.
:param context: The object that is being edited.
@@ -474,21 +573,28 @@ class InlineMultiCheckboxWidget(WidgetBase):
"""
super().__init__(
- context, exported_field, content_box_id,
- edit_view, edit_url, edit_title)
+ context,
+ exported_field,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ )
linkify_items = attribute_type == "reference"
if header is None:
header = self.exported_field.title + ":"
- self.header = header,
+ self.header = (header,)
self.empty_display_value = empty_display_value
self.label = label
self.label_open_tag = "<%s>" % label_tag
self.label_close_tag = "</%s>" % label_tag
self.items = selected_items
- self.items_open_tag = ("<%s id='%s'>" %
- (items_tag, self.content_box_id + "-items"))
+ self.items_open_tag = "<%s id='%s'>" % (
+ items_tag,
+ self.content_box_id + "-items",
+ )
self.items_close_tag = "</%s>" % items_tag
self.linkify_items = linkify_items
@@ -503,20 +609,21 @@ class InlineMultiCheckboxWidget(WidgetBase):
# Construct checkbox data dict for each item in the vocabulary.
items = []
- style = ';'.join(['font-weight: normal', items_style])
+ style = ";".join(["font-weight: normal", items_style])
for item in vocabulary:
- item_value = getattr(item, 'value', item)
+ item_value = getattr(item, "value", item)
checked = item_value in selected_items
if linkify_items:
save_value = canonical_url(item_value, force_local_path=True)
else:
save_value = item_value.name
new_item = {
- 'name': item.title,
- 'token': item.token,
- 'style': style,
- 'checked': checked,
- 'value': save_value}
+ "name": item.title,
+ "token": item.token,
+ "style": style,
+ "checked": checked,
+ "value": save_value,
+ }
items.append(new_item)
self.has_choices = len(items)
@@ -526,13 +633,14 @@ class InlineMultiCheckboxWidget(WidgetBase):
self.json_attribute_type = simplejson.dumps(attribute_type)
self.json_items = simplejson.dumps(items)
self.json_description = simplejson.dumps(
- self.exported_field.description)
+ self.exported_field.description
+ )
@property
def config(self):
return dict(
header=self.header,
- )
+ )
@property
def json_config(self):
@@ -540,9 +648,16 @@ class InlineMultiCheckboxWidget(WidgetBase):
def vocabulary_to_choice_edit_items(
- vocab, include_description=False, css_class_prefix=None,
- disabled_items=None, excluded_items=None,
- as_json=False, name_fn=None, value_fn=None, description_fn=None):
+ vocab,
+ include_description=False,
+ css_class_prefix=None,
+ disabled_items=None,
+ excluded_items=None,
+ as_json=False,
+ name_fn=None,
+ value_fn=None,
+ description_fn=None,
+):
"""Convert an enumerable to JSON for a ChoiceEdit.
:vocab: The enumeration to iterate over.
@@ -558,7 +673,7 @@ def vocabulary_to_choice_edit_items(
for item in vocab:
# Introspect to make sure we're dealing with the object itself.
# SimpleTerm objects have the object itself at item.value.
- if hasattr(item, 'value'):
+ if hasattr(item, "value"):
item = item.value
if excluded_items and item in excluded_items:
continue
@@ -571,22 +686,25 @@ def vocabulary_to_choice_edit_items(
else:
value = item.title
if description_fn is None:
- description_fn = lambda item: getattr(item, 'description', '')
- description = ''
+ description_fn = lambda item: getattr(item, "description", "")
+ description = ""
if include_description:
description = description_fn(item)
new_item = {
- 'name': name,
- 'value': value,
- 'description': description,
- 'description_css_class': 'choice-description',
- 'style': '', 'help': '', 'disabled': False}
+ "name": name,
+ "value": value,
+ "description": description,
+ "description_css_class": "choice-description",
+ "style": "",
+ "help": "",
+ "disabled": False,
+ }
for disabled_item in disabled_items:
if disabled_item == item:
- new_item['disabled'] = True
+ new_item["disabled"] = True
break
if css_class_prefix is not None:
- new_item['css_class'] = css_class_prefix + item.name
+ new_item["css_class"] = css_class_prefix + item.name
items.append(new_item)
if as_json:
@@ -601,7 +719,7 @@ def standard_text_html_representation(value, linkify_text=True):
For this we obfuscate email and render as html.
"""
if value is None:
- return ''
+ return ""
nomail = FormattersAPI(value).obfuscate_email()
return FormattersAPI(nomail).text_to_html(linkify_text=linkify_text)
@@ -609,12 +727,23 @@ def standard_text_html_representation(value, linkify_text=True):
class BooleanChoiceWidget(WidgetBase, DefinedTagMixin):
"""A ChoiceEdit for a boolean field."""
- __call__ = ViewPageTemplateFile('../templates/boolean-choice-widget.pt')
-
- def __init__(self, context, exported_field,
- tag, false_text, true_text, css_class=None, prefix=None,
- edit_view="+edit", edit_url=None, edit_title='',
- content_box_id=None, header='Select an item'):
+ __call__ = ViewPageTemplateFile("../templates/boolean-choice-widget.pt")
+
+ def __init__(
+ self,
+ context,
+ exported_field,
+ tag,
+ false_text,
+ true_text,
+ css_class=None,
+ prefix=None,
+ edit_view="+edit",
+ edit_url=None,
+ edit_title="",
+ content_box_id=None,
+ header="Select an item",
+ ):
"""Create a widget wrapper.
:param context: The object that is being edited.
@@ -635,8 +764,13 @@ class BooleanChoiceWidget(WidgetBase, DefinedTagMixin):
:param header: The large text at the top of the choice popup.
"""
super().__init__(
- context, exported_field, content_box_id,
- edit_view, edit_url, edit_title)
+ context,
+ exported_field,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ )
self.header = header
self.tag = tag
self.css_class = css_class
@@ -655,14 +789,26 @@ class BooleanChoiceWidget(WidgetBase, DefinedTagMixin):
@property
def config(self):
return dict(
- contentBox='#' + self.content_box_id,
+ contentBox="#" + self.content_box_id,
value=self.current_value,
title=self.header,
items=[
- dict(name=self.true_text, value=True, style='', help='',
- disabled=False),
- dict(name=self.false_text, value=False, style='', help='',
- disabled=False)])
+ dict(
+ name=self.true_text,
+ value=True,
+ style="",
+ help="",
+ disabled=False,
+ ),
+ dict(
+ name=self.false_text,
+ value=False,
+ style="",
+ help="",
+ disabled=False,
+ ),
+ ],
+ )
@property
def json_config(self):
@@ -672,12 +818,21 @@ class BooleanChoiceWidget(WidgetBase, DefinedTagMixin):
class EnumChoiceWidget(WidgetBase):
"""A popup choice widget."""
- __call__ = ViewPageTemplateFile('../templates/enum-choice-widget.pt')
-
- def __init__(self, context, exported_field, header,
- content_box_id=None, enum=None,
- edit_view="+edit", edit_url=None, edit_title='',
- css_class_prefix='', include_description=False):
+ __call__ = ViewPageTemplateFile("../templates/enum-choice-widget.pt")
+
+ def __init__(
+ self,
+ context,
+ exported_field,
+ header,
+ content_box_id=None,
+ enum=None,
+ edit_view="+edit",
+ edit_url=None,
+ edit_title="",
+ css_class_prefix="",
+ include_description=False,
+ ):
"""Create a widget wrapper.
:param context: The object that is being edited.
@@ -695,8 +850,13 @@ class EnumChoiceWidget(WidgetBase):
:param css_class_prefix: Added to the start of the enum titles.
"""
super().__init__(
- context, exported_field, content_box_id,
- edit_view, edit_url, edit_title)
+ context,
+ exported_field,
+ content_box_id,
+ edit_view,
+ edit_url,
+ edit_title,
+ )
self.header = header
value = getattr(self.context, self.attribute_name)
self.css_class = "value %s%s" % (css_class_prefix, value.name)
@@ -705,18 +865,21 @@ class EnumChoiceWidget(WidgetBase):
# Get the enum from the exported field.
enum = exported_field.vocabulary
if IEnumeratedType(enum, None) is None:
- raise ValueError('%r does not provide IEnumeratedType' % enum)
+ raise ValueError("%r does not provide IEnumeratedType" % enum)
self.items = vocabulary_to_choice_edit_items(
- enum, include_description=include_description,
- css_class_prefix=css_class_prefix)
+ enum,
+ include_description=include_description,
+ css_class_prefix=css_class_prefix,
+ )
@property
def config(self):
return dict(
- contentBox='#' + self.content_box_id,
+ contentBox="#" + self.content_box_id,
value=self.value,
title=self.header,
- items=self.items)
+ items=self.items,
+ )
@property
def json_config(self):
diff --git a/lib/lp/app/browser/linkchecker.py b/lib/lp/app/browser/linkchecker.py
index fbdacc1..2ad22eb 100644
--- a/lib/lp/app/browser/linkchecker.py
+++ b/lib/lp/app/browser/linkchecker.py
@@ -2,8 +2,8 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
- 'LinkCheckerAPI',
- ]
+ "LinkCheckerAPI",
+]
import simplejson
from zope.component import getUtility
@@ -16,7 +16,7 @@ from lp.code.errors import (
InvalidNamespace,
NoLinkedBranch,
NoSuchBranch,
- )
+)
from lp.code.interfaces.branchlookup import IBranchLookup
from lp.code.interfaces.gitlookup import IGitLookup
from lp.registry.interfaces.product import InvalidProductName
@@ -54,7 +54,7 @@ class LinkCheckerAPI(LaunchpadView):
def __call__(self):
result = {}
- links_to_check_data = self.request.get('link_hrefs')
+ links_to_check_data = self.request.get("link_hrefs")
if links_to_check_data is None:
return simplejson.dumps(result)
links_to_check = simplejson.loads(links_to_check_data)
@@ -64,7 +64,7 @@ class LinkCheckerAPI(LaunchpadView):
link_info = self.link_checkers[link_type](links)
result[link_type] = link_info
- self.request.response.setHeader('Content-type', 'application/json')
+ self.request.response.setHeader("Content-type", "application/json")
return simplejson.dumps(result)
def check_branch_links(self, links):
@@ -73,15 +73,20 @@ class LinkCheckerAPI(LaunchpadView):
bzr_branch_lookup = getUtility(IBranchLookup)
git_branch_lookup = getUtility(IGitLookup)
for link in links:
- path = link.strip('/')[len('+code/'):]
+ path = link.strip("/")[len("+code/") :]
if git_branch_lookup.getByPath(path)[0] is None:
try:
bzr_branch_lookup.getByLPPath(path)
- except (CannotHaveLinkedBranch, InvalidNamespace,
- InvalidProductName, NoLinkedBranch, NoSuchBranch,
- NotFoundError) as e:
+ except (
+ CannotHaveLinkedBranch,
+ InvalidNamespace,
+ InvalidProductName,
+ NoLinkedBranch,
+ NoSuchBranch,
+ NotFoundError,
+ ) as e:
invalid_links[link] = self._error_message(e)
- return {'invalid': invalid_links}
+ return {"invalid": invalid_links}
def check_bug_links(self, links):
"""Checks links of the form /bugs/100"""
@@ -89,24 +94,25 @@ class LinkCheckerAPI(LaunchpadView):
valid_links = {}
user = self.user
# List of all the bugs we are checking.
- bugs_ids = {int(link[len('/bugs/'):]) for link in links}
+ bugs_ids = {int(link[len("/bugs/") :]) for link in links}
if bugs_ids:
params = BugTaskSearchParams(
- user=user, status=None,
- bug=any(*bugs_ids))
+ user=user, status=None, bug=any(*bugs_ids)
+ )
bugtasks = getUtility(IBugTaskSet).search(params)
for task in bugtasks:
- valid_links['/bugs/' + str(task.bug.id)] = task.bug.title
+ valid_links["/bugs/" + str(task.bug.id)] = task.bug.title
# Remove valid bugs from the list of all the bugs.
if task.bug.id in bugs_ids:
bugs_ids.remove(task.bug.id)
# We should now have only invalid bugs in bugs list
for bug in bugs_ids:
- invalid_links['/bugs/%d' % bug] = (
- "Bug %s cannot be found" % bug)
- return {'valid': valid_links, 'invalid': invalid_links}
+ invalid_links["/bugs/%d" % bug] = (
+ "Bug %s cannot be found" % bug
+ )
+ return {"valid": valid_links, "invalid": invalid_links}
def _error_message(self, ex):
- if hasattr(ex, 'display_message'):
+ if hasattr(ex, "display_message"):
return ex.display_message
return str(ex)
diff --git a/lib/lp/app/browser/multistep.py b/lib/lp/app/browser/multistep.py
index 6215eca..a6b7aee 100644
--- a/lib/lp/app/browser/multistep.py
+++ b/lib/lp/app/browser/multistep.py
@@ -4,9 +4,9 @@
"""Multiple step views."""
__all__ = [
- 'MultiStepView',
- 'StepView',
- ]
+ "MultiStepView",
+ "StepView",
+]
from zope.formlib import form
@@ -16,23 +16,19 @@ from zope.interface import Interface
from zope.schema import TextLine
from lp import _
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
-from lp.services.webapp import (
- canonical_url,
- LaunchpadView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
+from lp.services.webapp import LaunchpadView, canonical_url
class _IStepMachinery(Interface):
"""A schema solely for the multistep wizard machinery."""
+
__visited_steps__ = TextLine(
- title=_('Visited steps'),
+ title=_("Visited steps"),
description=_(
- "Used to keep track of the steps visited in a multistep form.")
- )
+ "Used to keep track of the steps visited in a multistep form."
+ ),
+ )
class MultiStepView(LaunchpadView):
@@ -85,7 +81,7 @@ class MultiStepView(LaunchpadView):
def initialize(self):
"""Initialize the view and handle stepping through sub-views."""
view = self.first_step(self.context, self.request)
- assert isinstance(view, StepView), 'Not a StepView: %s' % view
+ assert isinstance(view, StepView), "Not a StepView: %s" % view
# We should be calling injectStepNameInRequest() after initialize() in
# both this case and inside the loop below, otherwise the form will be
# processed when it's first rendered, thus showing warning/error
@@ -102,14 +98,14 @@ class MultiStepView(LaunchpadView):
action_required = None
for name in self.request.form.keys():
- if name.startswith('field.actions.'):
+ if name.startswith("field.actions."):
action_required = (name, self.request.form[name])
break
action_taken = view.action_taken
while view.next_step is not None:
view = view.next_step(self.context, self.request)
- assert isinstance(view, StepView), 'Not a StepView: %s' % view
+ assert isinstance(view, StepView), "Not a StepView: %s" % view
view.initialize()
view.step_number = self.step_number
view.total_steps = self.total_steps
@@ -126,8 +122,8 @@ class MultiStepView(LaunchpadView):
# in invalid form data via a dictionary instead of
# using a test browser.
raise AssertionError(
- 'MultiStepView did not find action for %s=%r'
- % action_required)
+ "MultiStepView did not find action for %s=%r" % action_required
+ )
def render(self):
return self.view.render()
@@ -146,13 +142,15 @@ class StepView(LaunchpadFormView):
If views want to change the label of their Continue button, they should
override `main_action_label`.
"""
+
# Use a custom widget in order to make it invisible.
custom_widget___visited_steps__ = CustomWidgetFactory(
- TextWidget, visible=False)
+ TextWidget, visible=False
+ )
_field_names = []
- step_name = ''
- main_action_label = 'Continue'
+ step_name = ""
+ main_action_label = "Continue"
next_step = None
# Step information. These get filled in by the controller view.
@@ -161,7 +159,7 @@ class StepView(LaunchpadFormView):
# Define this here so it can be overridden in subclasses, if for some
# crazy reason you need step names with vertical bars in them.
- SEPARATOR = '|'
+ SEPARATOR = "|"
def extendFields(self):
"""See `LaunchpadFormView`."""
@@ -170,7 +168,7 @@ class StepView(LaunchpadFormView):
@property
def field_names(self):
"""Do not override."""
- return self._field_names + ['__visited_steps__']
+ return self._field_names + ["__visited_steps__"]
def validateStep(self, data):
"""Validation specific to a given step.
@@ -182,7 +180,7 @@ class StepView(LaunchpadFormView):
def main_action(self, data):
raise NotImplementedError
- @action(main_action_label, name='continue')
+ @action(main_action_label, name="continue")
def continue_action(self, action, data):
"""The action of the continue button.
@@ -207,14 +205,14 @@ class StepView(LaunchpadFormView):
def injectStepNameInRequest(self):
"""Inject this step's name into the request if necessary."""
- visited_steps = self.request.form.get('field.__visited_steps__')
+ visited_steps = self.request.form.get("field.__visited_steps__")
if not visited_steps:
- self.request.form['field.__visited_steps__'] = self.step_name
+ self.request.form["field.__visited_steps__"] = self.step_name
elif self.step_name not in visited_steps:
steps = visited_steps.split(self.SEPARATOR)
steps.append(self.step_name)
new_steps = self.SEPARATOR.join(steps)
- self.request.form['field.__visited_steps__'] = new_steps
+ self.request.form["field.__visited_steps__"] = new_steps
else:
# We already visited this step, so there's no need to inject our
# step_name in the request again.
@@ -230,7 +228,7 @@ class StepView(LaunchpadFormView):
that to find out whether or not to process them, so we use an extra
hidden input to store the views the user has visited already.
"""
- steps = data['__visited_steps__'].split('|')
+ steps = data["__visited_steps__"].split("|")
return self.step_name in steps
def render(self):
@@ -239,7 +237,7 @@ class StepView(LaunchpadFormView):
actions = []
for operation in self.actions:
# Only change the label of our 'continue' action.
- if operation.__name__ == 'field.actions.continue':
+ if operation.__name__ == "field.actions.continue":
operation.label = self.main_action_label
actions.append(operation)
self.actions = actions
diff --git a/lib/lp/app/browser/root.py b/lib/lp/app/browser/root.py
index 694c8d2..7d5acda 100644
--- a/lib/lp/app/browser/root.py
+++ b/lib/lp/app/browser/root.py
@@ -3,17 +3,17 @@
"""Browser code for the Launchpad root page."""
__all__ = [
- 'LaunchpadRootIndexView',
- 'LaunchpadSearchView',
- ]
+ "LaunchpadRootIndexView",
+ "LaunchpadSearchView",
+]
import re
import time
import feedparser
-from lazr.batchnavigator.z3batching import batch
import requests
+from lazr.batchnavigator.z3batching import batch
from zope.component import getUtility
from zope.formlib.interfaces import ConversionError
from zope.interface import Interface
@@ -23,11 +23,7 @@ from zope.schema.vocabulary import getVocabularyRegistry
from lp import _
from lp.answers.interfaces.questioncollection import IQuestionSet
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- safe_action,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action, safe_action
from lp.app.errors import NotFoundError
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.validators.name import sanitize_name
@@ -41,9 +37,9 @@ from lp.services.features import getFeatureFlag
from lp.services.memcache.interfaces import IMemcacheClient
from lp.services.propertycache import cachedproperty
from lp.services.sitesearch.interfaces import (
- active_search_service,
SiteSearchResponseError,
- )
+ active_search_service,
+)
from lp.services.statistics.interfaces.statistic import ILaunchpadStatisticSet
from lp.services.timeout import urlfetch
from lp.services.webapp import LaunchpadView
@@ -52,14 +48,13 @@ from lp.services.webapp.batching import BatchNavigator
from lp.services.webapp.publisher import canonical_url
from lp.services.webapp.vhosts import allvhosts
-
-shipit_faq_url = 'http://www.ubuntu.com/getubuntu/shipit-faq'
+shipit_faq_url = "http://www.ubuntu.com/getubuntu/shipit-faq"
class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
"""An view for the default view of the LaunchpadRoot."""
- page_title = 'Launchpad'
+ page_title = "Launchpad"
featured_projects = []
featured_projects_top = None
@@ -82,7 +77,8 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
# The maximum number of projects to be displayed as defined by the
# number of items plus one top featured project.
self.featured_projects = list(
- getUtility(IPillarNameSet).featured_projects)
+ getUtility(IPillarNameSet).featured_projects
+ )
self._setFeaturedProjectsTop()
def _setFeaturedProjectsTop(self):
@@ -91,36 +87,38 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
if project_count > 0:
top_project = self._get_day_of_year() % project_count
self.featured_projects_top = self.featured_projects.pop(
- top_project)
+ top_project
+ )
@cachedproperty
def apphomes(self):
return {
- 'answers': canonical_url(self.context, rootsite='answers'),
- 'blueprints': canonical_url(self.context, rootsite='blueprints'),
- 'bugs': canonical_url(self.context, rootsite='bugs'),
- 'code': canonical_url(self.context, rootsite='code'),
- 'translations': canonical_url(self.context,
- rootsite='translations'),
- 'ubuntu': canonical_url(
- getUtility(ILaunchpadCelebrities).ubuntu),
- }
+ "answers": canonical_url(self.context, rootsite="answers"),
+ "blueprints": canonical_url(self.context, rootsite="blueprints"),
+ "bugs": canonical_url(self.context, rootsite="bugs"),
+ "code": canonical_url(self.context, rootsite="code"),
+ "translations": canonical_url(
+ self.context, rootsite="translations"
+ ),
+ "ubuntu": canonical_url(getUtility(ILaunchpadCelebrities).ubuntu),
+ }
@property
def branch_count(self):
"""The total branch count of public branches in all of Launchpad."""
- return getUtility(ILaunchpadStatisticSet).value('public_branch_count')
+ return getUtility(ILaunchpadStatisticSet).value("public_branch_count")
@property
def gitrepository_count(self):
"""The total Git repository count in all of Launchpad."""
return getUtility(ILaunchpadStatisticSet).value(
- 'public_git_repository_count')
+ "public_git_repository_count"
+ )
@property
def bug_count(self):
"""The total bug count in all of Launchpad."""
- return getUtility(ILaunchpadStatisticSet).value('bug_count')
+ return getUtility(ILaunchpadStatisticSet).value("bug_count")
@property
def project_count(self):
@@ -129,19 +127,20 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
@property
def translation_count(self):
- """The total count of translatable strings in all of Launchpad """
- return getUtility(ILaunchpadStatisticSet).value('pomsgid_count')
+ """The total count of translatable strings in all of Launchpad"""
+ return getUtility(ILaunchpadStatisticSet).value("pomsgid_count")
@property
def blueprint_count(self):
"""The total blueprint count in all of Launchpad."""
return getUtility(ILaunchpadStatisticSet).value(
- 'public_specification_count')
+ "public_specification_count"
+ )
@property
def answer_count(self):
"""The total blueprint count in all of Launchpad."""
- return getUtility(ILaunchpadStatisticSet).value('question_count')
+ return getUtility(ILaunchpadStatisticSet).value("question_count")
@property
def show_whatslaunchpad(self):
@@ -165,7 +164,7 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
FeedParser takes care of sanitizing the HTML contained in the feed.
"""
- key = '%s:homepage-blog-posts' % config.instance_name
+ key = "%s:homepage-blog-posts" % config.instance_name
cached_data = getUtility(IMemcacheClient).get(key)
if cached_data:
return cached_data
@@ -179,12 +178,14 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
max_count = config.launchpad.homepage_recent_posts_count
# FeedParser takes care of HTML sanitisation.
for entry in feed.entries[:max_count]:
- posts.append({
- 'title': entry.title,
- 'description': entry.description,
- 'link': entry.link,
- 'date': time.strftime('%d %b %Y', entry.published_parsed),
- })
+ posts.append(
+ {
+ "title": entry.title,
+ "description": entry.description,
+ "link": entry.link,
+ "date": time.strftime("%d %b %Y", entry.published_parsed),
+ }
+ )
# The cache of posts expires after an hour.
getUtility(IMemcacheClient).set(key, posts, expire=3600)
return posts
@@ -192,7 +193,8 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
class LaunchpadSearchFormView(LaunchpadView):
"""A view to display the global search form in any page."""
- id_suffix = '-secondary'
+
+ id_suffix = "-secondary"
text = None
focusedElementScript = None
form_wide_errors = None
@@ -204,12 +206,13 @@ class LaunchpadSearchFormView(LaunchpadView):
@property
def rooturl(self):
"""Return the site's root url."""
- return allvhosts.configs['mainsite'].rooturl
+ return allvhosts.configs["mainsite"].rooturl
class LaunchpadPrimarySearchFormView(LaunchpadSearchFormView):
"""A view to display the global search form in the page."""
- id_suffix = ''
+
+ id_suffix = ""
@property
def text(self):
@@ -239,35 +242,56 @@ class LaunchpadPrimarySearchFormView(LaunchpadSearchFormView):
@property
def error(self):
"""The context view's text field error."""
- return self.context.getFieldError('text')
+ return self.context.getFieldError("text")
@property
def error_class(self):
"""Return the 'error' if there is an error, or None."""
if self.error:
- return 'error'
+ return "error"
return None
class ILaunchpadSearch(Interface):
"""The Schema for performing searches across all Launchpad."""
- text = TextLine(
- title=_('Search text'), required=False, max_length=250)
+ text = TextLine(title=_("Search text"), required=False, max_length=250)
class LaunchpadSearchView(LaunchpadFormView):
"""A view to search for Launchpad pages and objects."""
+
schema = ILaunchpadSearch
- field_names = ['text']
+ field_names = ["text"]
shipit_keywords = {
- 'ubuntu', 'kubuntu', 'edubuntu',
- 'ship', 'shipit', 'send', 'get', 'mail', 'free',
- 'cd', 'cds', 'dvd', 'dvds', 'disc'}
+ "ubuntu",
+ "kubuntu",
+ "edubuntu",
+ "ship",
+ "shipit",
+ "send",
+ "get",
+ "mail",
+ "free",
+ "cd",
+ "cds",
+ "dvd",
+ "dvds",
+ "disc",
+ }
shipit_anti_keywords = {
- 'burn', 'burning', 'enable', 'error', 'errors', 'image', 'iso',
- 'read', 'rip', 'write'}
+ "burn",
+ "burning",
+ "enable",
+ "error",
+ "errors",
+ "image",
+ "iso",
+ "read",
+ "rip",
+ "write",
+ }
def __init__(self, context, request):
"""Initialize the view.
@@ -283,39 +307,39 @@ class LaunchpadSearchView(LaunchpadFormView):
self._pages = None
self.search_params = self._getDefaultSearchParams()
# The Search Action should always run.
- self.request.form['field.actions.search'] = 'Search'
+ self.request.form["field.actions.search"] = "Search"
def _getDefaultSearchParams(self):
"""Return a dict of the search param set to their default state."""
return {
- 'text': None,
- 'start': 0,
- }
+ "text": None,
+ "start": 0,
+ }
def _updateSearchParams(self):
"""Sanitize the search_params and add the BatchNavigator params."""
- if self.search_params['text'] is not None:
- text = self.search_params['text'].strip()
- if text == '':
- self.search_params['text'] = None
+ if self.search_params["text"] is not None:
+ text = self.search_params["text"].strip()
+ if text == "":
+ self.search_params["text"] = None
else:
- self.search_params['text'] = text
- request_start = self.request.get('start', self.search_params['start'])
+ self.search_params["text"] = text
+ request_start = self.request.get("start", self.search_params["start"])
try:
start = int(request_start)
except (ValueError, TypeError):
return
- self.search_params['start'] = start
+ self.search_params["start"] = start
@property
def text(self):
"""Return the text or None."""
- return self.search_params['text']
+ return self.search_params["text"]
@property
def start(self):
"""Return the start index of the batch."""
- return self.search_params['start']
+ return self.search_params["start"]
@property
def page_title(self):
@@ -326,7 +350,7 @@ class LaunchpadSearchView(LaunchpadFormView):
def page_heading(self):
"""Heading to display above the search results."""
if self.text is None:
- return 'Search Launchpad'
+ return "Search Launchpad"
else:
return 'Pages matching "%s" in Launchpad' % self.text
@@ -334,11 +358,15 @@ class LaunchpadSearchView(LaunchpadFormView):
def batch_heading(self):
"""Heading to display in the batch navigation."""
if self.has_exact_matches:
- return ('other page matching "%s"' % self.text,
- 'other pages matching "%s"' % self.text)
+ return (
+ 'other page matching "%s"' % self.text,
+ 'other pages matching "%s"' % self.text,
+ )
else:
- return ('page matching "%s"' % self.text,
- 'pages matching "%s"' % self.text)
+ return (
+ 'page matching "%s"' % self.text,
+ 'pages matching "%s"' % self.text,
+ )
@property
def focusedElementScript(self):
@@ -387,8 +415,13 @@ class LaunchpadSearchView(LaunchpadFormView):
@property
def has_exact_matches(self):
"""Return True if something exactly matched the search terms."""
- kinds = (self.bug, self.question, self.pillar,
- self.person_or_team, self.has_shipit)
+ kinds = (
+ self.bug,
+ self.question,
+ self.pillar,
+ self.person_or_team,
+ self.has_shipit,
+ )
return self.containsMatchingKind(kinds)
@property
@@ -399,18 +432,24 @@ class LaunchpadSearchView(LaunchpadFormView):
@property
def has_matches(self):
"""Return True if something matched the search terms, or False."""
- kinds = (self.bug, self.question, self.pillar,
- self.person_or_team, self.has_shipit, self.pages)
+ kinds = (
+ self.bug,
+ self.question,
+ self.pillar,
+ self.person_or_team,
+ self.has_shipit,
+ self.pages,
+ )
return self.containsMatchingKind(kinds)
@property
def url(self):
"""Return the requested URL."""
- if 'QUERY_STRING' in self.request:
- query_string = self.request['QUERY_STRING']
+ if "QUERY_STRING" in self.request:
+ query_string = self.request["QUERY_STRING"]
else:
- query_string = ''
- return self.request.getURL() + '?' + query_string
+ query_string = ""
+ return self.request.getURL() + "?" + query_string
def containsMatchingKind(self, kinds):
"""Return True if one of the items in kinds is not None, or False."""
@@ -424,17 +463,18 @@ class LaunchpadSearchView(LaunchpadFormView):
errors = list(self.errors)
for error in errors:
if isinstance(error, ConversionError):
- self.setFieldError(
- 'text', 'Can not convert your search term.')
+ self.setFieldError("text", "Can not convert your search term.")
elif isinstance(error, str):
continue
- elif (error.field_name == 'text'
- and isinstance(error.errors, TooLong)):
+ elif error.field_name == "text" and isinstance(
+ error.errors, TooLong
+ ):
self.setFieldError(
- 'text', 'The search text cannot exceed 250 characters.')
+ "text", "The search text cannot exceed 250 characters."
+ )
@safe_action
- @action('Search', name='search')
+ @action("Search", name="search")
def search_action(self, action, data):
"""The Action executed when the user uses the search button.
@@ -462,13 +502,14 @@ class LaunchpadSearchView(LaunchpadFormView):
if name_token is not None:
self._person_or_team = self._getPersonOrTeam(name_token)
self._pillar = self._getDistributionOrProductOrProjectGroup(
- name_token)
+ name_token
+ )
self._pages = self.searchPages(self.text, start=self.start)
def _getNumericToken(self, text):
"""Return the first group of numbers in the search text, or None."""
- numeric_pattern = re.compile(r'(\d+)')
+ numeric_pattern = re.compile(r"(\d+)")
match = numeric_pattern.search(text)
if match is None:
return None
@@ -480,16 +521,18 @@ class LaunchpadSearchView(LaunchpadFormView):
Launchpad names may contain ^[a-z0-9][a-z0-9\+\.\-]+$.
See `valid_name_pattern`.
"""
- hypen_pattern = re.compile(r'[ _]')
- name = hypen_pattern.sub('-', text.strip().lower())
+ hypen_pattern = re.compile(r"[ _]")
+ name = hypen_pattern.sub("-", text.strip().lower())
return sanitize_name(name)
def _getPersonOrTeam(self, name):
"""Return the matching active person or team."""
person_or_team = getUtility(IPersonSet).getByName(name)
- if (person_or_team is not None
+ if (
+ person_or_team is not None
and person_or_team.is_valid_person_or_team
- and check_permission('launchpad.View', person_or_team)):
+ and check_permission("launchpad.View", person_or_team)
+ ):
return person_or_team
return None
@@ -497,7 +540,8 @@ class LaunchpadSearchView(LaunchpadFormView):
"""Return the matching distribution, product or project, or None."""
vocabulary_registry = getVocabularyRegistry()
vocab = vocabulary_registry.get(
- None, 'DistributionOrProductOrProjectGroup')
+ None, "DistributionOrProductOrProjectGroup"
+ )
try:
pillar = vocab.getTermByToken(name).value
if check_permission("launchpad.View", pillar):
@@ -513,12 +557,11 @@ class LaunchpadSearchView(LaunchpadFormView):
:param start: The index of the page that starts the set of pages.
:return: A SiteSearchBatchNavigator or None.
"""
- if query_terms in [None, '']:
+ if query_terms in [None, ""]:
return None
site_search = active_search_service()
try:
- page_matches = site_search.search(
- terms=query_terms, start=start)
+ page_matches = site_search.search(terms=query_terms, start=start)
except SiteSearchResponseError:
# There was a connectivity or search service issue that means
# there is no data available at this moment.
@@ -527,7 +570,8 @@ class LaunchpadSearchView(LaunchpadFormView):
if len(page_matches) == 0:
return None
navigator = SiteSearchBatchNavigator(
- page_matches, self.request, start=start)
+ page_matches, self.request, start=start
+ )
navigator.setHeadings(*self.batch_heading)
return navigator
@@ -596,12 +640,20 @@ class SiteSearchBatchNavigator(BatchNavigator):
# good chance of getting over 100,000 results.
show_last_link = False
- singular_heading = 'page'
- plural_heading = 'pages'
-
- def __init__(self, results, request, start=0, size=20, callback=None,
- transient_parameters=None, force_start=False,
- range_factory=None):
+ singular_heading = "page"
+ plural_heading = "pages"
+
+ def __init__(
+ self,
+ results,
+ request,
+ start=0,
+ size=20,
+ callback=None,
+ transient_parameters=None,
+ force_start=False,
+ range_factory=None,
+ ):
"""See `BatchNavigator`.
:param results: A `PageMatches` object that contains the matching
@@ -613,10 +665,16 @@ class SiteSearchBatchNavigator(BatchNavigator):
:param callback: Not used.
"""
results = WindowedList(results, start, results.total)
- super().__init__(results, request,
- start=start, size=size, callback=callback,
+ super().__init__(
+ results,
+ request,
+ start=start,
+ size=size,
+ callback=callback,
transient_parameters=transient_parameters,
- force_start=force_start, range_factory=range_factory)
+ force_start=force_start,
+ range_factory=range_factory,
+ )
def determineSize(self, size, batch_params_source):
# Force the default and users requested sizes to 20.
diff --git a/lib/lp/app/browser/stringformatter.py b/lib/lp/app/browser/stringformatter.py
index 4ca97e1..f54f44a 100644
--- a/lib/lp/app/browser/stringformatter.py
+++ b/lib/lp/app/browser/stringformatter.py
@@ -4,48 +4,38 @@
"""TALES formatter for strings."""
__all__ = [
- 'add_word_breaks',
- 'break_long_words',
- 'extract_bug_numbers',
- 'extract_email_addresses',
- 'FormattersAPI',
- 'linkify_bug_numbers',
- 'parse_diff',
- 're_substitute',
- 'split_paragraphs',
- ]
+ "add_word_breaks",
+ "break_long_words",
+ "extract_bug_numbers",
+ "extract_email_addresses",
+ "FormattersAPI",
+ "linkify_bug_numbers",
+ "parse_diff",
+ "re_substitute",
+ "split_paragraphs",
+]
-from base64 import urlsafe_b64encode
-from itertools import zip_longest
import re
import sys
+from base64 import urlsafe_b64encode
+from itertools import zip_longest
-from breezy.patches import hunk_from_header
-from lxml import html
import markdown
import six
+from breezy.patches import hunk_from_header
+from lxml import html
from zope.component import getUtility
from zope.error.interfaces import IErrorReportingUtility
from zope.interface import implementer
-from zope.traversing.interfaces import (
- ITraversable,
- TraversalError,
- )
+from zope.traversing.interfaces import ITraversable, TraversalError
from lp.answers.interfaces.faq import IFAQSet
from lp.registry.interfaces.person import IPersonSet
from lp.services.config import config
from lp.services.features import getFeatureFlag
-from lp.services.utils import (
- obfuscate_email,
- re_email_address,
- )
+from lp.services.utils import obfuscate_email, re_email_address
from lp.services.webapp import canonical_url
-from lp.services.webapp.escaping import (
- html_escape,
- html_unescape,
- structured,
- )
+from lp.services.webapp.escaping import html_escape, html_unescape, structured
from lp.services.webapp.interfaces import ILaunchBag
@@ -94,13 +84,13 @@ def re_substitute(pattern, replace_match, replace_nomatch, string):
position = 0
for match in re.finditer(pattern, string):
if match.start() != position:
- parts.append(replace_nomatch(string[position:match.start()]))
+ parts.append(replace_nomatch(string[position : match.start()]))
parts.append(replace_match(match))
position = match.end()
remainder = string[position:]
if remainder:
parts.append(replace_nomatch(remainder))
- return ''.join(parts)
+ return "".join(parts)
def next_word_chunk(word, pos, minlen, maxlen):
@@ -117,10 +107,10 @@ def next_word_chunk(word, pos, minlen, maxlen):
endpos = pos
while endpos < len(word):
# advance by one character
- if word[endpos] == '&':
+ if word[endpos] == "&":
# make sure we grab the entity as a whole
- semicolon = word.find(';', endpos)
- assert semicolon >= 0, 'badly formed entity: %r' % word[endpos:]
+ semicolon = word.find(";", endpos)
+ assert semicolon >= 0, "badly formed entity: %r" % word[endpos:]
endpos = semicolon + 1
else:
endpos += 1
@@ -149,17 +139,20 @@ def add_word_breaks(word):
while pos < len(word):
chunk, pos = next_word_chunk(word, pos, 7, 15)
broken.append(chunk)
- return '<wbr />'.join(broken)
+ return "<wbr />".join(broken)
-break_text_pat = re.compile(r'''
+break_text_pat = re.compile(
+ r"""
(?P<tag>
<[^>]*>
) |
(?P<longword>
(?<![^\s<>])(?:[^\s<>&]|&[^;]*;){20,}
)
-''', re.VERBOSE)
+""",
+ re.VERBOSE,
+)
def break_long_words(text):
@@ -169,17 +162,18 @@ def break_long_words(text):
"""
def replace(match):
- if match.group('tag'):
+ if match.group("tag"):
return match.group()
- elif match.group('longword'):
+ elif match.group("longword"):
return add_word_breaks(match.group())
else:
- raise AssertionError('text matched but neither named group found')
+ raise AssertionError("text matched but neither named group found")
+
return break_text_pat.sub(replace, text)
def extract_bug_numbers(text):
- '''Unique bug numbers matching the "LP: #n(, #n)*" pattern in the text.'''
+ """Unique bug numbers matching the "LP: #n(, #n)*" pattern in the text."""
# FormattersAPI._linkify_substitution requires a match object
# that has named groups "bug" and "bugnum". The matching text for
# the "bug" group is used as the link text and "bugnum" forms part
@@ -199,16 +193,16 @@ def extract_bug_numbers(text):
unique_bug_matches = dict()
line_matches = re.finditer(
- r'LP:\s*(?P<buglist>(.+?[^,]))($|\n)', text,
- re.DOTALL | re.IGNORECASE)
+ r"LP:\s*(?P<buglist>(.+?[^,]))($|\n)", text, re.DOTALL | re.IGNORECASE
+ )
for line_match in line_matches:
bug_matches = re.finditer(
- r'\s*((?P<bug>#(?P<bugnum>\d+)),?\s*)',
- line_match.group('buglist'))
+ r"\s*((?P<bug>#(?P<bugnum>\d+)),?\s*)", line_match.group("buglist")
+ )
for bug_match in bug_matches:
- bugnum = bug_match.group('bugnum')
+ bugnum = bug_match.group("bugnum")
if bugnum in unique_bug_matches:
# We got this bug already, ignore it.
continue
@@ -221,7 +215,7 @@ def linkify_bug_numbers(text):
"""Linkify to a bug if LP: #number appears in the (changelog) text."""
unique_bug_matches = extract_bug_numbers(text)
for bug_match in unique_bug_matches.values():
- replace_text = bug_match.group('bug')
+ replace_text = bug_match.group("bug")
if replace_text is not None:
# XXX julian 2008-01-10
# Note that re.sub would be far more efficient to use
@@ -235,13 +229,13 @@ def linkify_bug_numbers(text):
# #999. The liklihood of this happening is very, very
# small, however.
text = text.replace(
- replace_text,
- FormattersAPI._linkify_substitution(bug_match))
+ replace_text, FormattersAPI._linkify_substitution(bug_match)
+ )
return text
def extract_email_addresses(text):
- '''Unique email addresses in the text.'''
+ """Unique email addresses in the text."""
matches = re.finditer(re_email_address, text)
return list({match.group() for match in matches})
@@ -258,18 +252,20 @@ def parse_diff(text):
mod_row = 0
for row, line in enumerate(text.splitlines()[:max_format_lines]):
row += 1
- if (line.startswith('===') or
- line.startswith('diff') or
- line.startswith('index')):
- yield 'diff-file text', row, None, None, line
+ if (
+ line.startswith("===")
+ or line.startswith("diff")
+ or line.startswith("index")
+ ):
+ yield "diff-file text", row, None, None, line
header_next = True
- elif (header_next and
- (line.startswith('+++') or
- line.startswith('---'))):
- yield 'diff-header text', row, None, None, line
- elif line.startswith('@@'):
+ elif header_next and (
+ line.startswith("+++") or line.startswith("---")
+ ):
+ yield "diff-header text", row, None, None, line
+ elif line.startswith("@@"):
try:
- hunk = hunk_from_header((line + '\n').encode('UTF-8'))
+ hunk = hunk_from_header((line + "\n").encode("UTF-8"))
# The positions indicate the per-file line numbers of
# the next row.
orig_row = hunk.orig_pos
@@ -278,22 +274,22 @@ def parse_diff(text):
getUtility(IErrorReportingUtility).raising(sys.exc_info())
orig_row = 1
mod_row = 1
- yield 'diff-chunk text', row, None, None, line
+ yield "diff-chunk text", row, None, None, line
header_next = False
- elif line.startswith('+'):
- yield 'diff-added text', row, None, mod_row, line
+ elif line.startswith("+"):
+ yield "diff-added text", row, None, mod_row, line
mod_row += 1
- elif line.startswith('-'):
- yield 'diff-removed text', row, orig_row, None, line
+ elif line.startswith("-"):
+ yield "diff-removed text", row, orig_row, None, line
orig_row += 1
- elif line.startswith('#'):
+ elif line.startswith("#"):
# This doesn't occur in normal unified diffs, but does
# appear in merge directives, which use text/x-diff or
# text/x-patch.
- yield 'diff-comment text', row, None, None, line
+ yield "diff-comment text", row, None, None, line
header_next = False
else:
- yield 'text', row, orig_row, mod_row, line
+ yield "text", row, orig_row, mod_row, line
orig_row += 1
mod_row += 1
header_next = False
@@ -308,7 +304,7 @@ class FormattersAPI:
def nl_to_br(self):
"""Quote HTML characters, then replace newlines with <br /> tags."""
- return html_escape(self._stringtoformat).replace('\n', '<br />\n')
+ return html_escape(self._stringtoformat).replace("\n", "<br />\n")
def escape(self):
return html_escape(self._stringtoformat)
@@ -328,16 +324,15 @@ class FormattersAPI:
"""
groups = match.groups()
assert len(groups) == 1
- return ' ' * len(groups[0])
+ return " " * len(groups[0])
@staticmethod
- def _linkify_bug_number(text, bugnum, trailers=''):
+ def _linkify_bug_number(text, bugnum, trailers=""):
# Don't look up the bug or display anything about the bug, just
# linkify to the general bug url.
- url = '/bugs/%s' % bugnum
+ url = "/bugs/%s" % bugnum
# The text will have already been cgi escaped.
- return '<a href="%s" class="bug-link">%s</a>%s' % (
- url, text, trailers)
+ return '<a href="%s" class="bug-link">%s</a>%s' % (url, text, trailers)
@staticmethod
def _handle_parens_in_trailers(url, trailers):
@@ -346,13 +341,13 @@ class FormattersAPI:
If there are opening parens in the url that are matched by closing
parens at the start of the trailer, those closing parens should be
part of the url."""
- assert trailers != '', ("Trailers must not be an empty string.")
- opencount = url.count('(')
- closedcount = url.count(')')
+ assert trailers != "", "Trailers must not be an empty string."
+ opencount = url.count("(")
+ closedcount = url.count(")")
missing = opencount - closedcount
slice_idx = 0
while slice_idx < missing:
- if trailers[slice_idx] == ')':
+ if trailers[slice_idx] == ")":
slice_idx += 1
else:
break
@@ -369,47 +364,49 @@ class FormattersAPI:
match = FormattersAPI._re_url_trailers.search(url)
if match:
trailers = match.group(1)
- url = url[:-len(trailers)]
+ url = url[: -len(trailers)]
return FormattersAPI._handle_parens_in_trailers(url, trailers)
else:
# No match, return URL with empty string for trailers
- return url, ''
+ return url, ""
@staticmethod
def _linkify_url_should_be_ignored(url):
"""Don't linkify URIs consisting of just the protocol."""
protocol_bases = [
- 'about',
- 'gopher',
- 'http',
- 'https',
- 'sftp',
- 'news',
- 'ftp',
- 'mailto',
- 'irc',
- 'jabber',
- 'apt',
- ]
+ "about",
+ "gopher",
+ "http",
+ "https",
+ "sftp",
+ "news",
+ "ftp",
+ "mailto",
+ "irc",
+ "jabber",
+ "apt",
+ ]
for base in protocol_bases:
- if url in ('%s' % base, '%s:' % base, '%s://' % base):
+ if url in ("%s" % base, "%s:" % base, "%s://" % base):
return True
return False
@staticmethod
def _linkify_substitution(match):
- if match.group('bug') is not None:
+ if match.group("bug") is not None:
return FormattersAPI._linkify_bug_number(
- match.group('bug'), match.group('bugnum'))
- elif match.group('url') is not None:
+ match.group("bug"), match.group("bugnum")
+ )
+ elif match.group("url") is not None:
# The text will already have been cgi escaped. We temporarily
# unescape it so that we can strip common trailing characters
# that aren't part of the URL.
- full_url = match.group('url')
+ full_url = match.group("url")
url, trailers = FormattersAPI._split_url_and_trailers(
- html_unescape(full_url))
+ html_unescape(full_url)
+ )
# We use nofollow for these links to reduce the value of
# adding spam URLs to our comments; it's a way of moderately
# devaluing the return on effort for spammers that consider
@@ -422,56 +419,67 @@ class FormattersAPI:
'href="%(url)s">%(linked_text)s</a>%(trailers)s',
url=url,
linked_text=structured(add_word_breaks(html_escape(url))),
- trailers=trailers).escapedtext
+ trailers=trailers,
+ ).escapedtext
else:
return full_url
- elif match.group('faq') is not None:
+ elif match.group("faq") is not None:
# This is *BAD*. We shouldn't be doing database lookups to
# linkify text.
- text = match.group('faq')
- faqnum = match.group('faqnum')
+ text = match.group("faq")
+ faqnum = match.group("faqnum")
faqset = getUtility(IFAQSet)
faq = faqset.getFAQ(faqnum)
if not faq:
return text
url = canonical_url(faq)
return '<a href="%s">%s</a>' % (url, text)
- elif match.group('oops') is not None:
- text = match.group('oops')
+ elif match.group("oops") is not None:
+ text = match.group("oops")
if not getUtility(ILaunchBag).developer:
return text
root_url = config.launchpad.oops_root_url
- url = root_url + "OOPS-" + match.group('oopscode')
+ url = root_url + "OOPS-" + match.group("oopscode")
return '<a href="%s">%s</a>' % (url, text)
- elif match.group('lpbranchurl') is not None:
- lp_url = match.group('lpbranchurl')
- path = match.group('branch')
+ elif match.group("lpbranchurl") is not None:
+ lp_url = match.group("lpbranchurl")
+ path = match.group("branch")
lp_url, trailers = FormattersAPI._split_url_and_trailers(
- html_unescape(lp_url))
+ html_unescape(lp_url)
+ )
path, trailers = FormattersAPI._split_url_and_trailers(
- html_unescape(path))
+ html_unescape(path)
+ )
if path.isdigit():
return FormattersAPI._linkify_bug_number(
- lp_url, path, trailers)
- url = '/+code/%s' % path
+ lp_url, path, trailers
+ )
+ url = "/+code/%s" % path
# Mark the links with a 'branch-short-link' class so they can be
# harvested and validated when the page is rendered.
return structured(
'<a href="%s" class="branch-short-link">%s</a>%s',
- url, lp_url, trailers).escapedtext
+ url,
+ lp_url,
+ trailers,
+ ).escapedtext
elif match.group("clbug") is not None:
# 'clbug' matches Ubuntu changelog format bugs. 'bugnumbers' is
# all of the bug numbers, that look something like "#1234, #434".
# 'leader' is the 'LP: ' bit at the beginning.
bug_parts = []
# Split the bug numbers into multiple bugs.
- splitted = re.split(r"(,(?:\s|<br\s*/>)+)",
- match.group("bugnumbers")) + [""]
+ splitted = re.split(
+ r"(,(?:\s|<br\s*/>)+)", match.group("bugnumbers")
+ ) + [""]
for bug_id, spacer in zip(splitted[::2], splitted[1::2]):
- bug_parts.append(FormattersAPI._linkify_bug_number(
- bug_id, bug_id.lstrip("#")))
+ bug_parts.append(
+ FormattersAPI._linkify_bug_number(
+ bug_id, bug_id.lstrip("#")
+ )
+ )
bug_parts.append(spacer)
return match.group("leader") + "".join(bug_parts)
else:
@@ -486,8 +494,8 @@ class FormattersAPI:
"""
linkified_text = FormattersAPI._linkify_substitution(match)
element_tree = html.fromstring(linkified_text)
- for link in element_tree.xpath('//a'):
- link.set('target', '_new')
+ for link in element_tree.xpath("//a"):
+ link.set("target", "_new")
# html.tostring returns bytes; we want text. (Passing
# encoding='unicode' would cause it to return text, but that would
# also disable "&#...;" character encoding of non-ASCII characters,
@@ -495,7 +503,7 @@ class FormattersAPI:
return six.ensure_text(html.tostring(element_tree))
# match whitespace at the beginning of a line
- _re_leadingspace = re.compile(r'^(\s+)')
+ _re_leadingspace = re.compile(r"^(\s+)")
# From RFC 3986 ABNF for URIs:
#
@@ -558,7 +566,8 @@ class FormattersAPI:
# it's probably best to accomodate them.
# Match urls or bugs or oopses.
- _re_linkify = re.compile(r'''
+ _re_linkify = re.compile(
+ r"""
(?P<url>
\b
(?:about|gopher|http|https|sftp|news|ftp|mailto|irc|jabber|apt)
@@ -624,17 +633,23 @@ class FormattersAPI:
\blp:(?:///|/)?
(?P<branch>%(unreserved)s(?:%(unreserved)s|/)*)
)
- ''' % {'unreserved': r"(?:[-a-zA-Z0-9._~%!$'()*+,;=]|&|&\#x27;)"},
- re.IGNORECASE | re.VERBOSE)
+ """
+ % {"unreserved": r"(?:[-a-zA-Z0-9._~%!$'()*+,;=]|&|&\#x27;)"},
+ re.IGNORECASE | re.VERBOSE,
+ )
# There is various punctuation that can occur at the end of a link that
# shouldn't be included. The regex below matches on the set of characters
# we don't generally want. See also _handle_parens_in_trailers, which
# re-attaches parens if we do want them to be part of the url.
- _re_url_trailers = re.compile(r'([,.?:);>]+)$')
-
- def text_to_html(self, linkify_text=True, linkify_substitution=None,
- last_paragraph_class=None):
+ _re_url_trailers = re.compile(r"([,.?:);>]+)$")
+
+ def text_to_html(
+ self,
+ linkify_text=True,
+ linkify_substitution=None,
+ last_paragraph_class=None,
+ ):
"""Quote text according to DisplayingParagraphsOfText."""
# This is based on the algorithm in the
# DisplayingParagraphsOfText spec, but is a little more
@@ -650,33 +665,35 @@ class FormattersAPI:
last_paragraph_index = len(paras) - 1
for index, para in enumerate(paras):
if index > 0:
- output.append('\n')
- css_class = ''
+ output.append("\n")
+ css_class = ""
if last_paragraph_class and index == last_paragraph_index:
css_class = ' class="%s"' % last_paragraph_class
- output.append('<p%s>' % css_class)
+ output.append("<p%s>" % css_class)
first_line = True
for line in para:
if not first_line:
- output.append('<br />\n')
+ output.append("<br />\n")
first_line = False
# escape ampersands, etc in text
line = html_escape(line)
# convert leading space in logical line to non-breaking space
line = self._re_leadingspace.sub(
- self._substitute_matchgroup_for_spaces, line)
+ self._substitute_matchgroup_for_spaces, line
+ )
output.append(line)
- output.append('</p>')
+ output.append("</p>")
- text = ''.join(output)
+ text = "".join(output)
# Linkify the text, if allowed.
if linkify_text is True:
if linkify_substitution is None:
linkify_substitution = self._linkify_substitution
- text = re_substitute(self._re_linkify, linkify_substitution,
- break_long_words, text)
+ text = re_substitute(
+ self._re_linkify, linkify_substitution, break_long_words, text
+ )
return text
@@ -687,7 +704,8 @@ class FormattersAPI:
"""
return self.text_to_html(
linkify_text=True,
- linkify_substitution=self._linkify_substitution_with_target)
+ linkify_substitution=self._linkify_substitution_with_target,
+ )
def nice_pre(self):
"""<pre>, except the browser knows it is allowed to break long lines
@@ -702,9 +720,12 @@ class FormattersAPI:
if not self._stringtoformat:
return self._stringtoformat
else:
- linkified_text = re_substitute(self._re_linkify,
- self._linkify_substitution, break_long_words,
- html_escape(self._stringtoformat))
+ linkified_text = re_substitute(
+ self._re_linkify,
+ self._linkify_substitution,
+ break_long_words,
+ html_escape(self._stringtoformat),
+ )
return '<pre class="wrap">%s</pre>' % linkified_text
# Match lines that start with one or more quote symbols followed
@@ -713,11 +734,11 @@ class FormattersAPI:
# '> > ' are valid quoting sequences.
# The dpkg version is used for exceptional cases where it
# is better to not assume '|' is a start of a quoted passage.
- _re_quoted = re.compile('^(([|] ?)+|(> ?)+)')
- _re_dpkg_quoted = re.compile('^(> ?)+ ')
+ _re_quoted = re.compile("^(([|] ?)+|(> ?)+)")
+ _re_dpkg_quoted = re.compile("^(> ?)+ ")
# Match blocks that start as signatures or PGP inclusions.
- _re_include = re.compile('^<p>(--<br />|-----BEGIN PGP)')
+ _re_include = re.compile("^<p>(--<br />|-----BEGIN PGP)")
def email_to_html(self):
"""text_to_html and hide signatures and full-quoted emails.
@@ -731,7 +752,7 @@ class FormattersAPI:
"""
start_fold_markup = '<span class="foldable">'
start_fold_quoted_markup = '<span class="foldable-quoted">'
- end_fold_markup = '%s\n</span></p>'
+ end_fold_markup = "%s\n</span></p>"
re_quoted = self._re_quoted
re_include = self._re_include
output = []
@@ -747,17 +768,20 @@ class FormattersAPI:
that next and previous lines of text consistently uses '>>> '
as Python would.
"""
- python_block = '>>> '
- return (not line.startswith(python_block)
- and re_quoted.match(line) is not None)
+ python_block = ">>> "
+ return (
+ not line.startswith(python_block)
+ and re_quoted.match(line) is not None
+ )
def strip_leading_p_tag(line):
"""Return the characters after the paragraph mark (<p>).
The caller must be certain the line starts with a paragraph mark.
"""
- assert line.startswith('<p>'), (
- "The line must start with a paragraph mark (<p>).")
+ assert line.startswith(
+ "<p>"
+ ), "The line must start with a paragraph mark (<p>)."
return line[3:]
def strip_trailing_p_tag(line):
@@ -765,12 +789,13 @@ class FormattersAPI:
The caller must be certain the line ends with a paragraph mark.
"""
- assert line.endswith('</p>'), (
- "The line must end with a paragraph mark (</p>).")
+ assert line.endswith(
+ "</p>"
+ ), "The line must end with a paragraph mark (</p>)."
return line[:-4]
- for line in self.text_to_html().split('\n'):
- if 'Desired=<wbr />Unknown/' in line and not in_fold:
+ for line in self.text_to_html().split("\n"):
+ if "Desired=<wbr />Unknown/" in line and not in_fold:
# When we see a evidence of dpkg output, we switch the
# quote matching rules. We do not assume lines that start
# with a pipe are quoted passages. dpkg output is often
@@ -782,15 +807,22 @@ class FormattersAPI:
# This line is a paragraph with a signature or PGP inclusion.
# Start a foldable paragraph.
in_fold = True
- line = '<p>%s%s' % (start_fold_markup,
- strip_leading_p_tag(line))
- elif (not in_fold and line.startswith('<p>')
- and is_quoted(strip_leading_p_tag(line))):
+ line = "<p>%s%s" % (
+ start_fold_markup,
+ strip_leading_p_tag(line),
+ )
+ elif (
+ not in_fold
+ and line.startswith("<p>")
+ and is_quoted(strip_leading_p_tag(line))
+ ):
# The paragraph starts with quoted marks.
# Start a foldable quoted paragraph.
in_fold = True
- line = '<p>%s%s' % (
- start_fold_quoted_markup, strip_leading_p_tag(line))
+ line = "<p>%s%s" % (
+ start_fold_quoted_markup,
+ strip_leading_p_tag(line),
+ )
elif not in_fold and is_quoted(line):
# This line in the paragraph is quoted.
# Start foldable quoted lines in a paragraph.
@@ -804,23 +836,23 @@ class FormattersAPI:
# We must test line starts and ends in separate blocks to
# close the rare single line that is foldable.
- if in_fold and line.endswith('</p>') and in_false_paragraph:
+ if in_fold and line.endswith("</p>") and in_false_paragraph:
# The line ends with a false paragraph in a PGP signature.
# Restore the line break to join with the next paragraph.
- line = '%s<br />\n<br />' % strip_trailing_p_tag(line)
- elif (in_quoted and self._re_quoted.match(line) is None):
+ line = "%s<br />\n<br />" % strip_trailing_p_tag(line)
+ elif in_quoted and self._re_quoted.match(line) is None:
# The line is not quoted like the previous line.
# End fold before we append this line.
in_fold = False
in_quoted = False
output.append("</span>\n")
- elif in_fold and line.endswith('</p>'):
+ elif in_fold and line.endswith("</p>"):
# The line is quoted or an inclusion, and ends the paragraph.
# End the fold before the close paragraph mark.
in_fold = False
in_quoted = False
line = end_fold_markup % strip_trailing_p_tag(line)
- elif in_false_paragraph and line.startswith('<p>'):
+ elif in_false_paragraph and line.startswith("<p>"):
# This line continues a PGP signature, but starts a paragraph.
# Remove the paragraph to join with the previous paragraph.
in_false_paragraph = False
@@ -830,7 +862,7 @@ class FormattersAPI:
# This line is not a transition.
pass
- if in_fold and 'PGP SIGNATURE' in line:
+ if in_fold and "PGP SIGNATURE" in line:
# PGP signature blocks are split into two paragraphs
# by the text_to_html. The foldable feature works with
# a single paragraph, so we merge this paragraph with
@@ -838,7 +870,7 @@ class FormattersAPI:
in_false_paragraph = True
output.append(line)
- return '\n'.join(output)
+ return "\n".join(output)
def obfuscate_email(self):
"""Obfuscate an email address if there's no authenticated user.
@@ -903,11 +935,14 @@ class FormattersAPI:
# Circular dependancies now. Should be resolved by moving the
# object image display api.
from lp.app.browser.tales import ObjectImageDisplayAPI
+
css_sprite = ObjectImageDisplayAPI(person).sprite_css()
seen_addresses.append(address)
text = text.replace(
- address, '<a href="%s" class="%s">%s</a>' % (
- canonical_url(person), css_sprite, address))
+ address,
+ '<a href="%s" class="%s">%s</a>'
+ % (canonical_url(person), css_sprite, address),
+ )
return text
@@ -918,7 +953,7 @@ class FormattersAPI:
def shorten(self, maxlength):
"""Use like tal:content="context/foo/fmt:shorten/60"."""
if len(self._stringtoformat) > maxlength:
- return '%s...' % self._stringtoformat[:maxlength - 3]
+ return "%s..." % self._stringtoformat[: maxlength - 3]
else:
return self._stringtoformat
@@ -927,15 +962,17 @@ class FormattersAPI:
if len(self._stringtoformat) > maxlength:
length = (maxlength - 3) // 2
return (
- self._stringtoformat[:maxlength - length - 3] + '...' +
- self._stringtoformat[-length:])
+ self._stringtoformat[: maxlength - length - 3]
+ + "..."
+ + self._stringtoformat[-length:]
+ )
else:
return self._stringtoformat
def format_diff(self):
"""Format the string as a diff in a table with line numbers."""
# Trim off trailing carriage returns.
- text = self._stringtoformat.rstrip('\n')
+ text = self._stringtoformat.rstrip("\n")
if len(text) == 0:
return text
result = ['<table class="diff unidiff">']
@@ -945,11 +982,13 @@ class FormattersAPI:
result.append('<td class="line-no unselectable">%s</td>' % row)
result.append(
structured(
- '<td class="%s">%s</td>', css_class, line).escapedtext)
- result.append('</tr>')
+ '<td class="%s">%s</td>', css_class, line
+ ).escapedtext
+ )
+ result.append("</tr>")
- result.append('</table>')
- return ''.join(result)
+ result.append("</table>")
+ return "".join(result)
def _ssdiff_emit_line(self, result, row, cells):
result.append('<tr id="diff-line-%s">' % row)
@@ -958,40 +997,42 @@ class FormattersAPI:
# per-file line numbers.
result.append(
'<td class="line-no unselectable" '
- 'style="display: none">%s</td>' % row)
+ 'style="display: none">%s</td>' % row
+ )
result.extend(cells)
- result.append('</tr>')
+ result.append("</tr>")
def _ssdiff_emit_queued_lines(self, result, queue_removed, queue_added):
for removed, added in zip_longest(queue_removed, queue_added):
if removed:
removed_diff_row, removed_row, removed_line = removed
else:
- removed_diff_row, removed_row, removed_line = 0, '', ''
+ removed_diff_row, removed_row, removed_line = 0, "", ""
if added:
added_diff_row, added_row, added_line = added
else:
- added_diff_row, added_row, added_line = 0, '', ''
+ added_diff_row, added_row, added_line = 0, "", ""
cells = (
'<td class="ss-line-no unselectable">%s</td>' % removed_row,
structured(
- '<td class="diff-removed text">%s</td>',
- removed_line).escapedtext,
+ '<td class="diff-removed text">%s</td>', removed_line
+ ).escapedtext,
'<td class="ss-line-no unselectable">%s</td>' % added_row,
structured(
- '<td class="diff-added text">%s</td>',
- added_line).escapedtext,
- )
+ '<td class="diff-added text">%s</td>', added_line
+ ).escapedtext,
+ )
# Pick a reasonable row of the unified diff to attribute inline
# comments to. Whichever of the removed and added rows is later
# will do.
self._ssdiff_emit_line(
- result, max(removed_diff_row, added_diff_row), cells)
+ result, max(removed_diff_row, added_diff_row), cells
+ )
def format_ssdiff(self):
"""Format the string as a side-by-side diff."""
# Trim off trailing carriage returns.
- text = self._stringtoformat.rstrip('\n')
+ text = self._stringtoformat.rstrip("\n")
if not text:
return text
result = ['<table class="diff ssdiff">']
@@ -1012,7 +1053,8 @@ class FormattersAPI:
# this line.
if queue_orig or queue_mod:
self._ssdiff_emit_queued_lines(
- result, queue_orig, queue_mod)
+ result, queue_orig, queue_mod
+ )
queue_orig = []
queue_mod = []
if orig_row is None and mod_row is None:
@@ -1020,31 +1062,35 @@ class FormattersAPI:
cells = [
structured(
'<td class="%s" colspan="4">%s</td>',
- css_class, line).escapedtext,
- ]
+ css_class,
+ line,
+ ).escapedtext,
+ ]
else:
# Line common to both versions.
- if line.startswith(' '):
+ if line.startswith(" "):
line = line[1:]
cells = [
- '<td class="ss-line-no unselectable">%s</td>' %
- orig_row,
+ '<td class="ss-line-no unselectable">%s</td>'
+ % orig_row,
structured(
- '<td class="text">%s</td>', line).escapedtext,
- '<td class="ss-line-no unselectable">%s</td>' %
- mod_row,
+ '<td class="text">%s</td>', line
+ ).escapedtext,
+ '<td class="ss-line-no unselectable">%s</td>'
+ % mod_row,
structured(
- '<td class="text">%s</td>', line).escapedtext,
- ]
+ '<td class="text">%s</td>', line
+ ).escapedtext,
+ ]
self._ssdiff_emit_line(result, row, cells)
if queue_orig or queue_mod:
self._ssdiff_emit_queued_lines(result, queue_orig, queue_mod)
- result.append('</table>')
- return ''.join(result)
+ result.append("</table>")
+ return "".join(result)
- _css_id_strip_pattern = re.compile(r'[^a-zA-Z0-9-_]+')
- _zope_css_id_strip_pattern = re.compile(r'[^a-zA-Z0-9-_\.]+')
+ _css_id_strip_pattern = re.compile(r"[^a-zA-Z0-9-_]+")
+ _zope_css_id_strip_pattern = re.compile(r"[^a-zA-Z0-9-_\.]+")
def css_id(self, prefix=None):
"""Return a CSS compliant id."""
@@ -1074,7 +1120,7 @@ class FormattersAPI:
raw_text = prefix + self._stringtoformat
else:
raw_text = self._stringtoformat
- id_ = strip_pattern.sub('-', raw_text)
+ id_ = strip_pattern.sub("-", raw_text)
# If any characters are converted to a hyphen, we cannot be 100%
# assured of a unique id unless we take further action. Note that we
@@ -1086,17 +1132,18 @@ class FormattersAPI:
# we also have to strip off any padding characters ("=") because
# Python's URL-safe base 64 encoding includes those and they
# aren't allowed in IDs either.
- unique_suffix = (
- urlsafe_b64encode(raw_text.encode('ASCII')).decode('ASCII'))
+ unique_suffix = urlsafe_b64encode(raw_text.encode("ASCII")).decode(
+ "ASCII"
+ )
# Ensure we put a '-' between the id and base 64 encoding.
- if id_[-1] != '-':
- id_ += '-'
- id_ += unique_suffix.replace('=', '')
+ if id_[-1] != "-":
+ id_ += "-"
+ id_ += unique_suffix.replace("=", "")
- if id_[0] in '-_0123456789':
+ if id_[0] in "-_0123456789":
# 'j' is least common starting character in technical usage;
# engineers love 'z', 'q', and 'y'.
- return 'j' + id_
+ return "j" + id_
else:
return id_
@@ -1111,63 +1158,65 @@ class FormattersAPI:
return '<a href="%s">%s</a>' % (url, self._stringtoformat)
def markdown(self):
- if getFeatureFlag('markdown.enabled'):
+ if getFeatureFlag("markdown.enabled"):
return format_markdown(self._stringtoformat)
else:
return self.text_to_html()
def traverse(self, name, furtherPath):
- if name == 'nl_to_br':
+ if name == "nl_to_br":
return self.nl_to_br()
- elif name == 'escape':
+ elif name == "escape":
return self.escape()
- elif name == 'lower':
+ elif name == "lower":
return self.lower()
- elif name == 'break-long-words':
+ elif name == "break-long-words":
return self.break_long_words()
- elif name == 'markdown':
+ elif name == "markdown":
return self.markdown()
- elif name == 'text-to-html':
+ elif name == "text-to-html":
return self.text_to_html()
- elif name == 'text-to-html-with-target':
+ elif name == "text-to-html-with-target":
return self.text_to_html_with_target()
- elif name == 'nice_pre':
+ elif name == "nice_pre":
return self.nice_pre()
- elif name == 'email-to-html':
+ elif name == "email-to-html":
return self.email_to_html()
- elif name == 'obfuscate-email':
+ elif name == "obfuscate-email":
return self.obfuscate_email()
- elif name == 'strip-email':
+ elif name == "strip-email":
return self.strip_email()
- elif name == 'linkify-email':
+ elif name == "linkify-email":
return self.linkify_email()
- elif name == 'shorten':
+ elif name == "shorten":
if len(furtherPath) == 0:
raise TraversalError(
- "you need to traverse a number after fmt:shorten")
+ "you need to traverse a number after fmt:shorten"
+ )
maxlength = int(furtherPath.pop())
return self.shorten(maxlength)
- elif name == 'ellipsize':
+ elif name == "ellipsize":
if len(furtherPath) == 0:
raise TraversalError(
- "you need to traverse a number after fmt:ellipsize")
+ "you need to traverse a number after fmt:ellipsize"
+ )
maxlength = int(furtherPath.pop())
return self.ellipsize(maxlength)
- elif name == 'diff':
+ elif name == "diff":
return self.format_diff()
- elif name == 'ssdiff':
+ elif name == "ssdiff":
return self.format_ssdiff()
- elif name == 'css-id':
+ elif name == "css-id":
if len(furtherPath) > 0:
return self.css_id(furtherPath.pop())
else:
return self.css_id()
- elif name == 'zope-css-id':
+ elif name == "zope-css-id":
if len(furtherPath) > 0:
return self.zope_css_id(furtherPath.pop())
else:
return self.zope_css_id()
- elif name == 'oops-id':
+ elif name == "oops-id":
return self.oops_id()
else:
raise TraversalError(name)
@@ -1177,9 +1226,10 @@ def format_markdown(text):
"""Return html form of marked-up text."""
# This returns whole paragraphs (in p tags), similarly to text_to_html.
md = markdown.Markdown(
- safe_mode='escape',
+ safe_mode="escape",
extensions=[
- 'tables',
- 'nl2br',
- ])
+ "tables",
+ "nl2br",
+ ],
+ )
return md.convert(text) # How easy was that?
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
index cfe0af0..fe62ec3 100644
--- a/lib/lp/app/browser/tales.py
+++ b/lib/lp/app/browser/tales.py
@@ -3,38 +3,23 @@
"""Implementation of the lp: htmlform: fmt: namespaces in TALES."""
-from bisect import bisect
-from datetime import (
- datetime,
- timedelta,
- )
-from email.utils import (
- formatdate,
- mktime_tz,
- )
import math
import os.path
import sys
+from bisect import bisect
+from datetime import datetime, timedelta
+from email.utils import formatdate, mktime_tz
from textwrap import dedent
from urllib.parse import quote
+import pytz
from lazr.enum import enumerated_type_registry
from lazr.restful.utils import get_current_browser_request
from lazr.uri import URI
-import pytz
from zope.browserpage import ViewPageTemplateFile
-from zope.component import (
- adapter,
- getMultiAdapter,
- getUtility,
- queryAdapter,
- )
+from zope.component import adapter, getMultiAdapter, getUtility, queryAdapter
from zope.error.interfaces import IErrorReportingUtility
-from zope.interface import (
- Attribute,
- implementer,
- Interface,
- )
+from zope.interface import Attribute, Interface, implementer
from zope.publisher.browser import BrowserView
from zope.publisher.defaultview import getDefaultViewName
from zope.schema import TextLine
@@ -44,7 +29,7 @@ from zope.traversing.interfaces import (
IPathAdapter,
ITraversable,
TraversalError,
- )
+)
from lp import _
from lp.app.browser.badge import IHasBadges
@@ -55,7 +40,7 @@ from lp.app.interfaces.launchpad import (
IHasLogo,
IHasMugshot,
IPrivacy,
- )
+)
from lp.blueprints.interfaces.specification import ISpecification
from lp.blueprints.interfaces.sprint import ISprint
from lp.bugs.interfaces.bug import IBug
@@ -66,7 +51,7 @@ from lp.layers import LaunchpadLayer
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
- )
+)
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.projectgroup import IProjectGroup
@@ -74,10 +59,7 @@ from lp.services.utils import round_half_up
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.canonicalurl import nearest_adapter
from lp.services.webapp.error import SystemErrorView
-from lp.services.webapp.escaping import (
- html_escape,
- structured,
- )
+from lp.services.webapp.escaping import html_escape, structured
from lp.services.webapp.interfaces import (
IApplicationMenu,
IContextMenu,
@@ -85,36 +67,25 @@ from lp.services.webapp.interfaces import (
ILaunchBag,
INavigationMenu,
NoCanonicalUrl,
- )
-from lp.services.webapp.menu import (
- get_current_view,
- get_facet,
- )
-from lp.services.webapp.publisher import (
- canonical_url,
- LaunchpadView,
- nearest,
- )
+)
+from lp.services.webapp.menu import get_current_view, get_facet
+from lp.services.webapp.publisher import LaunchpadView, canonical_url, nearest
from lp.services.webapp.session import get_cookie_domain
from lp.services.webapp.url import urlappend
from lp.snappy.interfaces.snap import SnapBuildRequestStatus
from lp.soyuz.enums import ArchivePurpose
-from lp.soyuz.interfaces.archive import (
- IArchive,
- IPPA,
- )
+from lp.soyuz.interfaces.archive import IPPA, IArchive
from lp.soyuz.interfaces.binarypackagename import IBinaryAndSourcePackageName
-
-SEPARATOR = ' : '
+SEPARATOR = " : "
-def format_link(obj, view_name=None, empty_value='None'):
+def format_link(obj, view_name=None, empty_value="None"):
"""Return the equivalent of obj/fmt:link as a string."""
if obj is None:
return empty_value
- fmt_adapter = queryAdapter(obj, IPathAdapter, 'fmt')
- link = getattr(fmt_adapter, 'link', None)
+ fmt_adapter = queryAdapter(obj, IPathAdapter, "fmt")
+ link = getattr(fmt_adapter, "link", None)
if link is None:
raise NotImplementedError("Missing link function on adapter.")
return link(view_name)
@@ -136,7 +107,7 @@ class MenuLinksDict(dict):
# The object has the facet, but does not have a menu, this
# is probably the overview menu with is the default facet.
- if menu is None or getattr(menu, 'disabled', False):
+ if menu is None or getattr(menu, "disabled", False):
return
menu.request = request
@@ -180,6 +151,7 @@ class MenuLinksDict(dict):
def iterkeys(self):
return iter(self._all_link_names)
+
__iter__ = iterkeys
@@ -203,7 +175,8 @@ class MenuAPI:
self._context = self.view.context
self._request = self.view.request
self._selectedfacetname = getattr(
- self.view, '__launchpad_facetname__', None)
+ self.view, "__launchpad_facetname__", None
+ )
else:
self._context = context
self._request = get_current_browser_request()
@@ -226,11 +199,12 @@ class MenuAPI:
# be placed first, since all the properties it uses would need to
# be retrieved with object.__getattribute().
missing = object()
- if (getattr(MenuAPI, facet, missing) is not missing
- or facet in object.__getattribute__(self, '__dict__')):
+ if getattr(
+ MenuAPI, facet, missing
+ ) is not missing or facet in object.__getattribute__(self, "__dict__"):
return object.__getattribute__(self, facet)
- has_facet = object.__getattribute__(self, '_has_facet')
+ has_facet = object.__getattribute__(self, "_has_facet")
if not has_facet(facet):
raise AttributeError(facet)
menu = queryAdapter(self._context, IApplicationMenu, facet)
@@ -259,9 +233,9 @@ class MenuAPI:
# If the default view name is being used, we will want the url
# without the default view name.
defaultviewname = getDefaultViewName(self._context, request)
- if request_urlobj.path.rstrip('/').endswith(defaultviewname):
+ if request_urlobj.path.rstrip("/").endswith(defaultviewname):
request_urlobj = URI(request.getURL(1))
- query = request.get('QUERY_STRING')
+ query = request.get("QUERY_STRING")
if query:
request_urlobj = request_urlobj.replace(query=query)
return request_urlobj
@@ -272,9 +246,12 @@ class MenuAPI:
if menu is None:
return []
menu.request = self._request
- return list(menu.iterlinks(
- request_url=self._request_url(),
- selectedfacetname=self._selectedfacetname))
+ return list(
+ menu.iterlinks(
+ request_url=self._request_url(),
+ selectedfacetname=self._selectedfacetname,
+ )
+ )
def _facet_menu(self):
"""Return the IFacetMenu related to the context."""
@@ -283,6 +260,7 @@ class MenuAPI:
# ensuring that the facet tabs match what's above them. This
# whole class needs refactoring once the UI is stable.
from lp.app.browser.launchpad import Hierarchy
+
try:
context = self._context
crumbs = Hierarchy(context, self._request).heading_breadcrumbs
@@ -296,7 +274,7 @@ class MenuAPI:
def selectedfacetname(self):
if self._selectedfacetname is None:
- return 'unknown'
+ return "unknown"
else:
return self._selectedfacetname
@@ -322,7 +300,8 @@ class MenuAPI:
selectedfacetname = get_facet(view)
try:
menu = nearest_adapter(
- context, INavigationMenu, name=selectedfacetname)
+ context, INavigationMenu, name=selectedfacetname
+ )
except NoCanonicalUrl:
menu = None
return self._getMenuLinksAndAttributes(menu)
@@ -331,7 +310,8 @@ class MenuAPI:
# AssertionError. Otherwise, zope will hide the root cause
# of the error and just say that "navigation" can't be traversed.
new_exception = AssertionError(
- 'AttributError in MenuAPI.navigation: %s' % e)
+ "AttributError in MenuAPI.navigation: %s" % e
+ )
# We cannot use parens around the arguments to `raise`,
# since that will cause it to ignore the third argument,
# which is the original traceback.
@@ -379,8 +359,9 @@ class EnumValueAPI:
enum.getTermByToken(name)
except LookupError:
raise TraversalError(
- 'The enumerated type %s does not have a value %s.' %
- (enum.name, name))
+ "The enumerated type %s does not have a value %s."
+ % (enum.name, name)
+ )
return False
@@ -425,7 +406,6 @@ def htmlmatch(formvalue, value):
@implementer(ITraversable)
class HTMLFormOperation:
-
def __init__(self, formvalue, operation):
self.formvalue = formvalue
self.operation = operation
@@ -457,13 +437,13 @@ class RequestAPI:
@property
def cookie_scope(self):
- params = '; Path=/'
+ params = "; Path=/"
uri = URI(self.request.getURL())
- if uri.scheme == 'https':
- params += '; Secure'
+ if uri.scheme == "https":
+ params += "; Secure"
domain = get_cookie_domain(uri.host)
if domain is not None:
- params += '; Domain=%s' % domain
+ params += "; Domain=%s" % domain
return params
@@ -500,46 +480,47 @@ class NoneFormatter:
"""
allowed_names = {
- 'approximatedate',
- 'approximatedatetitle',
- 'approximateduration',
- 'break-long-words',
- 'date',
- 'datetime',
- 'displaydate',
- 'displaydatetitle',
- 'isodate',
- 'email-to-html',
- 'exactduration',
- 'lower',
- 'nice_pre',
- 'nl_to_br',
- 'pagetitle',
- 'rfc822utcdatetime',
- 'text-to-html',
- 'time',
- 'url',
- 'link',
- }
+ "approximatedate",
+ "approximatedatetitle",
+ "approximateduration",
+ "break-long-words",
+ "date",
+ "datetime",
+ "displaydate",
+ "displaydatetitle",
+ "isodate",
+ "email-to-html",
+ "exactduration",
+ "lower",
+ "nice_pre",
+ "nl_to_br",
+ "pagetitle",
+ "rfc822utcdatetime",
+ "text-to-html",
+ "time",
+ "url",
+ "link",
+ }
def __init__(self, context):
self.context = context
def traverse(self, name, furtherPath):
- if name == 'shorten':
+ if name == "shorten":
if not len(furtherPath):
raise TraversalError(
- "you need to traverse a number after fmt:shorten")
+ "you need to traverse a number after fmt:shorten"
+ )
# Remove the maxlength from the path as it is a parameter
# and not another traversal command.
furtherPath.pop()
- return ''
+ return ""
# We need to check to see if the name has been augmented with optional
# evaluation parameters, delimited by ":". These parameters are:
# param1 = rootsite (used with link and url)
# param2 = default value (in case of context being None)
# We are interested in the default value (param2).
- result = ''
+ result = ""
for nm in self.allowed_names:
if name.startswith(nm + ":"):
name_parts = name.split(":")
@@ -562,16 +543,16 @@ class ObjectFormatterAPI:
# frozenset (http://code.activestate.com/recipes/414283/) here, though.
# The names which can be traversed further (e.g context/fmt:url/+edit).
traversable_names = {
- 'api_url': 'api_url',
- 'link': 'link',
- 'url': 'url',
- }
+ "api_url": "api_url",
+ "link": "link",
+ "url": "url",
+ }
# Names which are allowed but can't be traversed further.
final_traversable_names = {
- 'pagetitle': 'pagetitle',
- 'global-css': 'global_css',
- }
+ "pagetitle": "pagetitle",
+ "global-css": "global_css",
+ }
def __init__(self, context):
self._context = context
@@ -587,8 +568,11 @@ class ObjectFormatterAPI:
"""
try:
url = canonical_url(
- self._context, path_only_if_possible=True,
- rootsite=rootsite, view_name=view_name)
+ self._context,
+ path_only_if_possible=True,
+ rootsite=rootsite,
+ view_name=view_name,
+ )
except Unauthorized:
url = ""
return url
@@ -617,18 +601,18 @@ class ObjectFormatterAPI:
param2 = default (used when self.context is None). The context
is not None here so this parameter is ignored.
"""
- if name.startswith('link:') or name.startswith('url:'):
- name_parts = name.split(':')
+ if name.startswith("link:") or name.startswith("url:"):
+ name_parts = name.split(":")
name = name_parts[0]
rootsite = name_parts[1]
- if rootsite != '':
+ if rootsite != "":
extra_path = None
if len(furtherPath) > 0:
- extra_path = '/'.join(reversed(furtherPath))
+ extra_path = "/".join(reversed(furtherPath))
# Remove remaining entries in furtherPath so that traversal
# stops here.
del furtherPath[:]
- if name == 'link':
+ if name == "link":
if rootsite is None:
return self.link(extra_path)
else:
@@ -638,11 +622,11 @@ class ObjectFormatterAPI:
self.url(extra_path)
else:
return self.url(extra_path, rootsite=rootsite)
- if '::' in name:
- name = name.split(':')[0]
+ if "::" in name:
+ name = name.split(":")[0]
if name in self.traversable_names:
if len(furtherPath) >= 1:
- extra_path = '/'.join(reversed(furtherPath))
+ extra_path = "/".join(reversed(furtherPath))
del furtherPath[:]
else:
extra_path = None
@@ -667,30 +651,31 @@ class ObjectFormatterAPI:
"""
raise NotImplementedError(
"No link implementation for %r, IPathAdapter implementation "
- "for %r." % (self, self._context))
+ "for %r." % (self, self._context)
+ )
def global_css(self):
css_classes = set()
view = self._context
# XXX: Bug #1076074
- private = getattr(view, 'private', False)
+ private = getattr(view, "private", False)
if private:
- css_classes.add('private')
+ css_classes.add("private")
else:
- css_classes.add('public')
- beta = getattr(view, 'beta_features', [])
+ css_classes.add("public")
+ beta = getattr(view, "beta_features", [])
if beta:
- css_classes.add('beta')
- return ' '.join(list(css_classes))
+ css_classes.add("beta")
+ return " ".join(list(css_classes))
def _getSaneBreadcrumbDetail(self, breadcrumb):
text = breadcrumb.detail
if len(text) > 64:
- truncated = '%s...' % text[0:64]
- if truncated.count('\u201c') > truncated.count('\u201cd'):
+ truncated = "%s..." % text[0:64]
+ if truncated.count("\u201c") > truncated.count("\u201cd"):
# Close the open smartquote if it was dropped.
- truncated += '\u201d'
+ truncated += "\u201d"
return truncated
return text
@@ -700,23 +685,25 @@ class ObjectFormatterAPI:
By default, reverse breadcrumbs are always used if they are available.
If not available, then the view's .page_title attribute is used.
"""
- ROOT_TITLE = 'Launchpad'
+ ROOT_TITLE = "Launchpad"
view = self._context
request = get_current_browser_request()
- hierarchy_view = getMultiAdapter((view, request), name='+hierarchy')
- if (isinstance(view, SystemErrorView) or
- hierarchy_view is None or
- len(hierarchy_view.items) < 2):
+ hierarchy_view = getMultiAdapter((view, request), name="+hierarchy")
+ if (
+ isinstance(view, SystemErrorView)
+ or hierarchy_view is None
+ or len(hierarchy_view.items) < 2
+ ):
# The breadcrumbs are either not available or are overridden. If
# the view has a .page_title attribute use that.
- page_title = getattr(view, 'page_title', None)
+ page_title = getattr(view, "page_title", None)
if page_title is not None:
return page_title
# If there is no template for the view, just use the default
# Launchpad title.
- template = getattr(view, 'template', None)
+ template = getattr(view, "template", None)
if template is None:
- template = getattr(view, 'index', None)
+ template = getattr(view, "index", None)
if template is None:
return ROOT_TITLE
# Use the reverse breadcrumbs.
@@ -724,7 +711,7 @@ class ObjectFormatterAPI:
if len(breadcrumbs) == 0:
# This implies there are no breadcrumbs, but this more often
# is caused when an Unauthorized error is being raised.
- return ''
+ return ""
detail_breadcrumb = self._getSaneBreadcrumbDetail(breadcrumbs[0])
title_breadcrumbs = [breadcrumb.text for breadcrumb in breadcrumbs[1:]]
title_text = SEPARATOR.join([detail_breadcrumb] + title_breadcrumbs)
@@ -739,7 +726,7 @@ class ObjectImageDisplayAPI:
def __init__(self, context):
self._context = context
- #def default_icon_resource(self, context):
+ # def default_icon_resource(self, context):
def sprite_css(self):
"""Return the CSS class for the sprite"""
# XXX: mars 2008-08-22 bug=260468
@@ -748,44 +735,44 @@ class ObjectImageDisplayAPI:
context = self._context
sprite_string = None
if IProduct.providedBy(context):
- sprite_string = 'product'
+ sprite_string = "product"
elif IProjectGroup.providedBy(context):
- sprite_string = 'project'
+ sprite_string = "project"
elif IPerson.providedBy(context):
if context.is_team:
- sprite_string = 'team'
+ sprite_string = "team"
else:
if context.is_valid_person:
- sprite_string = 'person'
+ sprite_string = "person"
else:
- sprite_string = 'person-inactive'
+ sprite_string = "person-inactive"
elif IDistribution.providedBy(context):
- sprite_string = 'distribution'
+ sprite_string = "distribution"
elif IDistributionSourcePackage.providedBy(context):
- sprite_string = 'package-source'
+ sprite_string = "package-source"
elif ISprint.providedBy(context):
- sprite_string = 'meeting'
+ sprite_string = "meeting"
elif IBug.providedBy(context):
- sprite_string = 'bug'
+ sprite_string = "bug"
elif IPPA.providedBy(context):
if context.enabled:
- sprite_string = 'ppa-icon'
+ sprite_string = "ppa-icon"
else:
- sprite_string = 'ppa-icon-inactive'
+ sprite_string = "ppa-icon-inactive"
elif IArchive.providedBy(context):
- sprite_string = 'distribution'
+ sprite_string = "distribution"
elif IBranch.providedBy(context):
- sprite_string = 'branch'
+ sprite_string = "branch"
elif ISpecification.providedBy(context):
- sprite_string = 'blueprint'
+ sprite_string = "blueprint"
elif IBinaryAndSourcePackageName.providedBy(context):
- sprite_string = 'package-source'
+ sprite_string = "package-source"
if sprite_string is None:
return None
else:
- if hasattr(context, 'private') and context.private:
- sprite_string = sprite_string + ' private'
+ if hasattr(context, "private") and context.private:
+ sprite_string = sprite_string + " private"
return "sprite %s" % sprite_string
@@ -794,21 +781,21 @@ class ObjectImageDisplayAPI:
# This should be refactored. We shouldn't have to do type-checking
# using interfaces.
if IProjectGroup.providedBy(context):
- return '/@@/project-logo'
+ return "/@@/project-logo"
elif IPerson.providedBy(context):
if context.is_team:
- return '/@@/team-logo'
+ return "/@@/team-logo"
else:
if context.is_valid_person:
- return '/@@/person-logo'
+ return "/@@/person-logo"
else:
- return '/@@/person-inactive-logo'
+ return "/@@/person-inactive-logo"
elif IProduct.providedBy(context):
- return '/@@/product-logo'
+ return "/@@/product-logo"
elif IDistribution.providedBy(context):
- return '/@@/distribution-logo'
+ return "/@@/distribution-logo"
elif ISprint.providedBy(context):
- return '/@@/meeting-logo'
+ return "/@@/meeting-logo"
return None
def default_mugshot_resource(self, context):
@@ -816,21 +803,21 @@ class ObjectImageDisplayAPI:
# This should be refactored. We shouldn't have to do type-checking
# using interfaces.
if IProjectGroup.providedBy(context):
- return '/@@/project-mugshot'
+ return "/@@/project-mugshot"
elif IPerson.providedBy(context):
if context.is_team:
- return '/@@/team-mugshot'
+ return "/@@/team-mugshot"
else:
if context.is_valid_person:
- return '/@@/person-mugshot'
+ return "/@@/person-mugshot"
else:
- return '/@@/person-inactive-mugshot'
+ return "/@@/person-inactive-mugshot"
elif IProduct.providedBy(context):
- return '/@@/product-mugshot'
+ return "/@@/product-mugshot"
elif IDistribution.providedBy(context):
- return '/@@/distribution-mugshot'
+ return "/@@/distribution-mugshot"
elif ISprint.providedBy(context):
- return '/@@/meeting-mugshot'
+ return "/@@/meeting-mugshot"
return None
def custom_icon_url(self):
@@ -840,12 +827,12 @@ class ObjectImageDisplayAPI:
icon_url = context.icon.getURL()
return icon_url
elif context is None:
- return ''
+ return ""
else:
return None
def icon(self):
- #XXX: this should go away as soon as all image:icon where replaced
+ # XXX: this should go away as soon as all image:icon where replaced
return None
def logo(self):
@@ -860,7 +847,7 @@ class ObjectImageDisplayAPI:
if context is None:
# we use the Launchpad logo for anything which is in no way
# related to a Pillar (for example, a buildfarm)
- url = '/@@/launchpad-logo'
+ url = "/@@/launchpad-logo"
elif context.logo is not None:
url = context.logo.getURL()
else:
@@ -879,7 +866,7 @@ class ObjectImageDisplayAPI:
a mugshot.
"""
context = self._context
- assert IHasMugshot.providedBy(context), 'No Mugshot for this item'
+ assert IHasMugshot.providedBy(context), "No Mugshot for this item"
if context.mugshot is not None:
url = context.mugshot.getURL()
else:
@@ -894,14 +881,15 @@ class ObjectImageDisplayAPI:
def badges(self):
raise NotImplementedError(
- "Badge display not implemented for this item")
+ "Badge display not implemented for this item"
+ )
def boolean(self):
"""Return an icon representing the context as a boolean value."""
if bool(self._context):
- icon = 'yes'
+ icon = "yes"
else:
- icon = 'no'
+ icon = "no"
markup = '<span class="sprite %(icon)s action-icon">%(icon)s</span>'
return markup % dict(icon=icon)
@@ -916,18 +904,16 @@ class BugTaskImageDisplayAPI(ObjectImageDisplayAPI):
"""
allowed_names = {
- 'icon',
- 'logo',
- 'mugshot',
- 'badges',
- 'sprite_css',
- }
+ "icon",
+ "logo",
+ "mugshot",
+ "badges",
+ "sprite_css",
+ }
- icon_template = (
- '<span alt="%s" title="%s" class="%s"></span>')
+ icon_template = '<span alt="%s" title="%s" class="%s"></span>'
- linked_icon_template = (
- '<a href="%s" alt="%s" title="%s" class="%s"></a>')
+ linked_icon_template = '<a href="%s" alt="%s" title="%s" class="%s"></a>'
def traverse(self, name, furtherPath):
"""Special-case traversal for icons with an optional rootsite."""
@@ -978,28 +964,44 @@ class BugTaskImageDisplayAPI(ObjectImageDisplayAPI):
badges = []
information_type = self._context.bug.information_type
if information_type in PRIVATE_INFORMATION_TYPES:
- badges.append(self.icon_template % (
- information_type.title, information_type.description,
- "sprite private"))
+ badges.append(
+ self.icon_template
+ % (
+ information_type.title,
+ information_type.description,
+ "sprite private",
+ )
+ )
if self._hasBugBranch():
- badges.append(self.icon_template % (
- "branch", "Branch exists", "sprite branch"))
+ badges.append(
+ self.icon_template
+ % ("branch", "Branch exists", "sprite branch")
+ )
if self._hasSpecification():
- badges.append(self.icon_template % (
- "blueprint", "Related to a blueprint", "sprite blueprint"))
+ badges.append(
+ self.icon_template
+ % ("blueprint", "Related to a blueprint", "sprite blueprint")
+ )
if self._context.milestone:
milestone_text = "milestone %s" % self._context.milestone.name
- badges.append(self.linked_icon_template % (
- canonical_url(self._context.milestone),
- milestone_text, "Linked to %s" % milestone_text,
- "sprite milestone"))
+ badges.append(
+ self.linked_icon_template
+ % (
+ canonical_url(self._context.milestone),
+ milestone_text,
+ "Linked to %s" % milestone_text,
+ "sprite milestone",
+ )
+ )
if self._hasPatch():
- badges.append(self.icon_template % (
- "haspatch", "Has a patch", "sprite haspatch-icon"))
+ badges.append(
+ self.icon_template
+ % ("haspatch", "Has a patch", "sprite haspatch-icon")
+ )
# Join with spaces to avoid the icons smashing into each other
# when multiple ones are presented.
@@ -1042,8 +1044,7 @@ class SpecificationImageDisplayAPI(ObjectImageDisplayAPI):
Used for image:icon.
"""
- icon_template = (
- '<span alt="%s" title="%s" class="%s" />')
+ icon_template = '<span alt="%s" title="%s" class="%s" />'
def sprite_css(self):
"""Return the CSS class for the sprite"""
@@ -1054,7 +1055,7 @@ class SpecificationImageDisplayAPI(ObjectImageDisplayAPI):
sprite_str = sprite_str + "-%s" % priority
if self._context.private:
- sprite_str = sprite_str + ' private'
+ sprite_str = sprite_str + " private"
return sprite_str
@@ -1063,16 +1064,21 @@ class SpecificationImageDisplayAPI(ObjectImageDisplayAPI):
def badges(self):
- badges = ''
+ badges = ""
if len(self._context.linked_branches) > 0:
badges += self.icon_template % (
- "branch", "Branch is available", "sprite branch")
+ "branch",
+ "Branch is available",
+ "sprite branch",
+ )
if self._context.informational:
badges += self.icon_template % (
- "informational", "Blueprint is purely informational",
- "sprite info")
+ "informational",
+ "Blueprint is purely informational",
+ "sprite info",
+ )
return badges
@@ -1084,17 +1090,20 @@ class KarmaCategoryImageDisplayAPI(ObjectImageDisplayAPI):
"""
icons_for_karma_categories = {
- 'bugs': '/@@/bug',
- 'code': '/@@/branch',
- 'translations': '/@@/translation',
- 'specs': '/@@/blueprint',
- 'soyuz': '/@@/package-source',
- 'answers': '/@@/question'}
+ "bugs": "/@@/bug",
+ "code": "/@@/branch",
+ "translations": "/@@/translation",
+ "specs": "/@@/blueprint",
+ "soyuz": "/@@/package-source",
+ "answers": "/@@/question",
+ }
def icon(self):
icon = self.icons_for_karma_categories[self._context.name]
- return ('<img height="14" width="14" alt="" title="%s" src="%s" />'
- % (self._context.title, icon))
+ return '<img height="14" width="14" alt="" title="%s" src="%s" />' % (
+ self._context.title,
+ icon,
+ )
class MilestoneImageDisplayAPI(ObjectImageDisplayAPI):
@@ -1114,40 +1123,42 @@ class BuildImageDisplayAPI(ObjectImageDisplayAPI):
Used for image:icon.
"""
+
icon_template = (
'<img width="%(width)s" height="14" alt="%(alt)s" '
- 'title="%(title)s" src="%(src)s" />')
+ 'title="%(title)s" src="%(src)s" />'
+ )
def icon(self):
"""Return the appropriate <img> tag for the build icon."""
icon_map = {
- BuildStatus.NEEDSBUILD: {'src': "/@@/build-needed"},
- BuildStatus.FULLYBUILT: {'src': "/@@/build-success"},
+ BuildStatus.NEEDSBUILD: {"src": "/@@/build-needed"},
+ BuildStatus.FULLYBUILT: {"src": "/@@/build-success"},
BuildStatus.FAILEDTOBUILD: {
- 'src': "/@@/build-failed",
- 'width': '16',
- },
- BuildStatus.MANUALDEPWAIT: {'src': "/@@/build-depwait"},
- BuildStatus.CHROOTWAIT: {'src': "/@@/build-chrootwait"},
- BuildStatus.SUPERSEDED: {'src': "/@@/build-superseded"},
- BuildStatus.BUILDING: {'src': "/@@/processing"},
- BuildStatus.FAILEDTOUPLOAD: {'src': "/@@/build-failedtoupload"},
- BuildStatus.UPLOADING: {'src': "/@@/processing"},
- BuildStatus.CANCELLING: {'src': "/@@/processing"},
- BuildStatus.CANCELLED: {'src': "/@@/build-failed"},
- }
-
- alt = '[%s]' % self._context.status.name
+ "src": "/@@/build-failed",
+ "width": "16",
+ },
+ BuildStatus.MANUALDEPWAIT: {"src": "/@@/build-depwait"},
+ BuildStatus.CHROOTWAIT: {"src": "/@@/build-chrootwait"},
+ BuildStatus.SUPERSEDED: {"src": "/@@/build-superseded"},
+ BuildStatus.BUILDING: {"src": "/@@/processing"},
+ BuildStatus.FAILEDTOUPLOAD: {"src": "/@@/build-failedtoupload"},
+ BuildStatus.UPLOADING: {"src": "/@@/processing"},
+ BuildStatus.CANCELLING: {"src": "/@@/processing"},
+ BuildStatus.CANCELLED: {"src": "/@@/build-failed"},
+ }
+
+ alt = "[%s]" % self._context.status.name
title = self._context.status.title
- source = icon_map[self._context.status].get('src')
- width = icon_map[self._context.status].get('width', '14')
+ source = icon_map[self._context.status].get("src")
+ width = icon_map[self._context.status].get("width", "14")
return self.icon_template % {
- 'alt': alt,
- 'title': title,
- 'src': source,
- 'width': width,
- }
+ "alt": alt,
+ "title": title,
+ "src": source,
+ "width": width,
+ }
class ArchiveImageDisplayAPI(ObjectImageDisplayAPI):
@@ -1155,6 +1166,7 @@ class ArchiveImageDisplayAPI(ObjectImageDisplayAPI):
Used for image:icon.
"""
+
icon_template = """
<img width="14" height="14" alt="%s" title="%s" src="%s" />
"""
@@ -1162,13 +1174,13 @@ class ArchiveImageDisplayAPI(ObjectImageDisplayAPI):
def icon(self):
"""Return the appropriate <img> tag for an archive."""
icon_map = {
- ArchivePurpose.PRIMARY: '/@@/distribution',
- ArchivePurpose.PARTNER: '/@@/distribution',
- ArchivePurpose.PPA: '/@@/ppa-icon',
- ArchivePurpose.COPY: '/@@/distribution',
- }
+ ArchivePurpose.PRIMARY: "/@@/distribution",
+ ArchivePurpose.PARTNER: "/@@/distribution",
+ ArchivePurpose.PPA: "/@@/ppa-icon",
+ ArchivePurpose.COPY: "/@@/distribution",
+ }
- alt = '[%s]' % self._context.purpose.title
+ alt = "[%s]" % self._context.purpose.title
title = self._context.purpose.title
source = icon_map[self._context.purpose]
@@ -1180,32 +1192,34 @@ class SnapBuildRequestImageDisplayAPI(ObjectImageDisplayAPI):
Used for image:icon.
"""
+
icon_template = (
'<img width="%(width)s" height="14" alt="%(alt)s" '
- 'title="%(title)s" src="%(src)s" />')
+ 'title="%(title)s" src="%(src)s" />'
+ )
def icon(self):
"""Return the appropriate <img> tag for the build request icon."""
icon_map = {
- SnapBuildRequestStatus.PENDING: {'src': "/@@/processing"},
+ SnapBuildRequestStatus.PENDING: {"src": "/@@/processing"},
SnapBuildRequestStatus.FAILED: {
- 'src': "/@@/build-failed",
- 'width': "16",
- },
- SnapBuildRequestStatus.COMPLETED: {'src': "/@@/build-success"},
- }
+ "src": "/@@/build-failed",
+ "width": "16",
+ },
+ SnapBuildRequestStatus.COMPLETED: {"src": "/@@/build-success"},
+ }
- alt = '[%s]' % self._context.status.name
+ alt = "[%s]" % self._context.status.name
title = self._context.status.title
- source = icon_map[self._context.status].get('src')
- width = icon_map[self._context.status].get('width', '14')
+ source = icon_map[self._context.status].get("src")
+ width = icon_map[self._context.status].get("width", "14")
return self.icon_template % {
- 'alt': alt,
- 'title': title,
- 'src': source,
- 'width': width,
- }
+ "alt": alt,
+ "title": title,
+ "src": source,
+ "width": width,
+ }
class RevisionStatusReportImageDisplayAPI(ObjectImageDisplayAPI):
@@ -1213,9 +1227,11 @@ class RevisionStatusReportImageDisplayAPI(ObjectImageDisplayAPI):
Used for image:icon.
"""
+
icon_template = (
'<img width="%(width)s" height="14" alt="%(alt)s" '
- 'title="%(title)s" src="%(src)s" />')
+ 'title="%(title)s" src="%(src)s" />'
+ )
def icon(self):
"""Return the appropriate <img> tag for the result icon."""
@@ -1228,8 +1244,8 @@ class RevisionStatusReportImageDisplayAPI(ObjectImageDisplayAPI):
RevisionStatusResult.CANCELLED: {
"src": "/@@/build-failed",
"width": "16",
- },
- }
+ },
+ }
result = self._context.result
if result is None:
@@ -1244,7 +1260,7 @@ class RevisionStatusReportImageDisplayAPI(ObjectImageDisplayAPI):
"title": title,
"src": source,
"width": width,
- }
+ }
class BadgeDisplayAPI:
@@ -1260,33 +1276,36 @@ class BadgeDisplayAPI:
def small(self):
"""Render the visible badge's icon images."""
badges = self.context.getVisibleBadges()
- return ''.join([badge.renderIconImage() for badge in badges])
+ return "".join([badge.renderIconImage() for badge in badges])
def large(self):
"""Render the visible badge's heading images."""
badges = self.context.getVisibleBadges()
- return ''.join([badge.renderHeadingImage() for badge in badges])
+ return "".join([badge.renderHeadingImage() for badge in badges])
class PersonFormatterAPI(ObjectFormatterAPI):
"""Adapter for `IPerson` objects to a formatted string."""
- traversable_names = {'link': 'link', 'url': 'url', 'api_url': 'api_url',
- 'icon': 'icon',
- 'displayname': 'displayname',
- 'unique_displayname': 'unique_displayname',
- 'link-display-name-id': 'link_display_name_id',
- }
+ traversable_names = {
+ "link": "link",
+ "url": "url",
+ "api_url": "api_url",
+ "icon": "icon",
+ "displayname": "displayname",
+ "unique_displayname": "unique_displayname",
+ "link-display-name-id": "link_display_name_id",
+ }
- final_traversable_names = {'local-time': 'local_time'}
+ final_traversable_names = {"local-time": "local_time"}
final_traversable_names.update(ObjectFormatterAPI.final_traversable_names)
def local_time(self):
"""Return the local time for this person."""
time_zone = self._context.time_zone
- return datetime.now(pytz.timezone(time_zone)).strftime('%T %Z')
+ return datetime.now(pytz.timezone(time_zone)).strftime("%T %Z")
- def url(self, view_name=None, rootsite='mainsite'):
+ def url(self, view_name=None, rootsite="mainsite"):
"""See `ObjectFormatterAPI`.
The default URL for a person is to the mainsite.
@@ -1300,15 +1319,18 @@ class PersonFormatterAPI(ObjectFormatterAPI):
if custom_icon is None:
css_class = ObjectImageDisplayAPI(person).sprite_css()
return structured(
- '<a href="%s" class="%s">%s</a>',
- url, css_class, text).escapedtext
+ '<a href="%s" class="%s">%s</a>', url, css_class, text
+ ).escapedtext
else:
return structured(
'<a href="%s" class="bg-image" '
'style="background-image: url(%s)">%s</a>',
- url, custom_icon, text).escapedtext
+ url,
+ custom_icon,
+ text,
+ ).escapedtext
- def link(self, view_name, rootsite='mainsite'):
+ def link(self, view_name, rootsite="mainsite"):
"""See `ObjectFormatterAPI`.
Return an HTML link to the person's page containing an icon
@@ -1329,8 +1351,7 @@ class PersonFormatterAPI(ObjectFormatterAPI):
def icon(self, view_name):
"""Return the URL for the person's icon."""
- custom_icon = ObjectImageDisplayAPI(
- self._context).custom_icon_url()
+ custom_icon = ObjectImageDisplayAPI(self._context).custom_icon_url()
if custom_icon is None:
css_class = ObjectImageDisplayAPI(self._context).sprite_css()
return '<span class="' + css_class + '"></span>'
@@ -1344,7 +1365,7 @@ class PersonFormatterAPI(ObjectFormatterAPI):
indicate which user profile is linked.
"""
text = self.unique_displayname(None)
- return self._makeLink(view_name, 'mainsite', text)
+ return self._makeLink(view_name, "mainsite", text)
class MixedVisibilityError(Exception):
@@ -1354,15 +1375,15 @@ class MixedVisibilityError(Exception):
class TeamFormatterAPI(PersonFormatterAPI):
"""Adapter for `ITeam` objects to a formatted string."""
- hidden = '<hidden>'
+ hidden = "<hidden>"
- def url(self, view_name=None, rootsite='mainsite'):
+ def url(self, view_name=None, rootsite="mainsite"):
"""See `ObjectFormatterAPI`.
The default URL for a team is to the mainsite. None is returned
when the user does not have permission to review the team.
"""
- if not check_permission('launchpad.LimitedView', self._context):
+ if not check_permission("launchpad.LimitedView", self._context):
# This person has no permission to view the team details.
self._report_visibility_leak()
return None
@@ -1370,29 +1391,30 @@ class TeamFormatterAPI(PersonFormatterAPI):
def api_url(self, context):
"""See `ObjectFormatterAPI`."""
- if not check_permission('launchpad.LimitedView', self._context):
+ if not check_permission("launchpad.LimitedView", self._context):
# This person has no permission to view the team details.
self._report_visibility_leak()
return None
return super().api_url(context)
- def link(self, view_name, rootsite='mainsite'):
+ def link(self, view_name, rootsite="mainsite"):
"""See `ObjectFormatterAPI`.
The default URL for a team is to the mainsite. None is returned
when the user does not have permission to review the team.
"""
person = self._context
- if not check_permission('launchpad.LimitedView', person):
+ if not check_permission("launchpad.LimitedView", person):
# This person has no permission to view the team details.
self._report_visibility_leak()
return structured(
- '<span class="sprite team">%s</span>', self.hidden).escapedtext
+ '<span class="sprite team">%s</span>', self.hidden
+ ).escapedtext
return super().link(view_name, rootsite)
def icon(self, view_name):
team = self._context
- if not check_permission('launchpad.LimitedView', team):
+ if not check_permission("launchpad.LimitedView", team):
css_class = ObjectImageDisplayAPI(team).sprite_css()
return '<span class="' + css_class + '"></span>'
else:
@@ -1401,7 +1423,7 @@ class TeamFormatterAPI(PersonFormatterAPI):
def displayname(self, view_name, rootsite=None):
"""See `PersonFormatterAPI`."""
person = self._context
- if not check_permission('launchpad.LimitedView', person):
+ if not check_permission("launchpad.LimitedView", person):
# This person has no permission to view the team details.
self._report_visibility_leak()
return self.hidden
@@ -1410,7 +1432,7 @@ class TeamFormatterAPI(PersonFormatterAPI):
def unique_displayname(self, view_name):
"""See `PersonFormatterAPI`."""
person = self._context
- if not check_permission('launchpad.LimitedView', person):
+ if not check_permission("launchpad.LimitedView", person):
# This person has no permission to view the team details.
self._report_visibility_leak()
return self.hidden
@@ -1421,8 +1443,7 @@ class TeamFormatterAPI(PersonFormatterAPI):
try:
raise MixedVisibilityError()
except MixedVisibilityError:
- getUtility(IErrorReportingUtility).raising(
- sys.exc_info(), request)
+ getUtility(IErrorReportingUtility).raising(sys.exc_info(), request)
class CustomizableFormatter(ObjectFormatterAPI):
@@ -1447,7 +1468,7 @@ class CustomizableFormatter(ObjectFormatterAPI):
If a different permission is required, override _link_permission.
"""
- _link_permission = 'launchpad.View'
+ _link_permission = "launchpad.View"
def _link_summary_values(self):
"""Return a dict of values to use for template substitution.
@@ -1464,8 +1485,9 @@ class CustomizableFormatter(ObjectFormatterAPI):
contexts like lists of items.
"""
values = {
- k: v if v is not None else ''
- for k, v in self._link_summary_values().items()}
+ k: v if v is not None else ""
+ for k, v in self._link_summary_values().items()
+ }
return structured(self._link_summary_template, **values).escapedtext
def _title_values(self):
@@ -1482,12 +1504,13 @@ class CustomizableFormatter(ObjectFormatterAPI):
This title is for use in fmt:link, which is meant to be used in
contexts like lists of items.
"""
- title_template = getattr(self, '_title_template', None)
+ title_template = getattr(self, "_title_template", None)
if title_template is None:
return None
values = {
- k: v if v is not None else ''
- for k, v in self._title_values().items()}
+ k: v if v is not None else ""
+ for k, v in self._title_values().items()
+ }
return structured(title_template, **values).escapedtext
def sprite_css(self):
@@ -1495,7 +1518,7 @@ class CustomizableFormatter(ObjectFormatterAPI):
:return: The icon css or None if no icon is available.
"""
- return queryAdapter(self._context, IPathAdapter, 'image').sprite_css()
+ return queryAdapter(self._context, IPathAdapter, "image").sprite_css()
def link(self, view_name, rootsite=None):
"""Return html including a link, description and icon.
@@ -1507,21 +1530,21 @@ class CustomizableFormatter(ObjectFormatterAPI):
"""
sprite = self.sprite_css()
if sprite is None:
- css = ''
+ css = ""
else:
css = ' class="' + sprite + '"'
summary = self._make_link_summary()
title = self._make_title()
if title is None:
- title = ''
+ title = ""
else:
title = ' title="%s"' % title
if check_permission(self._link_permission, self._context):
url = self.url(view_name, rootsite)
else:
- url = ''
+ url = ""
if url:
return '<a href="%s"%s%s>%s</a>' % (url, css, title, summary)
else:
@@ -1532,19 +1555,19 @@ class PillarFormatterAPI(CustomizableFormatter):
"""Adapter for IProduct, IDistribution and IProjectGroup objects to a
formatted string."""
- _link_summary_template = '%(displayname)s'
- _link_permission = 'zope.Public'
+ _link_summary_template = "%(displayname)s"
+ _link_permission = "zope.Public"
traversable_names = {
- 'api_url': 'api_url',
- 'link': 'link',
- 'url': 'url',
- 'link_with_displayname': 'link_with_displayname'
- }
+ "api_url": "api_url",
+ "link": "link",
+ "url": "url",
+ "link_with_displayname": "link_with_displayname",
+ }
def _link_summary_values(self):
displayname = self._context.displayname
- return {'displayname': displayname}
+ return {"displayname": displayname}
def url(self, view_name=None, rootsite=None):
"""See `ObjectFormatterAPI`.
@@ -1553,8 +1576,9 @@ class PillarFormatterAPI(CustomizableFormatter):
"""
return super().url(view_name, rootsite)
- def _getLinkHTML(self, view_name, rootsite,
- template, custom_icon_template):
+ def _getLinkHTML(
+ self, view_name, rootsite, template, custom_icon_template
+ ):
"""Generates html, mapping a link context to given templates.
The html is generated using given `template` or `custom_icon_template`
@@ -1571,19 +1595,19 @@ class PillarFormatterAPI(CustomizableFormatter):
context = self._context
# XXX wgrant: the structured() in this dict is evil; refactor.
mapping = {
- 'url': self.url(view_name, rootsite),
- 'name': context.name,
- 'displayname': context.displayname,
- 'summary': structured(self._make_link_summary()),
- }
+ "url": self.url(view_name, rootsite),
+ "name": context.name,
+ "displayname": context.displayname,
+ "summary": structured(self._make_link_summary()),
+ }
custom_icon = ObjectImageDisplayAPI(context).custom_icon_url()
if custom_icon is None:
- mapping['css_class'] = ObjectImageDisplayAPI(context).sprite_css()
+ mapping["css_class"] = ObjectImageDisplayAPI(context).sprite_css()
return structured(template, **mapping).escapedtext
- mapping['custom_icon'] = custom_icon
+ mapping["custom_icon"] = custom_icon
return structured(custom_icon_template, **mapping).escapedtext
- def link(self, view_name, rootsite='mainsite'):
+ def link(self, view_name, rootsite="mainsite"):
"""The html to show a link to a Product, ProjectGroup or distribution.
In the case of Products or ProjectGroups we display the custom
@@ -1594,11 +1618,12 @@ class PillarFormatterAPI(CustomizableFormatter):
custom_icon_template = (
'<a href="%(url)s" class="bg-image" '
'style="background-image: url(%(custom_icon)s)">%(summary)s</a>'
- )
+ )
return self._getLinkHTML(
- view_name, rootsite, template, custom_icon_template)
+ view_name, rootsite, template, custom_icon_template
+ )
- def link_with_displayname(self, view_name, rootsite='mainsite'):
+ def link_with_displayname(self, view_name, rootsite="mainsite"):
"""The html to show a link to a Product, ProjectGroup or
distribution, including displayname and name.
@@ -1609,43 +1634,45 @@ class PillarFormatterAPI(CustomizableFormatter):
template = (
'<a href="%(url)s" class="%(css_class)s">%(displayname)s</a>'
' (<a href="%(url)s">%(name)s</a>)'
- )
+ )
custom_icon_template = (
'<a href="%(url)s" class="bg-image" '
'style="background-image: url(%(custom_icon)s)">'
'%(displayname)s</a> (<a href="%(url)s">%(name)s</a>)'
- )
+ )
return self._getLinkHTML(
- view_name, rootsite, template, custom_icon_template)
+ view_name, rootsite, template, custom_icon_template
+ )
class DistroSeriesFormatterAPI(CustomizableFormatter):
"""Adapter for IDistroSeries objects to a formatted string."""
- _link_summary_template = '%(displayname)s'
- _link_permission = 'zope.Public'
+ _link_summary_template = "%(displayname)s"
+ _link_permission = "zope.Public"
def _link_summary_values(self):
displayname = self._context.displayname
- return {'displayname': displayname}
+ return {"displayname": displayname}
class SourcePackageReleaseFormatterAPI(CustomizableFormatter):
"""Adapter for ISourcePackageRelease objects to a formatted string."""
- _link_summary_template = '%(sourcepackage)s %(version)s'
+ _link_summary_template = "%(sourcepackage)s %(version)s"
def _link_summary_values(self):
- return {'sourcepackage':
- self._context.distrosourcepackage.displayname,
- 'version': self._context.version}
+ return {
+ "sourcepackage": self._context.distrosourcepackage.displayname,
+ "version": self._context.version,
+ }
class ProductReleaseFileFormatterAPI(ObjectFormatterAPI):
"""Adapter for `IProductReleaseFile` objects to a formatted string."""
- traversable_names = {'link': 'link', 'url': 'url'}
+ traversable_names = {"link": "link", "url": "url"}
def link(self, view_name):
"""A hyperlinked ProductReleaseFile.
@@ -1656,29 +1683,35 @@ class ProductReleaseFileFormatterAPI(ObjectFormatterAPI):
"""
file_ = self._context
file_size = NumberFormatterAPI(
- file_.libraryfile.content.filesize).bytes()
+ file_.libraryfile.content.filesize
+ ).bytes()
if file_.description is not None:
description = file_.description
else:
description = file_.libraryfile.filename
link_title = "%s (%s)" % (description, file_size)
download_url = self._getDownloadURL(file_.libraryfile)
- md5_url = urlappend(download_url, '+md5')
+ md5_url = urlappend(download_url, "+md5")
replacements = dict(
- url=download_url, filename=file_.libraryfile.filename,
- md5_url=md5_url, link_title=link_title)
+ url=download_url,
+ filename=file_.libraryfile.filename,
+ md5_url=md5_url,
+ link_title=link_title,
+ )
html = (
'<img alt="download icon" src="/@@/download" />'
- '<strong>'
+ "<strong>"
' <a title="%(link_title)s" href="%(url)s">%(filename)s</a> '
- '</strong>'
- '(<a href="%(md5_url)s">md5</a>')
+ "</strong>"
+ '(<a href="%(md5_url)s">md5</a>'
+ )
if file_.signature is not None:
html += ', <a href="%(signature_url)s">sig</a>)'
- replacements['signature_url'] = self._getDownloadURL(
- file_.signature)
+ replacements["signature_url"] = self._getDownloadURL(
+ file_.signature
+ )
else:
- html += ')'
+ html += ")"
return structured(html, **replacements).escapedtext
def url(self, view_name=None, rootsite=None):
@@ -1691,36 +1724,40 @@ class ProductReleaseFileFormatterAPI(ObjectFormatterAPI):
def _getDownloadURL(self, lfa):
"""Return the download URL for the given `LibraryFileAlias`."""
- url = urlappend(canonical_url(self._release), '+download')
+ url = urlappend(canonical_url(self._release), "+download")
# Quote the filename to eliminate non-ascii characters which
# are invalid in the url.
- return urlappend(url, quote(lfa.filename.encode('utf-8')))
+ return urlappend(url, quote(lfa.filename.encode("utf-8")))
class BranchFormatterAPI(ObjectFormatterAPI):
"""Adapter for IBranch objects to a formatted string."""
traversable_names = {
- 'link': 'link', 'url': 'url',
- 'title-link': 'titleLink', 'bzr-link': 'bzrLink',
- 'api_url': 'api_url'}
+ "link": "link",
+ "url": "url",
+ "title-link": "titleLink",
+ "bzr-link": "bzrLink",
+ "api_url": "api_url",
+ }
def _args(self, view_name):
"""Generate a dict of attributes for string template expansion."""
branch = self._context
return {
- 'bzr_identity': branch.bzr_identity,
- 'display_name': branch.displayname,
- 'name': branch.name,
- 'unique_name': branch.unique_name,
- 'url': self.url(view_name),
- }
+ "bzr_identity": branch.bzr_identity,
+ "display_name": branch.displayname,
+ "name": branch.name,
+ "unique_name": branch.unique_name,
+ "url": self.url(view_name),
+ }
def link(self, view_name):
"""A hyperlinked branch icon with the displayname."""
return structured(
- '<a href="%(url)s" class="sprite branch">'
- '%(display_name)s</a>', **self._args(view_name)).escapedtext
+ '<a href="%(url)s" class="sprite branch">' "%(display_name)s</a>",
+ **self._args(view_name),
+ ).escapedtext
def bzrLink(self, view_name):
"""A hyperlinked branch icon with the bazaar identity."""
@@ -1731,45 +1768,47 @@ class BranchFormatterAPI(ObjectFormatterAPI):
"""A hyperlinked branch name with following title."""
return structured(
'<a href="%(url)s" title="%(display_name)s">'
- '%(name)s</a>: %(title)s', **self._args(view_name)).escapedtext
+ "%(name)s</a>: %(title)s",
+ **self._args(view_name),
+ ).escapedtext
class BranchSubscriptionFormatterAPI(CustomizableFormatter):
"""Adapter for IBranchSubscription objects to a formatted string."""
- _link_summary_template = _('Subscription of %(person)s to %(branch)s')
+ _link_summary_template = _("Subscription of %(person)s to %(branch)s")
def _link_summary_values(self):
"""Provide values for template substitution"""
return {
- 'person': self._context.person.displayname,
- 'branch': self._context.branch.displayname,
+ "person": self._context.person.displayname,
+ "branch": self._context.branch.displayname,
}
class BranchMergeProposalFormatterAPI(CustomizableFormatter):
- _link_summary_template = _('%(title)s')
+ _link_summary_template = _("%(title)s")
def _link_summary_values(self):
return {
- 'title': self._context.title,
- }
+ "title": self._context.title,
+ }
class GitRepositoryFormatterAPI(CustomizableFormatter):
"""Adapter for IGitRepository objects to a formatted string."""
- _link_summary_template = '%(display_name)s'
+ _link_summary_template = "%(display_name)s"
def _link_summary_values(self):
- return {'display_name': self._context.display_name}
+ return {"display_name": self._context.display_name}
class GitRefFormatterAPI(CustomizableFormatter):
"""Adapter for IGitRef objects to a formatted string."""
- _link_summary_template = '%(display_name)s'
+ _link_summary_template = "%(display_name)s"
def url(self, view_name=None, rootsite=None):
"""See `ObjectFormatterAPI`.
@@ -1781,7 +1820,7 @@ class GitRefFormatterAPI(CustomizableFormatter):
return super().url(view_name, rootsite)
def _link_summary_values(self):
- return {'display_name': self._context.display_name}
+ return {"display_name": self._context.display_name}
class BugBranchFormatterAPI(CustomizableFormatter):
@@ -1805,21 +1844,23 @@ class BugBranchFormatterAPI(CustomizableFormatter):
class BugFormatterAPI(CustomizableFormatter):
"""Adapter for IBug objects to a formatted string."""
- _link_summary_template = 'Bug #%(id)s: %(title)s'
+ _link_summary_template = "Bug #%(id)s: %(title)s"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'id': str(self._context.id), 'title': self._context.title}
+ return {"id": str(self._context.id), "title": self._context.title}
class BugTaskFormatterAPI(CustomizableFormatter):
"""Adapter for IBugTask objects to a formatted string."""
- _title_template = '%(importance)s - %(status)s'
+ _title_template = "%(importance)s - %(status)s"
def _title_values(self):
- return {'importance': self._context.importance.title,
- 'status': self._context.status.title}
+ return {
+ "importance": self._context.importance.title,
+ "status": self._context.status.title,
+ }
def _make_link_summary(self):
return BugFormatterAPI(self._context.bug)._make_link_summary()
@@ -1828,14 +1869,15 @@ class BugTaskFormatterAPI(CustomizableFormatter):
class CodeImportFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for CodeImport objects"""
- _link_summary_template = _('Import of %(target)s: %(branch)s')
- _link_permission = 'zope.Public'
+ _link_summary_template = _("Import of %(target)s: %(branch)s")
+ _link_permission = "zope.Public"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'target': self._context.branch.target.displayname,
- 'branch': self._context.branch.bzr_identity,
- }
+ return {
+ "target": self._context.branch.target.displayname,
+ "branch": self._context.branch.bzr_identity,
+ }
def url(self, view_name=None, rootsite=None):
"""See `ObjectFormatterAPI`."""
@@ -1843,8 +1885,10 @@ class CodeImportFormatterAPI(CustomizableFormatter):
# This is still here primarily for supporting branch deletion,
# which does a fmt:link of the other entities that will be deleted.
url = canonical_url(
- self._context.branch, path_only_if_possible=True,
- view_name=view_name)
+ self._context.branch,
+ path_only_if_possible=True,
+ view_name=view_name,
+ )
return url
@@ -1859,182 +1903,201 @@ class PackageBuildFormatterAPI(ObjectFormatterAPI):
def link(self, view_name, rootsite=None):
build = self._context
- if (not check_permission('launchpad.View', build) or
- not check_permission('launchpad.View', build.archive.owner)):
- return 'private job'
+ if not check_permission(
+ "launchpad.View", build
+ ) or not check_permission("launchpad.View", build.archive.owner):
+ return "private job"
url = self.url(view_name=view_name, rootsite=rootsite)
archive = self._composeArchiveReference(build.archive)
return structured(
- '<a href="%s">%s</a>%s', url, build.title, archive).escapedtext
+ '<a href="%s">%s</a>%s', url, build.title, archive
+ ).escapedtext
class CodeImportMachineFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for CodeImport objects"""
- _link_summary_template = _('%(hostname)s')
- _link_permission = 'zope.Public'
+ _link_summary_template = _("%(hostname)s")
+ _link_permission = "zope.Public"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'hostname': self._context.hostname}
+ return {"hostname": self._context.hostname}
class MilestoneFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for Milestone objects."""
- _link_summary_template = _('%(title)s')
- _link_permission = 'zope.Public'
+ _link_summary_template = _("%(title)s")
+ _link_permission = "zope.Public"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'title': self._context.title}
+ return {"title": self._context.title}
class ProductReleaseFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for Milestone objects."""
- _link_summary_template = _('%(displayname)s %(code_name)s')
- _link_permission = 'zope.Public'
+ _link_summary_template = _("%(displayname)s %(code_name)s")
+ _link_permission = "zope.Public"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
code_name = self._context.milestone.code_name
- if code_name is None or code_name.strip() == '':
- code_name = ''
+ if code_name is None or code_name.strip() == "":
+ code_name = ""
else:
- code_name = '(%s)' % code_name.strip()
- return dict(displayname=self._context.milestone.displayname,
- code_name=code_name)
+ code_name = "(%s)" % code_name.strip()
+ return dict(
+ displayname=self._context.milestone.displayname,
+ code_name=code_name,
+ )
class ProductSeriesFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for ProductSeries objects"""
- _link_summary_template = _('%(product)s %(series)s series')
+ _link_summary_template = _("%(product)s %(series)s series")
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'series': self._context.name,
- 'product': self._context.product.displayname}
+ return {
+ "series": self._context.name,
+ "product": self._context.product.displayname,
+ }
class QuestionFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for question objects."""
- _link_summary_template = _('%(id)s: %(title)s')
- _link_permission = 'zope.Public'
+ _link_summary_template = _("%(id)s: %(title)s")
+ _link_permission = "zope.Public"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'id': str(self._context.id), 'title': self._context.title}
+ return {"id": str(self._context.id), "title": self._context.title}
class SourcePackageRecipeFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for ISourcePackageRecipe objects."""
- _link_summary_template = 'Recipe %(name)s for %(owner)s'
+ _link_summary_template = "Recipe %(name)s for %(owner)s"
def _link_summary_values(self):
- return {'name': self._context.name,
- 'owner': self._context.owner.displayname}
+ return {
+ "name": self._context.name,
+ "owner": self._context.owner.displayname,
+ }
class LiveFSFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for ILiveFS objects."""
_link_summary_template = _(
- 'Live filesystem %(distroseries)s %(name)s for %(owner)s')
+ "Live filesystem %(distroseries)s %(name)s for %(owner)s"
+ )
def _link_summary_values(self):
- return {'distroseries': self._context.distro_series.name,
- 'name': self._context.name,
- 'owner': self._context.owner.displayname}
+ return {
+ "distroseries": self._context.distro_series.name,
+ "name": self._context.name,
+ "owner": self._context.owner.displayname,
+ }
class OCIRecipeFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for IOCIRecipe objects."""
_link_summary_template = _(
- 'OCI recipe %(pillar_name)s/%(oci_project_name)s/%(recipe_name)s for '
- '%(owner)s')
+ "OCI recipe %(pillar_name)s/%(oci_project_name)s/%(recipe_name)s for "
+ "%(owner)s"
+ )
def _link_summary_values(self):
- return {'pillar_name': self._context.oci_project.pillar.name,
- 'oci_project_name': self._context.oci_project.name,
- 'recipe_name': self._context.name,
- 'owner': self._context.owner.displayname}
+ return {
+ "pillar_name": self._context.oci_project.pillar.name,
+ "oci_project_name": self._context.oci_project.name,
+ "recipe_name": self._context.name,
+ "owner": self._context.owner.displayname,
+ }
class SnapFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for ISnap objects."""
- _link_summary_template = _(
- 'Snap %(name)s for %(owner)s')
+ _link_summary_template = _("Snap %(name)s for %(owner)s")
def _link_summary_values(self):
- return {'name': self._context.name,
- 'owner': self._context.owner.displayname}
+ return {
+ "name": self._context.name,
+ "owner": self._context.owner.displayname,
+ }
class SnappySeriesFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for ISnappySeries objects."""
- _link_summary_template = _('%(title)s')
+ _link_summary_template = _("%(title)s")
def _link_summary_values(self):
- return {'title': self._context.title}
+ return {"title": self._context.title}
class CharmRecipeFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for ICharmRecipe objects."""
_link_summary_template = _(
- 'Charm recipe %(name)s for %(owner)s in %(project)s')
+ "Charm recipe %(name)s for %(owner)s in %(project)s"
+ )
def _link_summary_values(self):
- return {'name': self._context.name,
- 'owner': self._context.owner.displayname,
- 'project': self._context.project.displayname}
+ return {
+ "name": self._context.name,
+ "owner": self._context.owner.displayname,
+ "project": self._context.project.displayname,
+ }
class SpecificationFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for Specification objects"""
- _link_summary_template = _('%(title)s')
- _link_permission = 'zope.Public'
+ _link_summary_template = _("%(title)s")
+ _link_permission = "zope.Public"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'title': self._context.title}
+ return {"title": self._context.title}
class CodeReviewCommentFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for CodeReviewComment objects"""
- _link_summary_template = _('Comment by %(author)s')
- _link_permission = 'zope.Public'
+ _link_summary_template = _("Comment by %(author)s")
+ _link_permission = "zope.Public"
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'author': self._context.message.owner.displayname}
+ return {"author": self._context.message.owner.displayname}
class ArchiveFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for `IArchive` objects."""
- _link_summary_template = '%(display_name)s'
- _link_permission = 'launchpad.View'
- _reference_permission = 'launchpad.SubscriberView'
+ _link_summary_template = "%(display_name)s"
+ _link_permission = "launchpad.View"
+ _reference_permission = "launchpad.SubscriberView"
_reference_template = "ppa:%(owner_name)s/%(ppa_name)s"
- final_traversable_names = {'reference': 'reference'}
+ final_traversable_names = {"reference": "reference"}
final_traversable_names.update(
- CustomizableFormatter.final_traversable_names)
+ CustomizableFormatter.final_traversable_names
+ )
def _link_summary_values(self):
"""See CustomizableFormatter._link_summary_values."""
- return {'display_name': self._context.displayname}
+ return {"display_name": self._context.displayname}
def link(self, view_name):
"""Return html including a link for the context archive.
@@ -2052,8 +2115,8 @@ class ArchiveFormatterAPI(CustomizableFormatter):
if check_permission(self._link_permission, self._context):
if self._context.is_main:
url = queryAdapter(
- self._context.distribution, IPathAdapter, 'fmt').url(
- view_name)
+ self._context.distribution, IPathAdapter, "fmt"
+ ).url(view_name)
else:
url = self.url(view_name)
return '<a href="%s" class="%s">%s</a>' % (url, css, summary)
@@ -2061,19 +2124,21 @@ class ArchiveFormatterAPI(CustomizableFormatter):
if not self._context.private:
return '<span class="%s">%s</span>' % (css, summary)
else:
- return ''
+ return ""
def reference(self, view_name=None, rootsite=None):
"""Return the text PPA reference for a PPA."""
if not IPPA.providedBy(self._context):
raise NotImplementedError(
- "No reference implementation for non-PPA archive %r." %
- self._context)
+ "No reference implementation for non-PPA archive %r."
+ % self._context
+ )
if not check_permission(self._reference_permission, self._context):
- return ''
+ return ""
return self._reference_template % {
- 'owner_name': self._context.owner.name,
- 'ppa_name': self._context.name}
+ "owner_name": self._context.owner.name,
+ "ppa_name": self._context.name,
+ }
class SpecificationBranchFormatterAPI(CustomizableFormatter):
@@ -2091,16 +2156,18 @@ class SpecificationBranchFormatterAPI(CustomizableFormatter):
def sprite_css(self):
return queryAdapter(
- self._context.specification, IPathAdapter, 'image').sprite_css()
+ self._context.specification, IPathAdapter, "image"
+ ).sprite_css()
class BugTrackerFormatterAPI(ObjectFormatterAPI):
"""Adapter for `IBugTracker` objects to a formatted string."""
final_traversable_names = {
- 'aliases': 'aliases',
- 'external-link': 'external_link',
- 'external-title-link': 'external_title_link'}
+ "aliases": "aliases",
+ "external-link": "external_link",
+ "external-title-link": "external_title_link",
+ }
final_traversable_names.update(ObjectFormatterAPI.final_traversable_names)
def link(self, view_name):
@@ -2123,12 +2190,12 @@ class BugTrackerFormatterAPI(ObjectFormatterAPI):
text (i.e. no <a/> link).
"""
url = self._context.baseurl
- if url.startswith('mailto:') and getUtility(ILaunchBag).user is None:
- return html_escape('mailto:<email address hidden>')
+ if url.startswith("mailto:") and getUtility(ILaunchBag).user is None:
+ return html_escape("mailto:<email address hidden>")
else:
return structured(
- '<a class="link-external" href="%(url)s">%(url)s</a>',
- url=url).escapedtext
+ '<a class="link-external" href="%(url)s">%(url)s</a>', url=url
+ ).escapedtext
def external_title_link(self):
"""Return an HTML link to the external bugtracker.
@@ -2142,11 +2209,11 @@ class BugTrackerFormatterAPI(ObjectFormatterAPI):
title = self._context.title
if getUtility(ILaunchBag).user is None:
title = FormattersAPI(title).obfuscate_email()
- if url.startswith('mailto:'):
+ if url.startswith("mailto:"):
return html_escape(title)
return structured(
- '<a class="link-external" href="%s">%s</a>',
- url, title).escapedtext
+ '<a class="link-external" href="%s">%s</a>', url, title
+ ).escapedtext
def aliases(self):
"""Generate alias URLs, obfuscating where necessary.
@@ -2156,8 +2223,8 @@ class BugTrackerFormatterAPI(ObjectFormatterAPI):
"""
anonymous = getUtility(ILaunchBag).user is None
for alias in self._context.aliases:
- if anonymous and alias.startswith('mailto:'):
- yield 'mailto:<email address hidden>'
+ if anonymous and alias.startswith("mailto:"):
+ yield "mailto:<email address hidden>"
else:
yield alias
@@ -2166,8 +2233,9 @@ class BugWatchFormatterAPI(ObjectFormatterAPI):
"""Adapter for `IBugWatch` objects to a formatted string."""
final_traversable_names = {
- 'external-link': 'external_link',
- 'external-link-short': 'external_link_short'}
+ "external-link": "external_link",
+ "external-link-short": "external_link_short",
+ }
final_traversable_names.update(ObjectFormatterAPI.final_traversable_names)
def _make_external_link(self, summary=None):
@@ -2180,14 +2248,14 @@ class BugWatchFormatterAPI(ObjectFormatterAPI):
an email address, only the summary is returned (i.e. no link).
"""
if summary is None or len(summary) == 0:
- summary = structured('—')
+ summary = structured("—")
url = self._context.url
- if url.startswith('mailto:') and getUtility(ILaunchBag).user is None:
+ if url.startswith("mailto:") and getUtility(ILaunchBag).user is None:
return html_escape(summary)
else:
return structured(
- '<a class="link-external" href="%s">%s</a>',
- url, summary).escapedtext
+ '<a class="link-external" href="%s">%s</a>', url, summary
+ ).escapedtext
def external_link(self):
"""Return an HTML link with a detailed link text.
@@ -2198,7 +2266,7 @@ class BugWatchFormatterAPI(ObjectFormatterAPI):
summary = self._context.bugtracker.name
remotebug = self._context.remotebug
if remotebug is not None and len(remotebug) > 0:
- summary = '%s #%s' % (summary, remotebug)
+ summary = "%s #%s" % (summary, remotebug)
return self._make_external_link(summary)
def external_link_short(self):
@@ -2218,16 +2286,17 @@ class NumberFormatterAPI:
self._number = number
def traverse(self, name, furtherPath):
- if name == 'float':
+ if name == "float":
if len(furtherPath) != 1:
raise TraversalError(
- "fmt:float requires a single decimal argument")
+ "fmt:float requires a single decimal argument"
+ )
# coerce the argument to float to ensure it's safe
format = furtherPath.pop()
return self.float(float(format))
- elif name == 'bytes':
+ elif name == "bytes":
return self.bytes()
- elif name == 'intcomma':
+ elif name == "intcomma":
return self.intcomma()
else:
raise TraversalError(name)
@@ -2242,9 +2311,9 @@ class NumberFormatterAPI:
L = []
for index, char in enumerate(reversed(str(self._number))):
if index != 0 and (index % 3) == 0:
- L.insert(0, ',')
+ L.insert(0, ",")
L.insert(0, char)
- return ''.join(L)
+ return "".join(L)
def bytes(self):
"""Render number as byte contractions according to IEC60027-2."""
@@ -2264,7 +2333,7 @@ class NumberFormatterAPI:
if exponent < 1:
# If this is less than 1 KiB, no need for rounding.
return "%s bytes" % n
- return "%.1f %s" % (n / 1024.0 ** exponent, suffixes[exponent - 1])
+ return "%.1f %s" % (n / 1024.0**exponent, suffixes[exponent - 1])
def float(self, format):
"""Use like tal:content="context/foo/fmt:float/.2".
@@ -2286,29 +2355,30 @@ class DateTimeFormatterAPI:
self._datetime = datetimeobject
else:
self._datetime = datetime(
- datetimeobject.year, datetimeobject.month, datetimeobject.day,
- tzinfo=pytz.timezone('UTC'))
+ datetimeobject.year,
+ datetimeobject.month,
+ datetimeobject.day,
+ tzinfo=pytz.timezone("UTC"),
+ )
def time(self):
if self._datetime.tzinfo:
- value = self._datetime.astimezone(
- getUtility(ILaunchBag).time_zone)
- return value.strftime('%T %Z')
+ value = self._datetime.astimezone(getUtility(ILaunchBag).time_zone)
+ return value.strftime("%T %Z")
else:
- return self._datetime.strftime('%T')
+ return self._datetime.strftime("%T")
def date(self):
value = self._datetime
if value.tzinfo:
- value = value.astimezone(
- getUtility(ILaunchBag).time_zone)
- return value.strftime('%Y-%m-%d')
+ value = value.astimezone(getUtility(ILaunchBag).time_zone)
+ return value.strftime("%Y-%m-%d")
def _now(self):
# This method exists to be overridden in tests.
if self._datetime.tzinfo:
# datetime is offset-aware
- return datetime.now(pytz.timezone('UTC'))
+ return datetime.now(pytz.timezone("UTC"))
else:
# datetime is offset-naive
return datetime.utcnow()
@@ -2317,7 +2387,7 @@ class DateTimeFormatterAPI:
delta = abs(self._now() - self._datetime)
if delta > timedelta(1, 0, 0):
# far in the past or future, display the date
- return 'on ' + self.date()
+ return "on " + self.date()
return self.approximatedate()
def approximatedate(self):
@@ -2331,39 +2401,39 @@ class DateTimeFormatterAPI:
hours = delta.seconds // 3600
minutes = (delta.seconds - (3600 * hours)) // 60
seconds = delta.seconds % 60
- result = ''
+ result = ""
if future:
- result += 'in '
+ result += "in "
if days != 0:
amount = days
- unit = 'day'
+ unit = "day"
elif hours != 0:
amount = hours
- unit = 'hour'
+ unit = "hour"
elif minutes != 0:
amount = minutes
- unit = 'minute'
+ unit = "minute"
else:
if seconds <= 10:
- result += 'a moment'
+ result += "a moment"
if not future:
- result += ' ago'
+ result += " ago"
return result
else:
amount = seconds
- unit = 'second'
+ unit = "second"
if amount != 1:
- unit += 's'
- result += '%s %s' % (amount, unit)
+ unit += "s"
+ result += "%s %s" % (amount, unit)
if not future:
- result += ' ago'
+ result += " ago"
return result
def datetime(self):
return "%s %s" % (self.date(), self.time())
def rfc822utcdatetime(self):
- return formatdate(mktime_tz(self._datetime.utctimetuple() + (0, )))
+ return formatdate(mktime_tz(self._datetime.utctimetuple() + (0,)))
def isodate(self):
return self._datetime.isoformat()
@@ -2375,7 +2445,10 @@ class DateTimeFormatterAPI:
# is exact rather than being an estimate.
return structured(
'<time title="%s" datetime="%s">%s</time>',
- self.datetime(), self.isodate(), self.displaydate()).escapedtext
+ self.datetime(),
+ self.isodate(),
+ self.displaydate(),
+ ).escapedtext
def approximatedatetitle(self):
# Like `approximatedate`, but wrapped in an HTML element with `title`
@@ -2384,8 +2457,10 @@ class DateTimeFormatterAPI:
# timestamp is exact rather than being an estimate.
return structured(
'<time title="%s" datetime="%s">%s</time>',
- self.datetime(), self.isodate(),
- self.approximatedate()).escapedtext
+ self.datetime(),
+ self.isodate(),
+ self.approximatedate(),
+ ).escapedtext
@staticmethod
def _yearDelta(old, new):
@@ -2404,23 +2479,23 @@ class DateTimeFormatterAPI:
"""How long since the datetime, as a string."""
now = self._now()
number = self._yearDelta(self._datetime, now)
- unit = 'year'
+ unit = "year"
if number < 1:
delta = now - self._datetime
if delta.days > 0:
number = delta.days
- unit = 'day'
+ unit = "day"
else:
number = delta.seconds // 60
if number == 0:
- return 'less than a minute'
- unit = 'minute'
+ return "less than a minute"
+ unit = "minute"
if number >= 60:
number /= 60
- unit = 'hour'
+ unit = "hour"
if number != 1:
- unit += 's'
- return '%d %s' % (number, unit)
+ unit += "s"
+ return "%d %s" % (number, unit)
class SeriesSourcePackageBranchFormatter(ObjectFormatterAPI):
@@ -2432,12 +2507,13 @@ class SeriesSourcePackageBranchFormatter(ObjectFormatterAPI):
def url(self, view_name=None, rootsite=None):
return queryAdapter(
- self._context.sourcepackage, IPathAdapter, 'fmt').url(
- view_name, rootsite)
+ self._context.sourcepackage, IPathAdapter, "fmt"
+ ).url(view_name, rootsite)
def link(self, view_name):
return queryAdapter(
- self._context.sourcepackage, IPathAdapter, 'fmt').link(view_name)
+ self._context.sourcepackage, IPathAdapter, "fmt"
+ ).link(view_name)
@implementer(ITraversable)
@@ -2448,11 +2524,11 @@ class DurationFormatterAPI:
self._duration = duration
def traverse(self, name, furtherPath):
- if name == 'exactduration':
+ if name == "exactduration":
return self.exactduration()
- elif name == 'approximateduration':
+ elif name == "approximateduration":
return self.approximateduration()
- elif name == 'millisecondduration':
+ elif name == "millisecondduration":
return self.millisecondduration()
else:
raise TraversalError(name)
@@ -2462,26 +2538,26 @@ class DurationFormatterAPI:
parts = []
minutes, seconds = divmod(self._duration.seconds, 60)
hours, minutes = divmod(minutes, 60)
- seconds = seconds + (float(self._duration.microseconds) / 10 ** 6)
+ seconds = seconds + (float(self._duration.microseconds) / 10**6)
if self._duration.days > 0:
if self._duration.days == 1:
- parts.append('%d day' % self._duration.days)
+ parts.append("%d day" % self._duration.days)
else:
- parts.append('%d days' % self._duration.days)
+ parts.append("%d days" % self._duration.days)
if parts or hours > 0:
if hours == 1:
- parts.append('%d hour' % hours)
+ parts.append("%d hour" % hours)
else:
- parts.append('%d hours' % hours)
+ parts.append("%d hours" % hours)
if parts or minutes > 0:
if minutes == 1:
- parts.append('%d minute' % minutes)
+ parts.append("%d minute" % minutes)
else:
- parts.append('%d minutes' % minutes)
+ parts.append("%d minutes" % minutes)
if parts or seconds > 0:
- parts.append('%0.1f seconds' % seconds)
+ parts.append("%0.1f seconds" % seconds)
- return ', '.join(parts)
+ return ", ".join(parts)
def approximateduration(self):
"""Return a nicely-formatted approximate duration.
@@ -2505,19 +2581,19 @@ class DurationFormatterAPI:
# the display value corresponding to the lowest boundary that
# 'seconds' is less than, if one exists.
representation_in_seconds = [
- (1.5, '1 second'),
- (2.5, '2 seconds'),
- (3.5, '3 seconds'),
- (4.5, '4 seconds'),
- (7.5, '5 seconds'),
- (12.5, '10 seconds'),
- (17.5, '15 seconds'),
- (22.5, '20 seconds'),
- (27.5, '25 seconds'),
- (35, '30 seconds'),
- (45, '40 seconds'),
- (55, '50 seconds'),
- (90, '1 minute'),
+ (1.5, "1 second"),
+ (2.5, "2 seconds"),
+ (3.5, "3 seconds"),
+ (4.5, "4 seconds"),
+ (7.5, "5 seconds"),
+ (12.5, "10 seconds"),
+ (17.5, "15 seconds"),
+ (22.5, "20 seconds"),
+ (27.5, "25 seconds"),
+ (35, "30 seconds"),
+ (45, "40 seconds"),
+ (55, "50 seconds"),
+ (90, "1 minute"),
]
# Break representation_in_seconds into two pieces, to simplify
@@ -2553,7 +2629,7 @@ class DurationFormatterAPI:
hours, remaining_seconds = divmod(seconds, 3600)
ten_minute_chunks = round_half_up(remaining_seconds / 600.0)
minutes = ten_minute_chunks * 10
- hours += (minutes // 60)
+ hours += minutes // 60
minutes %= 60
if hours < 10:
if minutes:
@@ -2568,7 +2644,7 @@ class DurationFormatterAPI:
# Is the duration less than ten and a half hours?
if seconds < (10.5 * 3600):
- return '10 hours'
+ return "10 hours"
# Try to calculate the approximate number of hours, to a
# maximum of 47.
@@ -2578,7 +2654,7 @@ class DurationFormatterAPI:
# Is the duration fewer than two and a half days?
if seconds < (2.5 * 24 * 3600):
- return '2 days'
+ return "2 days"
# Try to approximate to day granularity, up to a maximum of 13
# days.
@@ -2588,7 +2664,7 @@ class DurationFormatterAPI:
# Is the duration fewer than two and a half weeks?
if seconds < (2.5 * 7 * 24 * 3600):
- return '2 weeks'
+ return "2 weeks"
# If we've made it this far, we'll calculate the duration to a
# granularity of weeks, once and for all.
@@ -2596,23 +2672,23 @@ class DurationFormatterAPI:
return "%d weeks" % weeks
def millisecondduration(self):
- return '%sms' % (self._duration.total_seconds() * 1000,)
+ return "%sms" % (self._duration.total_seconds() * 1000,)
class LinkFormatterAPI(ObjectFormatterAPI):
"""Adapter from Link objects to a formatted anchor."""
+
final_traversable_names = {
- 'icon': 'icon',
- 'icon-link': 'link',
- 'link-icon': 'link',
- }
+ "icon": "icon",
+ "icon-link": "link",
+ "link-icon": "link",
+ }
final_traversable_names.update(ObjectFormatterAPI.final_traversable_names)
def icon(self):
"""Return the icon representation of the link."""
request = get_current_browser_request()
- return getMultiAdapter(
- (self._context, request), name="+inline-icon")()
+ return getMultiAdapter((self._context, request), name="+inline-icon")()
def link(self, view_name=None, rootsite=None):
"""Return the default representation of the link."""
@@ -2623,20 +2699,21 @@ class LinkFormatterAPI(ObjectFormatterAPI):
if self._context.enabled:
return self._context.url
else:
- return ''
+ return ""
class RevisionAuthorFormatterAPI(ObjectFormatterAPI):
"""Adapter for `IRevisionAuthor` links."""
- traversable_names = {'link': 'link'}
+ traversable_names = {"link": "link"}
- def link(self, view_name=None, rootsite='mainsite'):
+ def link(self, view_name=None, rootsite="mainsite"):
"""See `ObjectFormatterAPI`."""
context = self._context
if context.person is not None:
return PersonFormatterAPI(self._context.person).link(
- view_name, rootsite)
+ view_name, rootsite
+ )
elif context.name_without_email:
return html_escape(context.name_without_email)
elif context.email and getUtility(ILaunchBag).user is not None:
@@ -2645,7 +2722,7 @@ class RevisionAuthorFormatterAPI(ObjectFormatterAPI):
return html_escape("<email address hidden>")
else:
# The RevisionAuthor name and email is None.
- return ''
+ return ""
@implementer(ITraversable)
@@ -2662,13 +2739,14 @@ class PermissionRequiredQuery:
def traverse(self, name, furtherPath):
if len(furtherPath) > 0:
raise TraversalError(
- "There should be no further path segments after "
- "required:permission")
+ "There should be no further path segments after "
+ "required:permission"
+ )
return check_permission(name, self.context)
class IMainTemplateFile(Interface):
- path = TextLine(title='The absolute path to this main template.')
+ path = TextLine(title="The absolute path to this main template.")
@adapter(LaunchpadLayer)
@@ -2676,27 +2754,27 @@ class IMainTemplateFile(Interface):
class LaunchpadLayerToMainTemplateAdapter:
def __init__(self, context):
here = os.path.dirname(os.path.realpath(__file__))
- self.path = os.path.join(here, '../templates/base-layout.pt')
+ self.path = os.path.join(here, "../templates/base-layout.pt")
@implementer(ITraversable)
class PageMacroDispatcher:
"""Selects a macro, while storing information about page layout.
- view/macro:page
- view/macro:page/main_side
- view/macro:page/main_only
- view/macro:page/searchless
+ view/macro:page
+ view/macro:page/main_side
+ view/macro:page/main_only
+ view/macro:page/searchless
- view/macro:pagehas/applicationtabs
- view/macro:pagehas/globalsearch
- view/macro:pagehas/portlets
- view/macro:pagehas/main
+ view/macro:pagehas/applicationtabs
+ view/macro:pagehas/globalsearch
+ view/macro:pagehas/portlets
+ view/macro:pagehas/main
- view/macro:pagetype
+ view/macro:pagetype
- view/macro:is-page-contentless
- view/macro:has-watermark
+ view/macro:is-page-contentless
+ view/macro:has-watermark
"""
def __init__(self, context):
@@ -2706,44 +2784,46 @@ class PageMacroDispatcher:
@property
def base(self):
return ViewPageTemplateFile(
- IMainTemplateFile(self.context.request).path)
+ IMainTemplateFile(self.context.request).path
+ )
def traverse(self, name, furtherPath):
- if name == 'page':
+ if name == "page":
if len(furtherPath) == 1:
pagetype = furtherPath.pop()
elif not furtherPath:
- pagetype = 'default'
+ pagetype = "default"
else:
raise TraversalError("Max one path segment after macro:page")
return self.page(pagetype)
- elif name == 'pagehas':
+ elif name == "pagehas":
if len(furtherPath) != 1:
raise TraversalError(
- "Exactly one path segment after macro:haspage")
+ "Exactly one path segment after macro:haspage"
+ )
layoutelement = furtherPath.pop()
return self.haspage(layoutelement)
- elif name == 'pagetype':
+ elif name == "pagetype":
return self.pagetype()
- elif name == 'is-page-contentless':
+ elif name == "is-page-contentless":
return self.isPageContentless()
- elif name == 'has-watermark':
+ elif name == "has-watermark":
return self.hasWatermark()
else:
raise TraversalError(name)
def page(self, pagetype):
if pagetype not in self._pagetypes:
- raise TraversalError('unknown pagetype: %s' % pagetype)
+ raise TraversalError("unknown pagetype: %s" % pagetype)
self.context.__pagetype__ = pagetype
- return self.base.macros['master']
+ return self.base.macros["master"]
def haspage(self, layoutelement):
- pagetype = getattr(self.context, '__pagetype__', None)
+ pagetype = getattr(self.context, "__pagetype__", None)
if pagetype is None:
- pagetype = 'unset'
+ pagetype = "unset"
return self._pagetypes[pagetype][layoutelement]
def hasWatermark(self):
@@ -2752,7 +2832,7 @@ class PageMacroDispatcher:
The default value is True, but the view can provide has_watermark
to force the page not render the standard location information.
"""
- return getattr(self.context, 'has_watermark', True)
+ return getattr(self.context, "has_watermark", True)
def isPageContentless(self):
"""Should the template avoid rendering detailed information.
@@ -2766,58 +2846,53 @@ class PageMacroDispatcher:
if privacy is None or not privacy.private:
return False
return not (
- check_permission('launchpad.SubscriberView', view_context) or
- check_permission('launchpad.View', view_context))
+ check_permission("launchpad.SubscriberView", view_context)
+ or check_permission("launchpad.View", view_context)
+ )
def pagetype(self):
- return getattr(self.context, '__pagetype__', 'unset')
+ return getattr(self.context, "__pagetype__", "unset")
class LayoutElements:
-
- def __init__(self,
+ def __init__(
+ self,
applicationtabs=False,
globalsearch=False,
portlets=False,
pagetypewasset=True,
- ):
+ ):
self.elements = vars()
def __getitem__(self, name):
return self.elements[name]
_pagetypes = {
- 'main_side':
- LayoutElements(
- applicationtabs=True,
- globalsearch=True,
- portlets=True),
- 'main_only':
- LayoutElements(
- applicationtabs=True,
- globalsearch=True,
- portlets=False),
- 'searchless':
- LayoutElements(
- applicationtabs=True,
- globalsearch=False,
- portlets=False),
- }
+ "main_side": LayoutElements(
+ applicationtabs=True, globalsearch=True, portlets=True
+ ),
+ "main_only": LayoutElements(
+ applicationtabs=True, globalsearch=True, portlets=False
+ ),
+ "searchless": LayoutElements(
+ applicationtabs=True, globalsearch=False, portlets=False
+ ),
+ }
class TranslationGroupFormatterAPI(ObjectFormatterAPI):
"""Adapter for `ITranslationGroup` objects to a formatted string."""
traversable_names = {
- 'link': 'link',
- 'url': 'url',
- 'displayname': 'displayname',
+ "link": "link",
+ "url": "url",
+ "displayname": "displayname",
}
- def url(self, view_name=None, rootsite='translations'):
+ def url(self, view_name=None, rootsite="translations"):
"""See `ObjectFormatterAPI`."""
return super().url(view_name, rootsite)
- def link(self, view_name, rootsite='translations'):
+ def link(self, view_name, rootsite="translations"):
"""See `ObjectFormatterAPI`."""
group = self._context
url = self.url(view_name, rootsite)
@@ -2830,22 +2905,25 @@ class TranslationGroupFormatterAPI(ObjectFormatterAPI):
class LanguageFormatterAPI(ObjectFormatterAPI):
"""Adapter for `ILanguage` objects to a formatted string."""
+
traversable_names = {
- 'link': 'link',
- 'url': 'url',
- 'displayname': 'displayname',
+ "link": "link",
+ "url": "url",
+ "displayname": "displayname",
}
- def url(self, view_name=None, rootsite='translations'):
+ def url(self, view_name=None, rootsite="translations"):
"""See `ObjectFormatterAPI`."""
return super().url(view_name, rootsite)
- def link(self, view_name, rootsite='translations'):
+ def link(self, view_name, rootsite="translations"):
"""See `ObjectFormatterAPI`."""
url = self.url(view_name, rootsite)
return structured(
'<a href="%s" class="sprite language">%s</a>',
- url, self._context.englishname).escapedtext
+ url,
+ self._context.englishname,
+ ).escapedtext
def displayname(self, view_name, rootsite=None):
"""See `ObjectFormatterAPI`."""
@@ -2856,16 +2934,16 @@ class POFileFormatterAPI(ObjectFormatterAPI):
"""Adapter for `IPOFile` objects to a formatted string."""
traversable_names = {
- 'link': 'link',
- 'url': 'url',
- 'displayname': 'displayname',
+ "link": "link",
+ "url": "url",
+ "displayname": "displayname",
}
- def url(self, view_name=None, rootsite='translations'):
+ def url(self, view_name=None, rootsite="translations"):
"""See `ObjectFormatterAPI`."""
return super().url(view_name, rootsite)
- def link(self, view_name, rootsite='translations'):
+ def link(self, view_name, rootsite="translations"):
"""See `ObjectFormatterAPI`."""
pofile = self._context
url = self.url(view_name, rootsite)
@@ -2880,20 +2958,22 @@ def download_link(url, description, file_size):
"""Return HTML for downloading an item."""
file_size = NumberFormatterAPI(file_size).bytes()
formatted = structured(
- '<a href="%s">%s</a> (%s)', url, description, file_size)
+ '<a href="%s">%s</a> (%s)', url, description, file_size
+ )
return formatted.escapedtext
class PackageDiffFormatterAPI(ObjectFormatterAPI):
-
def link(self, view_name, rootsite=None):
diff = self._context
if not diff.date_fulfilled:
- return structured('%s (pending)', diff.title).escapedtext
+ return structured("%s (pending)", diff.title).escapedtext
else:
return download_link(
- diff.diff_content.http_url, diff.title,
- diff.diff_content.content.filesize)
+ diff.diff_content.http_url,
+ diff.title,
+ diff.diff_content.content.filesize,
+ )
@implementer(ITraversable)
@@ -2911,7 +2991,7 @@ class CSSFormatter:
def select(self, furtherPath):
if len(furtherPath) < 2:
- raise TraversalError('select needs two subsequent path elements.')
+ raise TraversalError("select needs two subsequent path elements.")
true_value = furtherPath.pop()
false_value = furtherPath.pop()
if self.context:
@@ -2931,8 +3011,8 @@ class IRCNicknameFormatterAPI(ObjectFormatterAPI):
"""Adapter from IrcID objects to a formatted string."""
traversable_names = {
- 'displayname': 'displayname',
- 'formatted_displayname': 'formatted_displayname',
+ "displayname": "displayname",
+ "formatted_displayname": "formatted_displayname",
}
def displayname(self, view_name=None):
@@ -2940,9 +3020,13 @@ class IRCNicknameFormatterAPI(ObjectFormatterAPI):
def formatted_displayname(self, view_name=None):
return structured(
- dedent("""\
+ dedent(
+ """\
<strong>%s</strong>
<span class="lesser"> on </span>
<strong>%s</strong>
- """),
- self._context.nickname, self._context.network).escapedtext
+ """
+ ),
+ self._context.nickname,
+ self._context.network,
+ ).escapedtext
diff --git a/lib/lp/app/browser/tests/test_base_layout.py b/lib/lp/app/browser/tests/test_base_layout.py
index 271237f..2ccf8de 100644
--- a/lib/lp/app/browser/tests/test_base_layout.py
+++ b/lib/lp/app/browser/tests/test_base_layout.py
@@ -17,25 +17,19 @@ from lp.registry.interfaces.person import PersonVisibility
from lp.services.beautifulsoup import BeautifulSoup
from lp.services.webapp.publisher import LaunchpadView
from lp.services.webapp.servers import LaunchpadTestRequest
-from lp.testing import (
- login,
- person_logged_in,
- TestCaseWithFactory,
- )
+from lp.testing import TestCaseWithFactory, login, person_logged_in
from lp.testing.layers import DatabaseFunctionalLayer
-from lp.testing.pages import (
- extract_text,
- find_tag_by_id,
- )
+from lp.testing.pages import extract_text, find_tag_by_id
class TestBaseLayout(TestCaseWithFactory):
"""Test the page parts provided by the base-layout.pt."""
+
layer = DatabaseFunctionalLayer
def setUp(self):
super().setUp()
- self.user = self.factory.makePerson(name='waffles')
+ self.user = self.factory.makePerson(name="waffles")
self.context = None
def makeTemplateView(self, layout, context=None, view_attributes=None):
@@ -47,11 +41,13 @@ class TestBaseLayout(TestCaseWithFactory):
class TemplateView(LaunchpadView):
"""A simple view to test base-layout."""
- __name__ = '+template'
- __launchpad_facetname__ = 'overview'
+
+ __name__ = "+template"
+ __launchpad_facetname__ = "overview"
template = ViewPageTemplateFile(
- 'testfiles/%s.pt' % layout.replace('_', '-'))
- page_title = 'Test base-layout: %s' % layout
+ "testfiles/%s.pt" % layout.replace("_", "-")
+ )
+ page_title = "Test base-layout: %s" % layout
if context is None:
self.context = self.user
@@ -59,7 +55,8 @@ class TestBaseLayout(TestCaseWithFactory):
self.context = context
request = LaunchpadTestRequest(
- SERVER_URL='http://launchpad.test', PATH_INFO='/~waffles/+layout')
+ SERVER_URL="http://launchpad.test", PATH_INFO="/~waffles/+layout"
+ )
request.setPrincipal(self.user)
request.traversed_objects.append(self.context)
view = TemplateView(self.context, request)
@@ -71,18 +68,17 @@ class TestBaseLayout(TestCaseWithFactory):
def test_base_layout_doctype(self):
# Verify that the document is a html DOCTYPE.
- view = self.makeTemplateView('main_side')
+ view = self.makeTemplateView("main_side")
markup = view()
- self.assertTrue(markup.startswith('<!DOCTYPE html>'))
+ self.assertTrue(markup.startswith("<!DOCTYPE html>"))
def verify_base_layout_html_element(self, content):
# The html element states the namespace and language information.
- self.assertEqual(
- 'http://www.w3.org/1999/xhtml', content.html['xmlns'])
+ self.assertEqual("http://www.w3.org/1999/xhtml", content.html["xmlns"])
html_tag = content.html
- self.assertEqual('en', html_tag['xml:lang'])
- self.assertEqual('en', html_tag['lang'])
- self.assertEqual('ltr', html_tag['dir'])
+ self.assertEqual("en", html_tag["xml:lang"])
+ self.assertEqual("en", html_tag["lang"])
+ self.assertEqual("ltr", html_tag["dir"])
def verify_base_layout_head_parts(self, view, content):
# Verify the common head parts of every layout.
@@ -90,113 +86,120 @@ class TestBaseLayout(TestCaseWithFactory):
# The page's title starts with the view's page_title.
self.assertTrue(head.title.string.startswith(view.page_title))
# The shortcut icon for the browser chrome is provided.
- link_tag = head.find('link', rel='shortcut icon')
- self.assertEqual(['shortcut', 'icon'], link_tag['rel'])
- self.assertEqual('/@@/favicon.ico?v=2022', link_tag['href'])
+ link_tag = head.find("link", rel="shortcut icon")
+ self.assertEqual(["shortcut", "icon"], link_tag["rel"])
+ self.assertEqual("/@@/favicon.ico?v=2022", link_tag["href"])
# The template loads the common scripts.
- load_script = find_tag_by_id(head, 'base-layout-load-scripts').name
- self.assertEqual('script', load_script)
+ load_script = find_tag_by_id(head, "base-layout-load-scripts").name
+ self.assertEqual("script", load_script)
def verify_base_layout_body_parts(self, document):
# Verify the common body parts of every layout.
- self.assertEqual('body', document.name)
- yui_layout = document.find('div', 'yui-d0')
+ self.assertEqual("body", document.name)
+ yui_layout = document.find("div", "yui-d0")
self.assertTrue(yui_layout is not None)
self.assertEqual(
- ['login-logout'], yui_layout.find(True, id='locationbar')['class'])
+ ["login-logout"], yui_layout.find(True, id="locationbar")["class"]
+ )
self.assertEqual(
- ['yui-main'], yui_layout.find(True, id='maincontent')['class'])
+ ["yui-main"], yui_layout.find(True, id="maincontent")["class"]
+ )
self.assertEqual(
- ['footer'], yui_layout.find(True, id='footer')['class'])
+ ["footer"], yui_layout.find(True, id="footer")["class"]
+ )
def verify_watermark(self, document):
# Verify the parts of a watermark.
- yui_layout = document.find('div', 'yui-d0')
- watermark = yui_layout.find(True, id='watermark')
- self.assertEqual(['watermark-apps-portlet'], watermark['class'])
+ yui_layout = document.find("div", "yui-d0")
+ watermark = yui_layout.find(True, id="watermark")
+ self.assertEqual(["watermark-apps-portlet"], watermark["class"])
if self.context.is_team:
- self.assertEqual('/@@/team-logo', watermark.img['src'])
- self.assertEqual('\u201cWaffles\u201d team', watermark.h2.a.string)
+ self.assertEqual("/@@/team-logo", watermark.img["src"])
+ self.assertEqual("\u201cWaffles\u201d team", watermark.h2.a.string)
else:
- self.assertEqual('/@@/person-logo', watermark.img['src'])
- self.assertEqual('Waffles', watermark.h2.a.string)
- self.assertEqual(['facetmenu'], watermark.ul['class'])
+ self.assertEqual("/@@/person-logo", watermark.img["src"])
+ self.assertEqual("Waffles", watermark.h2.a.string)
+ self.assertEqual(["facetmenu"], watermark.ul["class"])
def test_main_side(self):
# The main_side layout has everything.
- view = self.makeTemplateView('main_side')
+ view = self.makeTemplateView("main_side")
content = BeautifulSoup(view())
- self.assertIsNot(None, content.find(text=' Extra head content '))
+ self.assertIsNot(None, content.find(text=" Extra head content "))
self.verify_base_layout_html_element(content)
self.verify_base_layout_head_parts(view, content)
- document = find_tag_by_id(content, 'document')
+ document = find_tag_by_id(content, "document")
self.verify_base_layout_body_parts(document)
- classes = 'tab-overview main_side public yui3-skin-sam'.split()
- self.assertEqual(classes, document['class'])
+ classes = "tab-overview main_side public yui3-skin-sam".split()
+ self.assertEqual(classes, document["class"])
self.verify_watermark(document)
self.assertEqual(
- ['registering'], document.find(True, id='registration')['class'])
+ ["registering"], document.find(True, id="registration")["class"]
+ )
self.assertEqual(
- 'Registered on 2005-09-16 by Illuminati',
- document.find(True, id='registration').string.strip(),
- )
+ "Registered on 2005-09-16 by Illuminati",
+ document.find(True, id="registration").string.strip(),
+ )
self.assertEndsWith(
- extract_text(document.find(True, id='maincontent')),
- 'Main content of the page.')
+ extract_text(document.find(True, id="maincontent")),
+ "Main content of the page.",
+ )
self.assertEqual(
- ['yui-b', 'side'],
- document.find(True, id='side-portlets')['class'])
- self.assertEqual('form', document.find(True, id='globalsearch').name)
+ ["yui-b", "side"], document.find(True, id="side-portlets")["class"]
+ )
+ self.assertEqual("form", document.find(True, id="globalsearch").name)
def test_main_only(self):
# The main_only layout has everything except side portlets.
- view = self.makeTemplateView('main_only')
+ view = self.makeTemplateView("main_only")
content = BeautifulSoup(view())
self.verify_base_layout_html_element(content)
self.verify_base_layout_head_parts(view, content)
- document = find_tag_by_id(content, 'document')
+ document = find_tag_by_id(content, "document")
self.verify_base_layout_body_parts(document)
- classes = 'tab-overview main_only public yui3-skin-sam'.split()
- self.assertEqual(classes, document['class'])
+ classes = "tab-overview main_only public yui3-skin-sam".split()
+ self.assertEqual(classes, document["class"])
self.verify_watermark(document)
self.assertEqual(
- ['registering'], document.find(True, id='registration')['class'])
- self.assertEqual(None, document.find(True, id='side-portlets'))
- self.assertEqual('form', document.find(True, id='globalsearch').name)
+ ["registering"], document.find(True, id="registration")["class"]
+ )
+ self.assertEqual(None, document.find(True, id="side-portlets"))
+ self.assertEqual("form", document.find(True, id="globalsearch").name)
def test_searchless(self):
# The searchless layout is missing side portlets and search.
- view = self.makeTemplateView('searchless')
+ view = self.makeTemplateView("searchless")
content = BeautifulSoup(view())
self.verify_base_layout_html_element(content)
self.verify_base_layout_head_parts(view, content)
- document = find_tag_by_id(content, 'document')
+ document = find_tag_by_id(content, "document")
self.verify_base_layout_body_parts(document)
self.verify_watermark(document)
- classes = 'tab-overview searchless public yui3-skin-sam'.split()
- self.assertEqual(classes, document['class'])
+ classes = "tab-overview searchless public yui3-skin-sam".split()
+ self.assertEqual(classes, document["class"])
self.assertEqual(
- ['registering'], document.find(True, id='registration')['class'])
- self.assertEqual(None, document.find(True, id='side-portlets'))
- self.assertEqual(None, document.find(True, id='globalsearch'))
+ ["registering"], document.find(True, id="registration")["class"]
+ )
+ self.assertEqual(None, document.find(True, id="side-portlets"))
+ self.assertEqual(None, document.find(True, id="globalsearch"))
def test_contact_support_logged_in(self):
# The support link points to /support when the user is logged in.
- view = self.makeTemplateView('main_only')
+ view = self.makeTemplateView("main_only")
view._user = self.user
content = BeautifulSoup(view())
- footer = find_tag_by_id(content, 'footer')
- link = footer.find('a', text='Contact Launchpad Support')
- self.assertEqual('/support', link['href'])
+ footer = find_tag_by_id(content, "footer")
+ link = footer.find("a", text="Contact Launchpad Support")
+ self.assertEqual("/support", link["href"])
def test_contact_support_anonymous(self):
# The support link points to /feedback when the user is anonymous.
- view = self.makeTemplateView('main_only')
+ view = self.makeTemplateView("main_only")
view._user = None
content = BeautifulSoup(view())
- footer = find_tag_by_id(content, 'footer')
- link = footer.find('a', text='Contact Launchpad Support')
- self.assertEqual('/feedback', link['href'])
+ footer = find_tag_by_id(content, "footer")
+ link = footer.find("a", text="Contact Launchpad Support")
+ self.assertEqual("/feedback", link["href"])
def test_user_without_launchpad_view(self):
# When the user does not have launchpad.View on the context,
@@ -204,87 +207,92 @@ class TestBaseLayout(TestCaseWithFactory):
owner = self.factory.makePerson()
with person_logged_in(owner):
team = self.factory.makeTeam(
- displayname='Waffles', owner=owner,
- visibility=PersonVisibility.PRIVATE)
+ displayname="Waffles",
+ owner=owner,
+ visibility=PersonVisibility.PRIVATE,
+ )
archive = self.factory.makeArchive(private=True, owner=team)
archive.newSubscription(self.user, registrant=owner)
with person_logged_in(self.user):
- view = self.makeTemplateView('main_side', context=team)
+ view = self.makeTemplateView("main_side", context=team)
content = BeautifulSoup(view())
- self.assertIs(None, content.find(text=' Extra head content '))
+ self.assertIs(None, content.find(text=" Extra head content "))
self.verify_base_layout_html_element(content)
self.verify_base_layout_head_parts(view, content)
- document = find_tag_by_id(content, 'document')
+ document = find_tag_by_id(content, "document")
self.verify_base_layout_body_parts(document)
self.verify_watermark(document)
# These parts are unique to the case without launchpad.View.
- self.assertIsNone(document.find(True, id='side-portlets'))
- self.assertIsNone(document.find(True, id='registration'))
+ self.assertIsNone(document.find(True, id="side-portlets"))
+ self.assertIsNone(document.find(True, id="registration"))
self.assertEndsWith(
- extract_text(document.find(True, id='maincontent')),
- 'The information in this page is not shared with you.')
+ extract_text(document.find(True, id="maincontent")),
+ "The information in this page is not shared with you.",
+ )
def test_user_with_launchpad_view(self):
# Users with launchpad.View do not see the sharing explanation.
# See the main_side, main_only, and searchless tests to know
# what content is provides to the user who can view.
- view = self.makeTemplateView('main_side')
- content = extract_text(find_tag_by_id(view(), 'maincontent'))
+ view = self.makeTemplateView("main_side")
+ content = extract_text(find_tag_by_id(view(), "maincontent"))
self.assertNotIn(
- 'The information in this page is not shared with you.', content)
+ "The information in this page is not shared with you.", content
+ )
def test_referrer_policy_set_private_view(self):
- login('admin@xxxxxxxxxxxxx')
+ login("admin@xxxxxxxxxxxxx")
owner = self.factory.makePerson()
with person_logged_in(owner):
team = self.factory.makeTeam(
- owner=owner,
- visibility=PersonVisibility.PRIVATE)
- view = self.makeTemplateView('main_side', context=team)
+ owner=owner, visibility=PersonVisibility.PRIVATE
+ )
+ view = self.makeTemplateView("main_side", context=team)
content = BeautifulSoup(view())
- referrer = content.find('meta',
- {'name': 'referrer',
- 'content': 'origin-when-cross-origin'})
+ referrer = content.find(
+ "meta", {"name": "referrer", "content": "origin-when-cross-origin"}
+ )
self.assertIsNotNone(referrer)
- self.assertEqual(referrer.get('content'), 'origin-when-cross-origin')
- self.assertEqual(referrer.get('name'), 'referrer')
+ self.assertEqual(referrer.get("content"), "origin-when-cross-origin")
+ self.assertEqual(referrer.get("name"), "referrer")
def test_referrer_policy_set_public_view(self):
- view = self.makeTemplateView('main_side')
+ view = self.makeTemplateView("main_side")
content = BeautifulSoup(view())
- referrer = content.find('meta', content="origin-when-cross-origin")
+ referrer = content.find("meta", content="origin-when-cross-origin")
self.assertIsNone(referrer)
def test_opengraph_metadata(self):
- view = self.makeTemplateView('main_side')
+ view = self.makeTemplateView("main_side")
content = BeautifulSoup(view())
# https://ogp.me/ - "The four required properties for every page are:"
- og_title = content.find('meta', {'property': 'og:title'})
+ og_title = content.find("meta", {"property": "og:title"})
self.assertIsNotNone(og_title)
- og_type = content.find('meta', {'property': 'og:type'})
+ og_type = content.find("meta", {"property": "og:type"})
self.assertIsNotNone(og_type)
- og_image = content.find('meta', {'property': 'og:image'})
+ og_image = content.find("meta", {"property": "og:image"})
self.assertIsNotNone(og_image)
- og_url = content.find('meta', {'property': 'og:url'})
+ og_url = content.find("meta", {"property": "og:url"})
self.assertIsNotNone(og_url)
# And some basic validity checks
- self.assertEqual(og_type.get('content'), 'website')
- self.assertIn('png', og_image.get('content'))
- self.assertIn('Test', og_title.get('content'))
- self.assertIn('http', og_url.get('content'))
+ self.assertEqual(og_type.get("content"), "website")
+ self.assertIn("png", og_image.get("content"))
+ self.assertIn("Test", og_title.get("content"))
+ self.assertIn("http", og_url.get("content"))
def test_opengraph_metadata_missing_on_404_page(self):
view = self.makeTemplateView(
- 'main_side', view_attributes={'show_opengraph_meta': False})
+ "main_side", view_attributes={"show_opengraph_meta": False}
+ )
content = BeautifulSoup(view())
- og_title = content.find('meta', {'property': 'og:title'})
+ og_title = content.find("meta", {"property": "og:title"})
self.assertIsNone(og_title)
- og_type = content.find('meta', {'property': 'og:type'})
+ og_type = content.find("meta", {"property": "og:type"})
self.assertIsNone(og_type)
- og_image = content.find('meta', {'property': 'og:image'})
+ og_image = content.find("meta", {"property": "og:image"})
self.assertIsNone(og_image)
- og_url = content.find('meta', {'property': 'og:url'})
+ og_url = content.find("meta", {"property": "og:url"})
self.assertIsNone(og_url)
diff --git a/lib/lp/app/browser/tests/test_css_formatter.py b/lib/lp/app/browser/tests/test_css_formatter.py
index 5c28923..c60af07 100644
--- a/lib/lp/app/browser/tests/test_css_formatter.py
+++ b/lib/lp/app/browser/tests/test_css_formatter.py
@@ -5,10 +5,7 @@
from testtools.matchers import Equals
-from lp.testing import (
- test_tales,
- TestCase,
- )
+from lp.testing import TestCase, test_tales
from lp.testing.layers import DatabaseFunctionalLayer
@@ -17,20 +14,21 @@ class TestCSSFormatter(TestCase):
layer = DatabaseFunctionalLayer
def test_select(self):
- value = test_tales('value/css:select/visible/hidden', value=None)
- self.assertThat(value, Equals('hidden'))
- value = test_tales('value/css:select/visible/hidden', value=False)
- self.assertThat(value, Equals('hidden'))
- value = test_tales('value/css:select/visible/hidden', value='')
- self.assertThat(value, Equals('hidden'))
- value = test_tales('value/css:select/visible/hidden', value=True)
- self.assertThat(value, Equals('visible'))
- value = test_tales('value/css:select/visible/hidden', value='Hello')
- self.assertThat(value, Equals('visible'))
- value = test_tales('value/css:select/visible/hidden', value=object())
- self.assertThat(value, Equals('visible'))
+ value = test_tales("value/css:select/visible/hidden", value=None)
+ self.assertThat(value, Equals("hidden"))
+ value = test_tales("value/css:select/visible/hidden", value=False)
+ self.assertThat(value, Equals("hidden"))
+ value = test_tales("value/css:select/visible/hidden", value="")
+ self.assertThat(value, Equals("hidden"))
+ value = test_tales("value/css:select/visible/hidden", value=True)
+ self.assertThat(value, Equals("visible"))
+ value = test_tales("value/css:select/visible/hidden", value="Hello")
+ self.assertThat(value, Equals("visible"))
+ value = test_tales("value/css:select/visible/hidden", value=object())
+ self.assertThat(value, Equals("visible"))
def test_select_chaining(self):
value = test_tales(
- 'value/css:select/VISIBLE/hidden/fmt:lower', value=None)
- self.assertThat(value, Equals('hidden'))
+ "value/css:select/VISIBLE/hidden/fmt:lower", value=None
+ )
+ self.assertThat(value, Equals("hidden"))
diff --git a/lib/lp/app/browser/tests/test_formatters.py b/lib/lp/app/browser/tests/test_formatters.py
index c917b5e..b659677 100644
--- a/lib/lp/app/browser/tests/test_formatters.py
+++ b/lib/lp/app/browser/tests/test_formatters.py
@@ -3,15 +3,9 @@
"""Tests for the TALES formatters."""
-from lp.app.browser.tales import (
- ObjectFormatterAPI,
- PillarFormatterAPI,
- )
+from lp.app.browser.tales import ObjectFormatterAPI, PillarFormatterAPI
from lp.services.webapp.publisher import canonical_url
-from lp.testing import (
- FakeAdapterMixin,
- TestCaseWithFactory,
- )
+from lp.testing import FakeAdapterMixin, TestCaseWithFactory
from lp.testing.layers import DatabaseFunctionalLayer
from lp.testing.views import create_view
@@ -21,90 +15,112 @@ class ObjectFormatterAPITestCase(TestCaseWithFactory, FakeAdapterMixin):
layer = DatabaseFunctionalLayer
def test_pagetitle_top_level(self):
- project = self.factory.makeProduct(name='fnord')
- view = create_view(project, name='+index', current_request=True)
+ project = self.factory.makeProduct(name="fnord")
+ view = create_view(project, name="+index", current_request=True)
view.request.traversed_objects = [project, view]
formatter = ObjectFormatterAPI(view)
- self.assertEqual('Fnord in Launchpad', formatter.pagetitle())
+ self.assertEqual("Fnord in Launchpad", formatter.pagetitle())
def test_pagetitle_vhost(self):
- project = self.factory.makeProduct(name='fnord')
- view = create_view(project, name='+bugs', rootsite='bugs',
- current_request=True, server_url='https://bugs.launchpad.test/')
+ project = self.factory.makeProduct(name="fnord")
+ view = create_view(
+ project,
+ name="+bugs",
+ rootsite="bugs",
+ current_request=True,
+ server_url="https://bugs.launchpad.test/",
+ )
view.request.traversed_objects = [project, view]
formatter = ObjectFormatterAPI(view)
- self.assertEqual('Bugs : Fnord', formatter.pagetitle())
+ self.assertEqual("Bugs : Fnord", formatter.pagetitle())
def test_pagetitle_lower_level_default_view(self):
- project = self.factory.makeProduct(name='fnord')
+ project = self.factory.makeProduct(name="fnord")
view = create_view(
- project.development_focus, name='+index', current_request=True)
+ project.development_focus, name="+index", current_request=True
+ )
view.request.traversed_objects = [
- project, project.development_focus, view]
+ project,
+ project.development_focus,
+ view,
+ ]
formatter = ObjectFormatterAPI(view)
- self.assertEqual('Series trunk : Fnord', formatter.pagetitle())
+ self.assertEqual("Series trunk : Fnord", formatter.pagetitle())
def test_pagetitle_lower_level_named_view(self):
- project = self.factory.makeProduct(name='fnord')
+ project = self.factory.makeProduct(name="fnord")
view = create_view(
- project.development_focus, name='+edit', current_request=True)
+ project.development_focus, name="+edit", current_request=True
+ )
view.request.traversed_objects = [
- project, project.development_focus, view]
+ project,
+ project.development_focus,
+ view,
+ ]
formatter = ObjectFormatterAPI(view)
self.assertEqual(
- 'Edit Fnord trunk series : Series trunk : Fnord',
- formatter.pagetitle())
+ "Edit Fnord trunk series : Series trunk : Fnord",
+ formatter.pagetitle(),
+ )
def test_pagetitle_last_breadcrumb_detail(self):
- project = self.factory.makeProduct(name='fnord')
- bug = self.factory.makeBug(target=project, title='bang')
+ project = self.factory.makeProduct(name="fnord")
+ bug = self.factory.makeBug(target=project, title="bang")
view = create_view(
- bug.bugtasks[0], name='+index', rootsite='bugs',
- current_request=True, server_url='https://bugs.launchpad.test/')
+ bug.bugtasks[0],
+ name="+index",
+ rootsite="bugs",
+ current_request=True,
+ server_url="https://bugs.launchpad.test/",
+ )
view.request.traversed_objects = [project, bug.bugtasks[0], view]
formatter = ObjectFormatterAPI(view)
self.assertEqual(
- '%s \u201cbang\u201d : Bugs : Fnord' % bug.displayname,
- formatter.pagetitle())
+ "%s \u201cbang\u201d : Bugs : Fnord" % bug.displayname,
+ formatter.pagetitle(),
+ )
def test_pagetitle_last_breadcrumb_detail_too_long(self):
- project = self.factory.makeProduct(name='fnord')
- title = 'Bang out go the lights ' * 4
+ project = self.factory.makeProduct(name="fnord")
+ title = "Bang out go the lights " * 4
bug = self.factory.makeBug(target=project, title=title)
view = create_view(
- bug.bugtasks[0], name='+index', rootsite='bugs',
- current_request=True, server_url='https://bugs.launchpad.test/')
+ bug.bugtasks[0],
+ name="+index",
+ rootsite="bugs",
+ current_request=True,
+ server_url="https://bugs.launchpad.test/",
+ )
view.request.traversed_objects = [project, bug.bugtasks[0], view]
formatter = ObjectFormatterAPI(view)
- detail = '%s \u201c%s\u201d' % (bug.displayname, title)
- expected_title = '%s...\u201d : Bugs : Fnord' % detail[0:64]
+ detail = "%s \u201c%s\u201d" % (bug.displayname, title)
+ expected_title = "%s...\u201d : Bugs : Fnord" % detail[0:64]
self.assertEqual(expected_title, formatter.pagetitle())
def test_global_css(self):
person = self.factory.makePerson()
view = create_view(person, name="+index")
formatter = ObjectFormatterAPI(view)
- self.assertEqual('public', formatter.global_css())
+ self.assertEqual("public", formatter.global_css())
view = create_view(person, name="+archivesubscriptions")
formatter = ObjectFormatterAPI(view)
- self.assertEqual(
- 'private',
- formatter.global_css())
+ self.assertEqual("private", formatter.global_css())
class TestPillarFormatterAPI(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
- FORMATTER_CSS_CLASS = 'sprite product'
+ FORMATTER_CSS_CLASS = "sprite product"
def setUp(self):
super().setUp()
self.product = self.factory.makeProduct()
self.formatter = PillarFormatterAPI(self.product)
self.product_url = canonical_url(
- self.product, path_only_if_possible=True)
+ self.product, path_only_if_possible=True
+ )
def test_link(self):
# Calling PillarFormatterAPI.link() will return a link to the
@@ -113,10 +129,10 @@ class TestPillarFormatterAPI(TestCaseWithFactory):
link = self.formatter.link(None)
template = '<a href="%(url)s" class="%(css_class)s">%(summary)s</a>'
mapping = {
- 'url': self.product_url,
- 'summary': self.product.displayname,
- 'css_class': self.FORMATTER_CSS_CLASS,
- }
+ "url": self.product_url,
+ "summary": self.product.displayname,
+ "css_class": self.FORMATTER_CSS_CLASS,
+ }
self.assertEqual(link, template % mapping)
def test_link_with_displayname(self):
@@ -128,11 +144,11 @@ class TestPillarFormatterAPI(TestCaseWithFactory):
template = (
'<a href="%(url)s" class="%(css_class)s">%(summary)s</a>'
' (<a href="%(url)s">%(name)s</a>)'
- )
+ )
mapping = {
- 'url': self.product_url,
- 'summary': self.product.displayname,
- 'name': self.product.name,
- 'css_class': self.FORMATTER_CSS_CLASS,
- }
+ "url": self.product_url,
+ "summary": self.product.displayname,
+ "name": self.product.name,
+ "css_class": self.FORMATTER_CSS_CLASS,
+ }
self.assertEqual(link, template % mapping)
diff --git a/lib/lp/app/browser/tests/test_inlineeditpickerwidget.py b/lib/lp/app/browser/tests/test_inlineeditpickerwidget.py
index bb78430..c6fe5c9 100644
--- a/lib/lp/app/browser/tests/test_inlineeditpickerwidget.py
+++ b/lib/lp/app/browser/tests/test_inlineeditpickerwidget.py
@@ -3,20 +3,14 @@
"""Tests for the InlineEditPickerWidget."""
-from zope.interface import (
- implementer,
- Interface,
- )
+from zope.interface import Interface, implementer
from zope.schema import Choice
from lp.app.browser.lazrjs import (
InlineEditPickerWidget,
InlinePersonEditPickerWidget,
- )
-from lp.testing import (
- login_person,
- TestCaseWithFactory,
- )
+)
+from lp.testing import TestCaseWithFactory, login_person
from lp.testing.layers import DatabaseFunctionalLayer
@@ -27,39 +21,47 @@ class TestInlineEditPickerWidget(TestCaseWithFactory):
def getWidget(self, **kwargs):
class ITest(Interface):
test_field = Choice(**kwargs)
+
return InlineEditPickerWidget(
- None, ITest['test_field'], None, edit_url='fake')
+ None, ITest["test_field"], None, edit_url="fake"
+ )
def test_huge_vocabulary_is_searchable(self):
# Make sure that when given a field for a huge vocabulary, the picker
# is set to show the search box.
- widget = self.getWidget(vocabulary='ValidPersonOrTeam')
- self.assertTrue(widget.config['show_search_box'])
+ widget = self.getWidget(vocabulary="ValidPersonOrTeam")
+ self.assertTrue(widget.config["show_search_box"])
def test_vocabulary_filters(self):
# Make sure that when given a vocabulary which supports vocab filters,
# the vocab filters are include in the widget config.
- widget = self.getWidget(vocabulary='ValidPersonOrTeam')
- self.assertEqual([
- {'name': 'ALL',
- 'title': 'All',
- 'description': 'Display all search results'},
- {'name': 'PERSON',
- 'title': 'Person',
- 'description':
- 'Display search results for people only'},
- {'name': 'TEAM',
- 'title': 'Team',
- 'description':
- 'Display search results for teams only'}
+ widget = self.getWidget(vocabulary="ValidPersonOrTeam")
+ self.assertEqual(
+ [
+ {
+ "name": "ALL",
+ "title": "All",
+ "description": "Display all search results",
+ },
+ {
+ "name": "PERSON",
+ "title": "Person",
+ "description": "Display search results for people only",
+ },
+ {
+ "name": "TEAM",
+ "title": "Team",
+ "description": "Display search results for teams only",
+ },
],
- widget.config['vocabulary_filters'])
+ widget.config["vocabulary_filters"],
+ )
def test_normal_vocabulary_is_not_searchable(self):
# Make sure that when given a field for a normal vocabulary, the
# picker is set to show the search box.
- widget = self.getWidget(vocabulary='UserTeamsParticipation')
- self.assertFalse(widget.config['show_search_box'])
+ widget = self.getWidget(vocabulary="UserTeamsParticipation")
+ self.assertFalse(widget.config["show_search_box"])
class TestInlinePersonEditPickerWidget(TestCaseWithFactory):
@@ -72,52 +74,60 @@ class TestInlinePersonEditPickerWidget(TestCaseWithFactory):
@implementer(ITest)
class Test:
-
def __init__(self):
self.test_field = widget_value
context = Test()
return InlinePersonEditPickerWidget(
- context, ITest['test_field'], None, edit_url='fake',
- show_create_team=show_create_team)
+ context,
+ ITest["test_field"],
+ None,
+ edit_url="fake",
+ show_create_team=show_create_team,
+ )
def test_person_selected_value_meta(self):
# The widget has the correct meta value for a person value.
widget_value = self.factory.makePerson()
- widget = self.getWidget(widget_value, vocabulary='ValidPersonOrTeam')
- self.assertEqual('person', widget.config['selected_value_metadata'])
+ widget = self.getWidget(widget_value, vocabulary="ValidPersonOrTeam")
+ self.assertEqual("person", widget.config["selected_value_metadata"])
def test_team_selected_value_meta(self):
# The widget has the correct meta value for a team value.
widget_value = self.factory.makeTeam()
- widget = self.getWidget(widget_value, vocabulary='ValidPersonOrTeam')
- self.assertEqual('team', widget.config['selected_value_metadata'])
+ widget = self.getWidget(widget_value, vocabulary="ValidPersonOrTeam")
+ self.assertEqual("team", widget.config["selected_value_metadata"])
def test_required_fields_dont_have_a_remove_link(self):
widget = self.getWidget(
- None, vocabulary='ValidPersonOrTeam', required=True)
- self.assertFalse(widget.config['show_remove_button'])
+ None, vocabulary="ValidPersonOrTeam", required=True
+ )
+ self.assertFalse(widget.config["show_remove_button"])
def test_optional_fields_do_have_a_remove_link(self):
widget = self.getWidget(
- None, vocabulary='ValidPersonOrTeam', required=False)
- self.assertTrue(widget.config['show_remove_button'])
+ None, vocabulary="ValidPersonOrTeam", required=False
+ )
+ self.assertTrue(widget.config["show_remove_button"])
def test_assign_me_exists_if_user_in_vocabulary(self):
widget = self.getWidget(
- None, vocabulary='ValidPersonOrTeam', required=True)
+ None, vocabulary="ValidPersonOrTeam", required=True
+ )
login_person(self.factory.makePerson())
- self.assertTrue(widget.config['show_assign_me_button'])
+ self.assertTrue(widget.config["show_assign_me_button"])
def test_assign_me_not_shown_if_user_not_in_vocabulary(self):
- widget = self.getWidget(
- None, vocabulary='TargetPPAs', required=True)
+ widget = self.getWidget(None, vocabulary="TargetPPAs", required=True)
login_person(self.factory.makePerson())
- self.assertFalse(widget.config['show_assign_me_button'])
+ self.assertFalse(widget.config["show_assign_me_button"])
def test_show_create_team_link(self):
widget = self.getWidget(
- None, vocabulary='ValidPersonOrTeam', required=True,
- show_create_team=True)
+ None,
+ vocabulary="ValidPersonOrTeam",
+ required=True,
+ show_create_team=True,
+ )
login_person(self.factory.makePerson())
- self.assertTrue(widget.config['show_create_team'])
+ self.assertTrue(widget.config["show_create_team"])
diff --git a/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py b/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py
index 745f203..2e0af72 100644
--- a/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py
+++ b/lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py
@@ -3,11 +3,8 @@
"""Tests for the InlineMultiCheckboxWidget."""
-from lazr.enum import (
- EnumeratedType,
- Item,
- )
import simplejson
+from lazr.enum import EnumeratedType, Item
from zope.interface import Interface
from zope.schema import List
from zope.schema._field import Choice
@@ -21,6 +18,7 @@ from lp.testing.layers import DatabaseFunctionalLayer
class Alphabet(EnumeratedType):
"""A vocabulary for testing."""
+
A = Item("A", "Letter A")
B = Item("B", "Letter B")
@@ -30,33 +28,35 @@ class TestInlineMultiCheckboxWidget(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
def _getWidget(self, **kwargs):
-
class ITest(Interface):
- test_field = List(
- Choice(vocabulary='BuildableDistroSeries'))
+ test_field = List(Choice(vocabulary="BuildableDistroSeries"))
+
return InlineMultiCheckboxWidget(
- None, ITest['test_field'], "Label", edit_url='fake', **kwargs)
+ None, ITest["test_field"], "Label", edit_url="fake", **kwargs
+ )
def _makeExpectedItems(self, vocab, selected=list(), value_fn=None):
if value_fn is None:
value_fn = lambda item: item.value.name
expected_items = []
- style = 'font-weight: normal;'
+ style = "font-weight: normal;"
for item in vocab:
new_item = {
- 'name': item.title,
- 'token': item.token,
- 'style': style,
- 'checked': (item.value in selected),
- 'value': value_fn(item)}
+ "name": item.title,
+ "token": item.token,
+ "style": style,
+ "checked": (item.value in selected),
+ "value": value_fn(item),
+ }
expected_items.append(new_item)
return expected_items
def test_items_for_field_vocabulary(self):
widget = self._getWidget(attribute_type="reference")
- vocab = getVocabularyRegistry().get(None, 'BuildableDistroSeries')
+ vocab = getVocabularyRegistry().get(None, "BuildableDistroSeries")
value_fn = lambda item: canonical_url(
- item.value, force_local_path=True)
+ item.value, force_local_path=True
+ )
expected_items = self._makeExpectedItems(vocab, value_fn=value_fn)
self.assertEqual(simplejson.dumps(expected_items), widget.json_items)
@@ -73,7 +73,9 @@ class TestInlineMultiCheckboxWidget(TestCaseWithFactory):
def test_selected_items_checked(self):
widget = self._getWidget(
- vocabulary=Alphabet, selected_items=[Alphabet.A])
+ vocabulary=Alphabet, selected_items=[Alphabet.A]
+ )
expected_items = self._makeExpectedItems(
- Alphabet, selected=[Alphabet.A])
+ Alphabet, selected=[Alphabet.A]
+ )
self.assertEqual(simplejson.dumps(expected_items), widget.json_items)
diff --git a/lib/lp/app/browser/tests/test_launchpad.py b/lib/lp/app/browser/tests/test_launchpad.py
index 3b2ceaa..72605c3 100644
--- a/lib/lp/app/browser/tests/test_launchpad.py
+++ b/lib/lp/app/browser/tests/test_launchpad.py
@@ -3,29 +3,22 @@
"""Tests for traversal from the root branch object."""
-from zope.component import (
- getMultiAdapter,
- getUtility,
- )
+from zope.component import getMultiAdapter, getUtility
from zope.publisher.interfaces import NotFound
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from lp.app.browser.launchpad import (
- iter_view_registrations,
LaunchpadRootNavigation,
- )
+ iter_view_registrations,
+)
from lp.app.enums import InformationType
from lp.app.errors import GoneError
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.interfaces.services import IService
from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
-from lp.registry.enums import (
- PersonVisibility,
- SharingPermission,
- VCSType,
- )
+from lp.registry.enums import PersonVisibility, SharingPermission, VCSType
from lp.registry.interfaces.person import IPersonSet
from lp.services.identity.interfaces.account import AccountStatus
from lp.services.webapp import canonical_url
@@ -33,39 +26,34 @@ from lp.services.webapp.escaping import html_escape
from lp.services.webapp.interfaces import (
BrowserNotificationLevel,
ILaunchpadRoot,
- )
+)
from lp.services.webapp.servers import (
LaunchpadTestRequest,
WebServiceTestRequest,
- )
+)
from lp.services.webapp.url import urlappend
from lp.testing import (
- admin_logged_in,
ANONYMOUS,
+ TestCaseWithFactory,
+ admin_logged_in,
celebrity_logged_in,
login,
login_person,
person_logged_in,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- FunctionalLayer,
- )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, FunctionalLayer
from lp.testing.publication import test_traverse
from lp.testing.views import create_view
-
# We set the request header HTTP_REFERER when we want to simulate navigation
# from a valid page. This is used in the assertDisplaysNotification check.
-DEFAULT_REFERER = 'http://launchpad.test'
+DEFAULT_REFERER = "http://launchpad.test"
class TraversalMixin:
-
def _validateNotificationContext(
- self, request, notification=None,
- level=BrowserNotificationLevel.INFO):
+ self, request, notification=None, level=BrowserNotificationLevel.INFO
+ ):
"""Check the browser notifications associated with the request.
Ensure that the notification instances attached to the request match
@@ -86,8 +74,8 @@ class TraversalMixin:
self.assertEqual(notification, notifications[0].message)
def assertDisplaysNotification(
- self, path, notification=None,
- level=BrowserNotificationLevel.INFO):
+ self, path, notification=None, level=BrowserNotificationLevel.INFO
+ ):
"""Assert that an invalid path redirects back to referrer.
The request object is expected to have a notification message to
@@ -103,19 +91,24 @@ class TraversalMixin:
redirection = self.traverse(path)
self.assertIs(redirection.target, DEFAULT_REFERER)
self._validateNotificationContext(
- redirection.request, notification, level)
+ redirection.request, notification, level
+ )
def assertNotFound(self, path, use_default_referer=True):
self.assertRaises(
- NotFound, self.traverse, path,
- use_default_referer=use_default_referer)
+ NotFound,
+ self.traverse,
+ path,
+ use_default_referer=use_default_referer,
+ )
def assertRedirects(self, segments, url, webservice=False):
redirection = self.traverse(segments, webservice=webservice)
self.assertEqual(url, redirection.target)
- def traverse(self, path, first_segment, use_default_referer=True,
- webservice=False):
+ def traverse(
+ self, path, first_segment, use_default_referer=True, webservice=False
+ ):
"""Traverse to 'path' using a 'LaunchpadRootNavigation' object.
Using the Zope traversal machinery, traverse to the path given by
@@ -134,16 +127,18 @@ class TraversalMixin:
"""
# XXX: What's the difference between first_segment and path? -- mbp
# 2011-06-27.
- extra = {'PATH_INFO': urlappend('/%s' % first_segment, path)}
+ extra = {"PATH_INFO": urlappend("/%s" % first_segment, path)}
if use_default_referer:
- extra['HTTP_REFERER'] = DEFAULT_REFERER
+ extra["HTTP_REFERER"] = DEFAULT_REFERER
request_factory = (
- WebServiceTestRequest if webservice else LaunchpadTestRequest)
+ WebServiceTestRequest if webservice else LaunchpadTestRequest
+ )
request = request_factory(**extra)
- segments = reversed(path.split('/'))
+ segments = reversed(path.split("/"))
request.setTraversalStack(segments)
traverser = LaunchpadRootNavigation(
- getUtility(ILaunchpadRoot), request=request)
+ getUtility(ILaunchpadRoot), request=request
+ )
return traverser.publishTraverse(request, first_segment)
@@ -159,15 +154,17 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
def assertDisplaysNotice(self, path, notification):
"""Assert that traversal redirects back with the specified notice."""
self.assertDisplaysNotification(
- path, notification, BrowserNotificationLevel.INFO)
+ path, notification, BrowserNotificationLevel.INFO
+ )
def assertDisplaysError(self, path, notification):
"""Assert that traversal redirects back with the specified notice."""
self.assertDisplaysNotification(
- path, notification, BrowserNotificationLevel.ERROR)
+ path, notification, BrowserNotificationLevel.ERROR
+ )
def traverse(self, path, **kwargs):
- return super().traverse(path, '+branch', **kwargs)
+ return super().traverse(path, "+branch", **kwargs)
def test_unique_name_traversal(self):
# Traversing to /+branch/<unique_name> redirects to the page for that
@@ -175,26 +172,31 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
branch = self.factory.makeAnyBranch()
self.assertRedirects(branch.unique_name, canonical_url(branch))
self.assertRedirects(
- branch.unique_name, canonical_url(branch, rootsite='api'),
- webservice=True)
+ branch.unique_name,
+ canonical_url(branch, rootsite="api"),
+ webservice=True,
+ )
def test_no_such_unique_name(self):
# Traversing to /+branch/<unique_name> where 'unique_name' is for a
# branch that doesn't exist will display an error message.
branch = self.factory.makeAnyBranch()
- bad_name = branch.unique_name + 'wibble'
+ bad_name = branch.unique_name + "wibble"
required_message = html_escape(
- "No such branch: '%s'." % (branch.name + "wibble"))
+ "No such branch: '%s'." % (branch.name + "wibble")
+ )
self.assertDisplaysError(bad_name, required_message)
def test_private_branch(self):
# If an attempt is made to access a private branch, display an error.
branch = self.factory.makeProductBranch(
- information_type=InformationType.USERDATA)
+ information_type=InformationType.USERDATA
+ )
branch_unique_name = removeSecurityProxy(branch).unique_name
login(ANONYMOUS)
required_message = html_escape(
- "No such branch: '%s'." % branch_unique_name)
+ "No such branch: '%s'." % branch_unique_name
+ )
self.assertDisplaysError(branch_unique_name, required_message)
def test_product_alias(self):
@@ -211,17 +213,16 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
branch = self.factory.makeProductBranch()
naked_product = removeSecurityProxy(branch.product)
ICanHasLinkedBranch(naked_product).setBranch(branch)
- removeSecurityProxy(branch).information_type = (
- InformationType.USERDATA)
+ removeSecurityProxy(branch).information_type = InformationType.USERDATA
login(ANONYMOUS)
requiredMessage = (
- "The target %s does not have a linked branch." %
- naked_product.name)
+ "The target %s does not have a linked branch." % naked_product.name
+ )
self.assertDisplaysNotice(naked_product.name, requiredMessage)
def test_nonexistent_product(self):
# Traversing to /+branch/<no-such-product> displays an error message.
- non_existent = 'non-existent'
+ non_existent = "non-existent"
required_message = "No such product: '%s'." % non_existent
self.assertDisplaysError(non_existent, html_escape(required_message))
@@ -229,7 +230,7 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
# Traversing to /+branch/<no-such-product> without a referer results
# in a 404 error. This happens if the user hacks the URL rather than
# navigating via a link
- self.assertNotFound('non-existent', use_default_referer=False)
+ self.assertNotFound("non-existent", use_default_referer=False)
def test_private_without_referer(self):
# If the development focus of a product is private and there is no
@@ -238,8 +239,7 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
branch = self.factory.makeProductBranch()
naked_product = removeSecurityProxy(branch.product)
ICanHasLinkedBranch(naked_product).setBranch(branch)
- removeSecurityProxy(branch).information_type = (
- InformationType.USERDATA)
+ removeSecurityProxy(branch).information_type = InformationType.USERDATA
login(ANONYMOUS)
self.assertNotFound(naked_product.name, use_default_referer=False)
@@ -248,7 +248,8 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
# user message on the same page.
product = self.factory.makeProduct()
requiredMessage = (
- "The target %s does not have a linked branch." % product.name)
+ "The target %s does not have a linked branch." % product.name
+ )
self.assertDisplaysNotice(product.name, requiredMessage)
def test_distro_package_alias(self):
@@ -270,31 +271,31 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
sourcepackage = self.factory.makeSourcePackage()
branch = self.factory.makePackageBranch(
sourcepackage=sourcepackage,
- information_type=InformationType.USERDATA)
+ information_type=InformationType.USERDATA,
+ )
distro_package = sourcepackage.distribution_sourcepackage
registrant = distro_package.distribution.owner
with person_logged_in(registrant):
ICanHasLinkedBranch(distro_package).setBranch(branch, registrant)
login(ANONYMOUS)
path = ICanHasLinkedBranch(distro_package).bzr_path
- requiredMessage = (
- "The target %s does not have a linked branch." % path)
+ requiredMessage = "The target %s does not have a linked branch." % path
self.assertDisplaysNotice(path, requiredMessage)
def test_trailing_path_redirect(self):
# If there are any trailing path segments after the branch identifier,
# these stick around at the redirected URL.
branch = self.factory.makeAnyBranch()
- path = urlappend(branch.unique_name, '+edit')
- self.assertRedirects(path, canonical_url(branch, view_name='+edit'))
+ path = urlappend(branch.unique_name, "+edit")
+ self.assertRedirects(path, canonical_url(branch, view_name="+edit"))
def test_alias_trailing_path_redirect(self):
# Redirects also support trailing path segments with aliases.
branch = self.factory.makeProductBranch()
with person_logged_in(branch.product.owner):
branch.product.development_focus.branch = branch
- path = '%s/+edit' % branch.product.name
- self.assertRedirects(path, canonical_url(branch, view_name='+edit'))
+ path = "%s/+edit" % branch.product.name
+ self.assertRedirects(path, canonical_url(branch, view_name="+edit"))
def test_product_series_redirect(self):
# Traversing to /+branch/<product>/<series> redirects to the branch
@@ -302,27 +303,27 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
branch = self.factory.makeBranch()
series = self.factory.makeProductSeries(branch=branch)
self.assertRedirects(
- ICanHasLinkedBranch(series).bzr_path, canonical_url(branch))
+ ICanHasLinkedBranch(series).bzr_path, canonical_url(branch)
+ )
def test_no_branch_for_series(self):
# If there's no branch for a product series, display a
# message telling the user there is no linked branch.
series = self.factory.makeProductSeries()
path = ICanHasLinkedBranch(series).bzr_path
- requiredMessage = (
- "The target %s does not have a linked branch." % path)
+ requiredMessage = "The target %s does not have a linked branch." % path
self.assertDisplaysNotice(path, requiredMessage)
def test_private_branch_for_series(self):
# If the development focus of a product series is private, display a
# message telling the user there is no linked branch.
branch = self.factory.makeBranch(
- information_type=InformationType.USERDATA)
+ information_type=InformationType.USERDATA
+ )
series = self.factory.makeProductSeries(branch=branch)
login(ANONYMOUS)
path = ICanHasLinkedBranch(series).bzr_path
- requiredMessage = (
- "The target %s does not have a linked branch." % path)
+ requiredMessage = "The target %s does not have a linked branch." % path
self.assertDisplaysNotice(path, requiredMessage)
def test_too_short_branch_name(self):
@@ -330,13 +331,14 @@ class TestBranchTraversal(TestCaseWithFactory, TraversalMixin):
# that's too short to be a real unique name.
owner = self.factory.makePerson()
requiredMessage = html_escape(
- "Cannot understand namespace name: '%s'" % owner.name)
- self.assertDisplaysError('~%s' % owner.name, requiredMessage)
+ "Cannot understand namespace name: '%s'" % owner.name
+ )
+ self.assertDisplaysError("~%s" % owner.name, requiredMessage)
def test_invalid_product_name(self):
# error notification if the thing following +branch has an invalid
# product name.
- self.assertDisplaysError('_foo', "Invalid name for product: _foo.")
+ self.assertDisplaysError("_foo", "Invalid name for product: _foo.")
def test_invalid_product_name_without_referer(self):
# error notification if the thing following +branch has an invalid
@@ -349,42 +351,48 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
layer = DatabaseFunctionalLayer
def traverse(self, path, **kwargs):
- return super().traverse(path, '+code', **kwargs)
+ return super().traverse(path, "+code", **kwargs)
def test_project_bzr_branch(self):
branch = self.factory.makeAnyBranch()
self.assertRedirects(branch.unique_name, canonical_url(branch))
self.assertRedirects(
- branch.unique_name, canonical_url(branch, rootsite='api'),
- webservice=True)
+ branch.unique_name,
+ canonical_url(branch, rootsite="api"),
+ webservice=True,
+ )
def test_project_git_branch(self):
git_repo = self.factory.makeGitRepository()
self.assertRedirects(git_repo.unique_name, canonical_url(git_repo))
self.assertRedirects(
- git_repo.unique_name, canonical_url(git_repo, rootsite='api'),
- webservice=True)
+ git_repo.unique_name,
+ canonical_url(git_repo, rootsite="api"),
+ webservice=True,
+ )
def test_no_such_bzr_unique_name(self):
branch = self.factory.makeAnyBranch()
- bad_name = branch.unique_name + 'wibble'
+ bad_name = branch.unique_name + "wibble"
self.assertNotFound(bad_name)
def test_no_such_git_unique_name(self):
repo = self.factory.makeGitRepository()
- bad_name = repo.unique_name + 'wibble'
+ bad_name = repo.unique_name + "wibble"
self.assertNotFound(bad_name)
def test_private_bzr_branch(self):
branch = self.factory.makeProductBranch(
- information_type=InformationType.USERDATA)
+ information_type=InformationType.USERDATA
+ )
branch_unique_name = removeSecurityProxy(branch).unique_name
login(ANONYMOUS)
self.assertNotFound(branch_unique_name)
def test_private_git_branch(self):
git_repo = self.factory.makeGitRepository(
- information_type=InformationType.USERDATA)
+ information_type=InformationType.USERDATA
+ )
repo_unique_name = removeSecurityProxy(git_repo).unique_name
login(ANONYMOUS)
self.assertNotFound(repo_unique_name)
@@ -401,15 +409,15 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
naked_project = removeSecurityProxy(project)
with person_logged_in(repo.target.owner):
getUtility(IGitRepositorySet).setDefaultRepository(
- repo.target, repo)
+ repo.target, repo
+ )
self.assertRedirects(naked_project.name, canonical_url(repo))
def test_private_bzr_branch_for_product(self):
branch = self.factory.makeProductBranch()
naked_product = removeSecurityProxy(branch.product)
ICanHasLinkedBranch(naked_product).setBranch(branch)
- removeSecurityProxy(branch).information_type = (
- InformationType.USERDATA)
+ removeSecurityProxy(branch).information_type = InformationType.USERDATA
login(ANONYMOUS)
self.assertNotFound(naked_product.name)
@@ -418,17 +426,17 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
repo = self.factory.makeGitRepository(target=project)
with person_logged_in(repo.target.owner):
getUtility(IGitRepositorySet).setDefaultRepository(
- repo.target, repo)
+ repo.target, repo
+ )
- removeSecurityProxy(repo).information_type = (
- InformationType.USERDATA)
+ removeSecurityProxy(repo).information_type = InformationType.USERDATA
login(ANONYMOUS)
naked_project = removeSecurityProxy(project)
self.assertNotFound(naked_project.name)
def test_nonexistent_product(self):
- non_existent = 'non-existent'
+ non_existent = "non-existent"
self.assertNotFound(non_existent)
def test_product_without_dev_focus(self):
@@ -452,7 +460,8 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
with admin_logged_in():
getUtility(IGitRepositorySet).setDefaultRepository(
- distro_package, repo)
+ distro_package, repo
+ )
self.assertRedirects("%s" % repo.shortened_path, canonical_url(repo))
@@ -460,7 +469,8 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
sourcepackage = self.factory.makeSourcePackage()
branch = self.factory.makePackageBranch(
sourcepackage=sourcepackage,
- information_type=InformationType.USERDATA)
+ information_type=InformationType.USERDATA,
+ )
distro_package = sourcepackage.distribution_sourcepackage
registrant = distro_package.distribution.owner
with person_logged_in(registrant):
@@ -473,46 +483,47 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
sourcepackage = self.factory.makeSourcePackage()
distro_package = sourcepackage.distribution_sourcepackage
repo = self.factory.makeGitRepository(
- target=distro_package,
- information_type=InformationType.USERDATA)
+ target=distro_package, information_type=InformationType.USERDATA
+ )
with admin_logged_in():
getUtility(IGitRepositorySet).setDefaultRepository(
- distro_package, repo)
+ distro_package, repo
+ )
login(ANONYMOUS)
path = removeSecurityProxy(repo).shortened_path
self.assertNotFound(path)
def test_trailing_path_redirect_bzr(self):
branch = self.factory.makeAnyBranch()
- path = urlappend(branch.unique_name, '+edit')
- self.assertRedirects(path, canonical_url(branch, view_name='+edit'))
+ path = urlappend(branch.unique_name, "+edit")
+ self.assertRedirects(path, canonical_url(branch, view_name="+edit"))
def test_trailing_path_redirect_git(self):
repo = self.factory.makeGitRepository()
- path = urlappend(repo.unique_name, '+edit')
- self.assertRedirects(path, canonical_url(repo, view_name='+edit'))
+ path = urlappend(repo.unique_name, "+edit")
+ self.assertRedirects(path, canonical_url(repo, view_name="+edit"))
def test_alias_trailing_path_redirect_bzr(self):
branch = self.factory.makeProductBranch()
with person_logged_in(branch.product.owner):
branch.product.development_focus.branch = branch
- path = '%s/+edit' % branch.product.name
- self.assertRedirects(path, canonical_url(branch, view_name='+edit'))
+ path = "%s/+edit" % branch.product.name
+ self.assertRedirects(path, canonical_url(branch, view_name="+edit"))
def test_alias_trailing_path_redirect_git(self):
project = self.factory.makeProduct()
repo = self.factory.makeGitRepository(target=project)
with admin_logged_in():
- getUtility(IGitRepositorySet).setDefaultRepository(
- project, repo)
- path = '%s/+edit' % project.name
- self.assertRedirects(path, canonical_url(repo, view_name='+edit'))
+ getUtility(IGitRepositorySet).setDefaultRepository(project, repo)
+ path = "%s/+edit" % project.name
+ self.assertRedirects(path, canonical_url(repo, view_name="+edit"))
def test_product_series_redirect_bzr(self):
branch = self.factory.makeBranch()
series = self.factory.makeProductSeries(branch=branch)
self.assertRedirects(
- ICanHasLinkedBranch(series).bzr_path, canonical_url(branch))
+ ICanHasLinkedBranch(series).bzr_path, canonical_url(branch)
+ )
def test_no_branch_for_series(self):
# If there's no branch for a product series, display a
@@ -525,7 +536,8 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
# If the development focus of a product series is private, display a
# message telling the user there is no linked branch.
branch = self.factory.makeBranch(
- information_type=InformationType.USERDATA)
+ information_type=InformationType.USERDATA
+ )
series = self.factory.makeProductSeries(branch=branch)
login(ANONYMOUS)
path = ICanHasLinkedBranch(series).bzr_path
@@ -533,10 +545,10 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
def test_too_short_branch_name(self):
owner = self.factory.makePerson()
- self.assertNotFound('~%s' % owner.name)
+ self.assertNotFound("~%s" % owner.name)
def test_invalid_product_name(self):
- self.assertNotFound('_foo')
+ self.assertNotFound("_foo")
def test_invalid_product_name_without_referer(self):
self.assertNotFound("_foo", use_default_referer=False)
@@ -556,8 +568,7 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
repo = self.factory.makeGitRepository(target=project)
with person_logged_in(project.owner):
ICanHasLinkedBranch(project).setBranch(bzr_branch, project.owner)
- getUtility(IGitRepositorySet).setDefaultRepository(
- project, repo)
+ getUtility(IGitRepositorySet).setDefaultRepository(project, repo)
self.assertNotFound(project.name)
@@ -567,8 +578,7 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
repo = self.factory.makeGitRepository(target=project)
with person_logged_in(project.owner):
ICanHasLinkedBranch(project).setBranch(bzr_branch, project.owner)
- getUtility(IGitRepositorySet).setDefaultRepository(
- project, repo)
+ getUtility(IGitRepositorySet).setDefaultRepository(project, repo)
project.vcs = VCSType.GIT
self.assertRedirects(project.name, canonical_url(repo))
@@ -579,8 +589,7 @@ class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
repo = self.factory.makeGitRepository(target=project)
with person_logged_in(project.owner):
ICanHasLinkedBranch(project).setBranch(bzr_branch, project.owner)
- getUtility(IGitRepositorySet).setDefaultRepository(
- project, repo)
+ getUtility(IGitRepositorySet).setDefaultRepository(project, repo)
project.vcs = VCSType.BZR
self.assertRedirects(project.name, canonical_url(bzr_branch))
@@ -603,7 +612,7 @@ class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
def setUp(self):
super().setUp()
self.any_user = self.factory.makePerson()
- self.admin = getUtility(IPersonSet).getByName('name16')
+ self.admin = getUtility(IPersonSet).getByName("name16")
self.registry_expert = self.factory.makePerson()
registry = getUtility(ILaunchpadCelebrities).registry_experts
with person_logged_in(registry.teamowner):
@@ -611,19 +620,19 @@ class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
def test_person(self):
# Verify a user is returned.
- name = 'active-person'
+ name = "active-person"
person = self.factory.makePerson(name=name)
- segment = '~%s' % name
+ segment = "~%s" % name
traversed = self.traverse(segment, segment)
self.assertEqual(person, traversed)
def test_suspended_person_visibility(self):
# Verify a suspended user is only traversable by an admin.
- name = 'suspended-person'
+ name = "suspended-person"
person = self.factory.makePerson(name=name)
login_person(self.admin)
- person.setAccountStatus(AccountStatus.SUSPENDED, None, 'Go away')
- segment = '~%s' % name
+ person.setAccountStatus(AccountStatus.SUSPENDED, None, "Go away")
+ segment = "~%s" % name
# Admins can see the suspended user.
traversed = self.traverse(segment, segment)
self.assertEqual(person, traversed)
@@ -636,10 +645,10 @@ class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
def test_placeholder_person_visibility(self):
# Verify a placeholder user is only traversable by an admin.
- name = 'placeholder-person'
+ name = "placeholder-person"
person = getUtility(IPersonSet).createPlaceholderPerson(name, name)
login_person(self.admin)
- segment = '~%s' % name
+ segment = "~%s" % name
# Admins can see the placeholder user.
traversed = self.traverse(segment, segment)
self.assertEqual(person, traversed)
@@ -653,19 +662,19 @@ class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
def test_public_team(self):
# Verify a public team is returned.
- name = 'public-team'
+ name = "public-team"
team = self.factory.makeTeam(name=name)
- segment = '~%s' % name
+ segment = "~%s" % name
traversed = self.traverse(segment, segment)
self.assertEqual(team, traversed)
def test_private_team_visible_to_admin_and_members_only(self):
# Verify a private team is team is returned.
- name = 'private-team'
+ name = "private-team"
team = self.factory.makeTeam(name=name)
login_person(self.admin)
team.visibility = PersonVisibility.PRIVATE
- segment = '~%s' % name
+ segment = "~%s" % name
# Admins can traverse to the team.
traversed = self.traverse(segment, segment)
self.assertEqual(team, traversed)
@@ -681,36 +690,33 @@ class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
# Just /~/ expands to the current user. (Bug 785800).
person = self.factory.makePerson()
login_person(person)
- obj, view, req = test_traverse('http://launchpad.test/~')
+ obj, view, req = test_traverse("http://launchpad.test/~")
view = removeSecurityProxy(view)
- self.assertEqual(
- canonical_url(person),
- view.target.rstrip('/'))
+ self.assertEqual(canonical_url(person), view.target.rstrip("/"))
def test_self_url_not_logged_in(self):
# /~/ when not logged in asks you to log in.
- self.assertRaises(Unauthorized,
- test_traverse, 'http://launchpad.test/~')
+ self.assertRaises(
+ Unauthorized, test_traverse, "http://launchpad.test/~"
+ )
def test_self_url_pathinfo(self):
# You can traverse below /~/.
person = self.factory.makePerson()
login_person(person)
- obj, view, req = test_traverse('http://launchpad.test/~/+editsshkeys')
+ obj, view, req = test_traverse("http://launchpad.test/~/+editsshkeys")
view = removeSecurityProxy(view)
- self.assertEqual(
- canonical_url(person) + '/+editsshkeys',
- view.target)
+ self.assertEqual(canonical_url(person) + "/+editsshkeys", view.target)
def test_self_url_app_domain(self):
# You can traverse below /~/.
person = self.factory.makePerson()
login_person(person)
- obj, view, req = test_traverse('http://bugs.launchpad.test/~')
+ obj, view, req = test_traverse("http://bugs.launchpad.test/~")
view = removeSecurityProxy(view)
self.assertEqual(
- canonical_url(person, rootsite='bugs'),
- view.target.rstrip('/'))
+ canonical_url(person, rootsite="bugs"), view.target.rstrip("/")
+ )
class TestErrorViews(TestCaseWithFactory):
@@ -718,9 +724,9 @@ class TestErrorViews(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
def test_GoneError(self):
- error = GoneError('User is suspended')
- view = create_view(error, 'index.html')
- self.assertEqual('Error: Page gone', view.page_title)
+ error = GoneError("User is suspended")
+ view = create_view(error, "index.html")
+ self.assertEqual("Error: Page gone", view.page_title)
self.assertEqual(410, view.request.response.getStatus())
@@ -729,17 +735,17 @@ class ExceptionHierarchyTestCase(TestCaseWithFactory):
layer = FunctionalLayer
def test_exception(self):
- view = create_view(IndexError('test'), '+hierarchy')
+ view = create_view(IndexError("test"), "+hierarchy")
view.request.traversed_objects = [getUtility(ILaunchpadRoot)]
self.assertEqual([], view.objects)
def test_zope_exception(self):
- view = create_view(Unauthorized('test'), '+hierarchy')
+ view = create_view(Unauthorized("test"), "+hierarchy")
view.request.traversed_objects = [getUtility(ILaunchpadRoot)]
self.assertEqual([], view.objects)
def test_launchapd_exception(self):
- view = create_view(NotFound(None, 'test'), '+hierarchy')
+ view = create_view(NotFound(None, "test"), "+hierarchy")
view.request.traversed_objects = [getUtility(ILaunchpadRoot)]
self.assertEqual([], view.objects)
@@ -751,11 +757,11 @@ class TestIterViewRegistrations(TestCaseWithFactory):
def test_iter_view_registrations(self):
"""iter_view_registrations provides only registrations of class."""
macros = getMultiAdapter(
- (object(), LaunchpadTestRequest()), name='+base-layout-macros')
- names = {
- reg.name for reg in iter_view_registrations(macros.__class__)}
- self.assertIn('+base-layout-macros', names)
- self.assertNotIn('+related-pages', names)
+ (object(), LaunchpadTestRequest()), name="+base-layout-macros"
+ )
+ names = {reg.name for reg in iter_view_registrations(macros.__class__)}
+ self.assertIn("+base-layout-macros", names)
+ self.assertNotIn("+related-pages", names)
class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
@@ -770,10 +776,12 @@ class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
self.proprietary_product_owner = self.factory.makePerson()
self.active_proprietary_product = self.factory.makeProduct(
owner=self.proprietary_product_owner,
- information_type=InformationType.PROPRIETARY)
+ information_type=InformationType.PROPRIETARY,
+ )
self.inactive_proprietary_product = self.factory.makeProduct(
owner=self.proprietary_product_owner,
- information_type=InformationType.PROPRIETARY)
+ information_type=InformationType.PROPRIETARY,
+ )
removeSecurityProxy(self.inactive_proprietary_product).active = False
def traverse_to_active_public_product(self):
@@ -798,11 +806,14 @@ class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
self.traverse_to_active_public_product()
# Access to other products raises a NotFound error.
self.assertRaises(
- NotFound, self.traverse_to_inactive_public_product)
+ NotFound, self.traverse_to_inactive_public_product
+ )
self.assertRaises(
- NotFound, self.traverse_to_active_proprietary_product)
+ NotFound, self.traverse_to_active_proprietary_product
+ )
self.assertRaises(
- NotFound, self.traverse_to_inactive_proprietary_product)
+ NotFound, self.traverse_to_inactive_proprietary_product
+ )
def test_access_for_ordinary_users(self):
# Ordinary logged in users can see only public active products.
@@ -810,32 +821,41 @@ class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
self.traverse_to_active_public_product()
# Access to other products raises a NotFound error.
self.assertRaises(
- NotFound, self.traverse_to_inactive_public_product)
+ NotFound, self.traverse_to_inactive_public_product
+ )
self.assertRaises(
- NotFound, self.traverse_to_active_proprietary_product)
+ NotFound, self.traverse_to_active_proprietary_product
+ )
self.assertRaises(
- NotFound, self.traverse_to_inactive_proprietary_product)
+ NotFound, self.traverse_to_inactive_proprietary_product
+ )
def test_access_for_person_with_pillar_grant(self):
# Persons with a policy grant for a proprietary product can
# access this product, if it is active.
user = self.factory.makePerson()
with person_logged_in(self.proprietary_product_owner):
- getUtility(IService, 'sharing').sharePillarInformation(
- self.active_proprietary_product, user,
+ getUtility(IService, "sharing").sharePillarInformation(
+ self.active_proprietary_product,
+ user,
self.proprietary_product_owner,
- {InformationType.PROPRIETARY: SharingPermission.ALL})
- getUtility(IService, 'sharing').sharePillarInformation(
- self.inactive_proprietary_product, user,
+ {InformationType.PROPRIETARY: SharingPermission.ALL},
+ )
+ getUtility(IService, "sharing").sharePillarInformation(
+ self.inactive_proprietary_product,
+ user,
self.proprietary_product_owner,
- {InformationType.PROPRIETARY: SharingPermission.ALL})
+ {InformationType.PROPRIETARY: SharingPermission.ALL},
+ )
with person_logged_in(user):
self.traverse_to_active_public_product()
self.assertRaises(
- NotFound, self.traverse_to_inactive_public_product)
+ NotFound, self.traverse_to_inactive_public_product
+ )
self.traverse_to_active_proprietary_product()
self.assertRaises(
- NotFound, self.traverse_to_inactive_proprietary_product)
+ NotFound, self.traverse_to_inactive_proprietary_product
+ )
def test_access_for_persons_with_artifact_grant(self):
# Persons with an artifact grant related to a private product
@@ -844,9 +864,11 @@ class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
with person_logged_in(self.proprietary_product_owner):
bug = self.factory.makeBug(
target=self.active_proprietary_product,
- information_type=InformationType.PROPRIETARY)
- getUtility(IService, 'sharing').ensureAccessGrants(
- [user], self.proprietary_product_owner, bugs=[bug])
+ information_type=InformationType.PROPRIETARY,
+ )
+ getUtility(IService, "sharing").ensureAccessGrants(
+ [user], self.proprietary_product_owner, bugs=[bug]
+ )
with person_logged_in(user):
self.traverse_to_active_proprietary_product()
@@ -859,8 +881,8 @@ class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
def test_access_for_persons_with_special_permissions(self):
# Admins have access all products, including inactive and propretary
# products.
- with celebrity_logged_in('admin'):
+ with celebrity_logged_in("admin"):
self.check_admin_access()
# Commercial admins have access to all products.
- with celebrity_logged_in('commercial_admin'):
+ with celebrity_logged_in("commercial_admin"):
self.check_admin_access()
diff --git a/lib/lp/app/browser/tests/test_launchpadform.py b/lib/lp/app/browser/tests/test_launchpadform.py
index cb08991..56f7f91 100644
--- a/lib/lp/app/browser/tests/test_launchpadform.py
+++ b/lib/lp/app/browser/tests/test_launchpadform.py
@@ -3,49 +3,32 @@
"""Tests for the lp.app.browser.launchpadform module."""
-from os.path import (
- dirname,
- join,
- )
+from os.path import dirname, join
-from lxml import html
import simplejson
+from lxml import html
from testtools.content import text_content
from zope.browserpage import ViewPageTemplateFile
from zope.formlib.form import action
from zope.interface import Interface
-from zope.schema import (
- Choice,
- Text,
- TextLine,
- )
+from zope.schema import Choice, Text, TextLine
from zope.schema.vocabulary import SimpleVocabulary
-from lp.app.browser.launchpadform import (
- has_structured_doc,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, has_structured_doc
from lp.services.config import config
from lp.services.webapp.servers import LaunchpadTestRequest
-from lp.testing import (
- test_tales,
- TestCase,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- FunctionalLayer,
- )
+from lp.testing import TestCase, TestCaseWithFactory, test_tales
+from lp.testing.layers import DatabaseFunctionalLayer, FunctionalLayer
class TestInterface(Interface):
"""Test interface for the view below."""
- normal = Text(title='normal', description='plain text')
+ normal = Text(title="normal", description="plain text")
structured = has_structured_doc(
- Text(title='structured',
- description='<strong>structured text</strong'))
+ Text(title="structured", description="<strong>structured text</strong")
+ )
class TestView(LaunchpadFormView):
@@ -59,7 +42,7 @@ class TestHasStructuredDoc(TestCase):
layer = FunctionalLayer
def _widget_annotation(self, widget):
- return widget.context.queryTaggedValue('has_structured_doc')
+ return widget.context.queryTaggedValue("has_structured_doc")
def test_has_structured_doc_sets_attribute(self):
# Test that has_structured_doc sets the field annotation.
@@ -81,18 +64,25 @@ class TestQueryTalesForHasStructuredDoc(TestCase):
view = TestView(None, request)
view.initialize()
normal_widget, structured_widget = view.widgets
- self.assertIs(None, test_tales(
- 'widget/query:has-structured-doc', widget=normal_widget))
- self.assertTrue(test_tales(
- 'widget/query:has-structured-doc', widget=structured_widget))
+ self.assertIs(
+ None,
+ test_tales(
+ "widget/query:has-structured-doc", widget=normal_widget
+ ),
+ )
+ self.assertTrue(
+ test_tales(
+ "widget/query:has-structured-doc", widget=structured_widget
+ )
+ )
class TestHelpLinksInterface(Interface):
"""Test interface for the view below."""
- nickname = Text(title='nickname')
+ nickname = Text(title="nickname")
- displayname = Text(title='displayname')
+ displayname = Text(title="displayname")
class TestHelpLinksView(LaunchpadFormView):
@@ -102,12 +92,13 @@ class TestHelpLinksView(LaunchpadFormView):
page_title = "TestHelpLinksView"
template = ViewPageTemplateFile(
- config.root + '/lib/lp/app/templates/generic-edit.pt')
+ config.root + "/lib/lp/app/templates/generic-edit.pt"
+ )
help_links = {
"nickname": "http://widget.example.com/name",
"displayname": "http://widget.example.com/displayname",
- }
+ }
class TestHelpLinks(TestCaseWithFactory):
@@ -122,11 +113,12 @@ class TestHelpLinks(TestCaseWithFactory):
view.initialize()
nickname_widget, displayname_widget = view.widgets
self.assertEqual(
- "http://widget.example.com/name",
- nickname_widget.help_link)
+ "http://widget.example.com/name", nickname_widget.help_link
+ )
self.assertEqual(
"http://widget.example.com/displayname",
- displayname_widget.help_link)
+ displayname_widget.help_link,
+ )
def test_help_links_render(self):
# The values in a view's help_links dictionary are rendered in the
@@ -138,25 +130,28 @@ class TestHelpLinks(TestCaseWithFactory):
view.initialize()
root = html.fromstring(view.render())
[nickname_help_link] = root.cssselect(
- "label[for$=nickname] ~ a[target=help]")
+ "label[for$=nickname] ~ a[target=help]"
+ )
self.assertEqual(
- "http://widget.example.com/name",
- nickname_help_link.get("href"))
+ "http://widget.example.com/name", nickname_help_link.get("href")
+ )
[displayname_help_link] = root.cssselect(
- "label[for$=displayname] ~ a[target=help]")
+ "label[for$=displayname] ~ a[target=help]"
+ )
self.assertEqual(
"http://widget.example.com/displayname",
- displayname_help_link.get("href"))
+ displayname_help_link.get("href"),
+ )
class TestWidgetDivInterface(Interface):
"""Test interface for the view below."""
- single_line = TextLine(title='single_line')
- multi_line = Text(title='multi_line')
+ single_line = TextLine(title="single_line")
+ multi_line = Text(title="multi_line")
checkbox = Choice(
- vocabulary=SimpleVocabulary.fromItems(
- (('yes', True), ('no', False))))
+ vocabulary=SimpleVocabulary.fromItems((("yes", True), ("no", False)))
+ )
class TestWidgetDivView(LaunchpadFormView):
@@ -164,7 +159,8 @@ class TestWidgetDivView(LaunchpadFormView):
schema = TestWidgetDivInterface
template = ViewPageTemplateFile(
- join(dirname(__file__), "test-widget-div.pt"))
+ join(dirname(__file__), "test-widget-div.pt")
+ )
class TestWidgetDiv(TestCase):
@@ -181,7 +177,8 @@ class TestWidgetDiv(TestCase):
# All the widgets appear in the page.
self.assertEqual(
["field.single_line", "field.multi_line", "field.checkbox"],
- root.xpath("//@id"))
+ root.xpath("//@id"),
+ )
def test_all_widgets_present_but_hidden(self):
request = LaunchpadTestRequest()
@@ -194,28 +191,36 @@ class TestWidgetDiv(TestCase):
root = html.fromstring(content)
# All the widgets appear in the page as hidden inputs.
self.assertEqual(
- ["field.single_line", "hidden",
- "field.multi_line", "hidden",
- "field.checkbox", "hidden"],
- root.xpath("//input/@id | //input/@type"))
+ [
+ "field.single_line",
+ "hidden",
+ "field.multi_line",
+ "hidden",
+ "field.checkbox",
+ "hidden",
+ ],
+ root.xpath("//input/@id | //input/@type"),
+ )
class TestFormView(TestWidgetDivView):
"""A trivial view with an action and a validator which sets errors."""
- @action('Test', name='test',
- failure=LaunchpadFormView.ajax_failure_handler)
+
+ @action(
+ "Test", name="test", failure=LaunchpadFormView.ajax_failure_handler
+ )
def test_action(self, action, data):
- single_line_value = data['single_line']
- if single_line_value == 'success':
+ single_line_value = data["single_line"]
+ if single_line_value == "success":
return
self.addError("An action error")
def validate(self, data):
- single_line_value = data['single_line']
- if single_line_value != 'error':
+ single_line_value = data["single_line"]
+ if single_line_value != "error":
return
- self.setFieldError('single_line', 'An error occurred')
- self.addError('A form error')
+ self.setFieldError("single_line", "An error occurred")
+ self.addError("A form error")
class TestAjaxValidator(TestCase):
@@ -227,58 +232,66 @@ class TestAjaxValidator(TestCase):
def test_ajax_failure_handler(self):
# Validation errors are recorded properly.
- extra = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+ extra = {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}
request = LaunchpadTestRequest(
- method='POST',
- form={
- 'field.actions.test': 'Test',
- 'field.single_line': 'error'},
- **extra)
+ method="POST",
+ form={"field.actions.test": "Test", "field.single_line": "error"},
+ **extra,
+ )
view = TestFormView({}, request)
view.initialize()
self.assertEqual(
- {"error_summary": "There are 2 errors.",
- "errors": {"field.single_line": "An error occurred"},
- "form_wide_errors": ["A form error"]},
- simplejson.loads(view.form_result))
+ {
+ "error_summary": "There are 2 errors.",
+ "errors": {"field.single_line": "An error occurred"},
+ "form_wide_errors": ["A form error"],
+ },
+ simplejson.loads(view.form_result),
+ )
def test_non_ajax_failure_handler(self):
# The ajax error handler is not run if the request is not ajax.
request = LaunchpadTestRequest(
- method='POST',
- form={
- 'field.actions.test': 'Test',
- 'field.single_line': 'error'})
+ method="POST",
+ form={"field.actions.test": "Test", "field.single_line": "error"},
+ )
view = TestFormView({}, request)
view.initialize()
self.assertIsNone(view.form_result)
def test_ajax_action_success(self):
# When there are no errors, form_result is None.
- extra = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+ extra = {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}
request = LaunchpadTestRequest(
- method='POST',
+ method="POST",
form={
- 'field.actions.test': 'Test',
- 'field.single_line': 'success'},
- **extra)
+ "field.actions.test": "Test",
+ "field.single_line": "success",
+ },
+ **extra,
+ )
view = TestFormView({}, request)
view.initialize()
self.assertIsNone(view.form_result)
def test_ajax_action_failure(self):
# When there are errors performing the action, these are recorded.
- extra = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+ extra = {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}
request = LaunchpadTestRequest(
- method='POST',
+ method="POST",
form={
- 'field.actions.test': 'Test',
- 'field.single_line': 'failure'},
- **extra)
+ "field.actions.test": "Test",
+ "field.single_line": "failure",
+ },
+ **extra,
+ )
view = TestFormView({}, request)
view.initialize()
self.assertEqual(
- {"error_summary": "There is 1 error.",
- "errors": {},
- "form_wide_errors": ["An action error"]},
- simplejson.loads(view.form_result))
+ {
+ "error_summary": "There is 1 error.",
+ "errors": {},
+ "form_wide_errors": ["An action error"],
+ },
+ simplejson.loads(view.form_result),
+ )
diff --git a/lib/lp/app/browser/tests/test_launchpadform_doc.py b/lib/lp/app/browser/tests/test_launchpadform_doc.py
index caafbcb..a628ee2 100644
--- a/lib/lp/app/browser/tests/test_launchpadform_doc.py
+++ b/lib/lp/app/browser/tests/test_launchpadform_doc.py
@@ -4,21 +4,15 @@
import doctest
import unittest
-from zope.formlib.interfaces import (
- IDisplayWidget,
- IInputWidget,
- )
-from zope.interface import (
- directlyProvides,
- implementer,
- )
+from zope.formlib.interfaces import IDisplayWidget, IInputWidget
+from zope.interface import directlyProvides, implementer
from lp.app.browser.launchpadform import LaunchpadFormView
from lp.services.webapp.interfaces import (
ICheckBoxWidgetLayout,
IMultiLineWidgetLayout,
ISingleLineWidgetLayout,
- )
+)
from lp.testing.layers import FunctionalLayer
@@ -38,7 +32,7 @@ class LaunchpadFormTest(unittest.TestCase):
pass
widget = FakeWidget()
- form.widgets = {'widget': widget}
+ form.widgets = {"widget": widget}
# test every combination of the three interfaces:
for use_single_line in [False, True]:
for use_multi_line in [False, True]:
@@ -54,16 +48,19 @@ class LaunchpadFormTest(unittest.TestCase):
# Now count how many of the is* functions return True:
count = 0
- if form.isSingleLineLayout('widget'):
+ if form.isSingleLineLayout("widget"):
count += 1
- if form.isMultiLineLayout('widget'):
+ if form.isMultiLineLayout("widget"):
count += 1
- if form.isCheckBoxLayout('widget'):
+ if form.isCheckBoxLayout("widget"):
count += 1
- self.assertEqual(count, 1,
- 'Expected count of 1 for %r. Got %d'
- % (provides, count))
+ self.assertEqual(
+ count,
+ 1,
+ "Expected count of 1 for %r. Got %d"
+ % (provides, count),
+ )
def test_showOptionalMarker(self):
"""Verify a field marked .for_display has no (Optional) marker."""
@@ -75,11 +72,11 @@ class LaunchpadFormTest(unittest.TestCase):
def __init__(self, required):
self.required = required
- form.widgets = {'widget': FakeInputWidget(required=False)}
- self.assertTrue(form.showOptionalMarker('widget'))
+ form.widgets = {"widget": FakeInputWidget(required=False)}
+ self.assertTrue(form.showOptionalMarker("widget"))
# Required IInputWidgets have no (Optional) marker.
- form.widgets = {'widget': FakeInputWidget(required=True)}
- self.assertFalse(form.showOptionalMarker('widget'))
+ form.widgets = {"widget": FakeInputWidget(required=True)}
+ self.assertFalse(form.showOptionalMarker("widget"))
# IDisplayWidgets have no (Optional) marker, regardless of whether
# they are required or not, since they are read only.
@@ -88,10 +85,10 @@ class LaunchpadFormTest(unittest.TestCase):
def __init__(self, required):
self.required = required
- form.widgets = {'widget': FakeDisplayWidget(required=False)}
- self.assertFalse(form.showOptionalMarker('widget'))
- form.widgets = {'widget': FakeDisplayWidget(required=True)}
- self.assertFalse(form.showOptionalMarker('widget'))
+ form.widgets = {"widget": FakeDisplayWidget(required=False)}
+ self.assertFalse(form.showOptionalMarker("widget"))
+ form.widgets = {"widget": FakeDisplayWidget(required=True)}
+ self.assertFalse(form.showOptionalMarker("widget"))
def doctest_custom_widget_with_setUpFields_override():
@@ -139,7 +136,9 @@ def doctest_custom_widget_with_setUpFields_override():
def test_suite():
- return unittest.TestSuite((
- unittest.TestLoader().loadTestsFromName(__name__),
- doctest.DocTestSuite()
- ))
+ return unittest.TestSuite(
+ (
+ unittest.TestLoader().loadTestsFromName(__name__),
+ doctest.DocTestSuite(),
+ )
+ )
diff --git a/lib/lp/app/browser/tests/test_launchpadroot.py b/lib/lp/app/browser/tests/test_launchpadroot.py
index 5c3b1d7..c5e5329 100644
--- a/lib/lp/app/browser/tests/test_launchpadroot.py
+++ b/lib/lp/app/browser/tests/test_launchpadroot.py
@@ -10,43 +10,34 @@ from zope.security.checker import selectChecker
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.pillar import IPillarNameSet
-from lp.services.beautifulsoup import (
- BeautifulSoup,
- SoupStrainer,
- )
+from lp.services.beautifulsoup import BeautifulSoup, SoupStrainer
from lp.services.config import config
from lp.services.features.testing import FeatureFixture
from lp.services.memcache.interfaces import IMemcacheClient
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.interfaces import ILaunchpadRoot
from lp.testing import (
+ TestCaseWithFactory,
anonymous_logged_in,
login_admin,
login_person,
record_two_runs,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
from lp.testing.matchers import HasQueryCount
from lp.testing.publication import test_traverse
-from lp.testing.views import (
- create_initialized_view,
- create_view,
- )
+from lp.testing.views import create_initialized_view, create_view
class LaunchpadRootPermissionTest(TestCaseWithFactory):
"""Test for the ILaunchpadRoot permission"""
+
layer = DatabaseFunctionalLayer
def setUp(self):
TestCaseWithFactory.setUp(self)
self.root = getUtility(ILaunchpadRoot)
- self.admin = getUtility(IPersonSet).getByEmail(
- 'foo.bar@xxxxxxxxxxxxx')
+ self.admin = getUtility(IPersonSet).getByEmail("foo.bar@xxxxxxxxxxxxx")
# Use a FakeLogger fixture to prevent Memcached warnings to be
# printed to stdout while browsing pages.
self.useFixture(FakeLogger())
@@ -56,44 +47,55 @@ class LaunchpadRootPermissionTest(TestCaseWithFactory):
login_person(self.admin)
self.expert = self.factory.makePerson()
getUtility(ILaunchpadCelebrities).registry_experts.addMember(
- self.expert, self.admin)
+ self.expert, self.admin
+ )
login_person(self.expert)
def test_anonymous_cannot_edit(self):
- self.assertFalse(check_permission('launchpad.Edit', self.root),
- "Anonymous user shouldn't have launchpad.Edit on ILaunchpadRoot")
+ self.assertFalse(
+ check_permission("launchpad.Edit", self.root),
+ "Anonymous user shouldn't have launchpad.Edit on ILaunchpadRoot",
+ )
def test_regular_user_cannot_edit(self):
login_person(self.factory.makePerson())
- self.assertFalse(check_permission('launchpad.Edit', self.root),
- "Regular users shouldn't have launchpad.Edit on ILaunchpadRoot")
+ self.assertFalse(
+ check_permission("launchpad.Edit", self.root),
+ "Regular users shouldn't have launchpad.Edit on ILaunchpadRoot",
+ )
def test_registry_expert_can_edit(self):
self.setUpRegistryExpert()
- self.assertTrue(check_permission('launchpad.Edit', self.root),
- "Registry experts should have launchpad.Edit on ILaunchpadRoot")
+ self.assertTrue(
+ check_permission("launchpad.Edit", self.root),
+ "Registry experts should have launchpad.Edit on ILaunchpadRoot",
+ )
def test_admins_can_edit(self):
login_person(self.admin)
- self.assertTrue(check_permission('launchpad.Edit', self.root),
- "Admins should have launchpad.Edit on ILaunchpadRoot")
+ self.assertTrue(
+ check_permission("launchpad.Edit", self.root),
+ "Admins should have launchpad.Edit on ILaunchpadRoot",
+ )
def test_featured_projects_view_requires_edit(self):
- view = create_view(self.root, '+featuredprojects')
+ view = create_view(self.root, "+featuredprojects")
checker = selectChecker(view)
- self.assertEqual('launchpad.Edit', checker.permission_id('__call__'))
+ self.assertEqual("launchpad.Edit", checker.permission_id("__call__"))
def test_featured_projects_manage_link_requires_edit(self):
self.setUpRegistryExpert()
view = create_initialized_view(
- self.root, 'index.html', principal=self.expert)
+ self.root, "index.html", principal=self.expert
+ )
# Stub out the getRecentBlogPosts which fetches a blog feed using
# urlfetch.
view.getRecentBlogPosts = lambda: []
- content = BeautifulSoup(view(), parse_only=SoupStrainer('a'))
+ content = BeautifulSoup(view(), parse_only=SoupStrainer("a"))
self.assertTrue(
- content.find('a', href='+featuredprojects'),
- "Cannot find the +featuredprojects link on the first page")
+ content.find("a", href="+featuredprojects"),
+ "Cannot find the +featuredprojects link on the first page",
+ )
class TestLaunchpadRootNavigation(TestCaseWithFactory):
@@ -103,23 +105,25 @@ class TestLaunchpadRootNavigation(TestCaseWithFactory):
def test_support(self):
# The /support link redirects to answers.
- context, view, request = test_traverse(
- 'http://launchpad.test/support')
+ context, view, request = test_traverse("http://launchpad.test/support")
view()
self.assertEqual(301, request.response.getStatus())
self.assertEqual(
- 'http://answers.launchpad.test/launchpad',
- request.response.getHeader('location'))
+ "http://answers.launchpad.test/launchpad",
+ request.response.getHeader("location"),
+ )
def test_feedback(self):
# The /feedback link redirects to the help site.
context, view, request = test_traverse(
- 'http://launchpad.test/feedback')
+ "http://launchpad.test/feedback"
+ )
view()
self.assertEqual(301, request.response.getStatus())
self.assertEqual(
- 'https://help.launchpad.net/Feedback',
- request.response.getHeader('location'))
+ "https://help.launchpad.net/Feedback",
+ request.response.getHeader("location"),
+ )
class LaunchpadRootIndexViewTestCase(TestCaseWithFactory):
@@ -136,94 +140,95 @@ class LaunchpadRootIndexViewTestCase(TestCaseWithFactory):
root = getUtility(ILaunchpadRoot)
user = self.factory.makePerson()
login_person(user)
- view = create_initialized_view(root, 'index.html', principal=user)
+ view = create_initialized_view(root, "index.html", principal=user)
# Replace the blog posts so the view does not make a network request.
view.getRecentBlogPosts = lambda: []
- markup = BeautifulSoup(view(), parse_only=SoupStrainer(id='document'))
+ markup = BeautifulSoup(view(), parse_only=SoupStrainer(id="document"))
self.assertIs(False, view.has_watermark)
- self.assertIs(None, markup.find(True, id='watermark'))
- logo = markup.find(True, id='launchpad-logo-and-name')
+ self.assertIs(None, markup.find(True, id="watermark"))
+ logo = markup.find(True, id="launchpad-logo-and-name")
self.assertIsNot(None, logo)
- self.assertEqual('/@@/launchpad-logo-and-name.png', logo['src'])
+ self.assertEqual("/@@/launchpad-logo-and-name.png", logo["src"])
@staticmethod
def _make_blog_post(linkid, title, body, date):
return {
- 'title': title,
- 'description': body,
- 'link': "http://blog.invalid/%s" % (linkid,),
- 'date': date,
- }
+ "title": title,
+ "description": body,
+ "link": "http://blog.invalid/%s" % (linkid,),
+ "date": date,
+ }
def test_blog_posts(self):
"""Posts from the launchpad blog are shown when feature is enabled"""
- self.useFixture(FeatureFixture({'app.root_blog.enabled': True}))
+ self.useFixture(FeatureFixture({"app.root_blog.enabled": True}))
posts = [
self._make_blog_post(1, "A post", "Post contents.", "2002"),
self._make_blog_post(2, "Another post", "More contents.", "2003"),
- ]
+ ]
calls = []
def _get_blog_posts():
- calls.append('called')
+ calls.append("called")
return posts
root = getUtility(ILaunchpadRoot)
with anonymous_logged_in():
- view = create_initialized_view(root, 'index.html')
+ view = create_initialized_view(root, "index.html")
view.getRecentBlogPosts = _get_blog_posts
result = view()
markup = BeautifulSoup(
- result, parse_only=SoupStrainer(id='homepage-blogposts'))
- self.assertEqual(['called'], calls)
- items = markup.find_all('li', 'news')
+ result, parse_only=SoupStrainer(id="homepage-blogposts")
+ )
+ self.assertEqual(["called"], calls)
+ items = markup.find_all("li", "news")
# Notice about launchpad being opened is always added at the end
self.assertEqual(3, len(items))
a = items[-1].find("a")
self.assertEqual("Launchpad now open source", a.string.strip())
for post, item in zip(posts, items):
a = item.find("a")
- self.assertEqual(post['link'], a["href"])
- self.assertEqual(post['title'], a.string)
+ self.assertEqual(post["link"], a["href"])
+ self.assertEqual(post["title"], a.string)
def test_blog_disabled(self):
"""Launchpad blog not queried for display without feature"""
calls = []
def _get_blog_posts():
- calls.append('called')
+ calls.append("called")
return []
root = getUtility(ILaunchpadRoot)
user = self.factory.makePerson()
login_person(user)
- view = create_initialized_view(root, 'index.html', principal=user)
+ view = create_initialized_view(root, "index.html", principal=user)
view.getRecentBlogPosts = _get_blog_posts
- markup = BeautifulSoup(
- view(), parse_only=SoupStrainer(id='homepage'))
+ markup = BeautifulSoup(view(), parse_only=SoupStrainer(id="homepage"))
self.assertEqual([], calls)
- self.assertIs(None, markup.find(True, id='homepage-blogposts'))
+ self.assertIs(None, markup.find(True, id="homepage-blogposts"))
# Even logged in users should get the launchpad intro text in the left
# column rather than blank space when the blog is not being displayed.
self.assertTrue(view.show_whatslaunchpad)
- self.assertTrue(markup.find(True, 'homepage-whatslaunchpad'))
+ self.assertTrue(markup.find(True, "homepage-whatslaunchpad"))
def test_blog_posts_with_memcache(self):
- self.useFixture(FeatureFixture({'app.root_blog.enabled': True}))
+ self.useFixture(FeatureFixture({"app.root_blog.enabled": True}))
posts = [
self._make_blog_post(1, "A post", "Post contents.", "2002"),
self._make_blog_post(2, "Another post", "More contents.", "2003"),
- ]
- key = '%s:homepage-blog-posts' % config.instance_name
+ ]
+ key = "%s:homepage-blog-posts" % config.instance_name
getUtility(IMemcacheClient).set(key, posts)
root = getUtility(ILaunchpadRoot)
with anonymous_logged_in():
- view = create_initialized_view(root, 'index.html')
+ view = create_initialized_view(root, "index.html")
result = view()
markup = BeautifulSoup(
- result, parse_only=SoupStrainer(id='homepage-blogposts'))
- items = markup.find_all('li', 'news')
+ result, parse_only=SoupStrainer(id="homepage-blogposts")
+ )
+ items = markup.find_all("li", "news")
self.assertEqual(3, len(items))
def test_featured_projects_query_count(self):
@@ -239,6 +244,10 @@ class LaunchpadRootIndexViewTestCase(TestCaseWithFactory):
user = self.factory.makePerson()
recorder1, recorder2 = record_two_runs(
lambda: create_initialized_view(
- root, 'index.html', principal=user)(),
- add_featured_projects, 5, login_method=login_admin)
+ root, "index.html", principal=user
+ )(),
+ add_featured_projects,
+ 5,
+ login_method=login_admin,
+ )
self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
diff --git a/lib/lp/app/browser/tests/test_linkchecker.py b/lib/lp/app/browser/tests/test_linkchecker.py
index 2781e2a..9393a2e 100644
--- a/lib/lp/app/browser/tests/test_linkchecker.py
+++ b/lib/lp/app/browser/tests/test_linkchecker.py
@@ -19,17 +19,17 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
- BRANCH_URL_TEMPLATE = '/+code/%s'
+ BRANCH_URL_TEMPLATE = "/+code/%s"
def check_invalid_links(self, result_json, link_type, invalid_links):
link_dict = simplejson.loads(result_json)
- links_to_check = link_dict[link_type]['invalid']
+ links_to_check = link_dict[link_type]["invalid"]
self.assertEqual(len(invalid_links), len(links_to_check))
self.assertEqual(set(invalid_links), set(links_to_check))
def check_valid_links(self, result_json, link_type, valid_links):
link_dict = simplejson.loads(result_json)
- links_to_check = link_dict[link_type]['valid']
+ links_to_check = link_dict[link_type]["valid"]
self.assertEqual(len(valid_links), len(links_to_check))
self.assertEqual(set(valid_links), set(links_to_check))
@@ -43,13 +43,16 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
# git branches are a thing now as well!
project_git_repo = removeSecurityProxy(
- self.factory.makeGitRepository(target=product))
+ self.factory.makeGitRepository(target=product)
+ )
dsp = self.factory.makeDistributionSourcePackage()
dsp_git_repo = removeSecurityProxy(
- self.factory.makeGitRepository(target=dsp))
+ self.factory.makeGitRepository(target=dsp)
+ )
person = self.factory.makePerson()
person_git_repo = removeSecurityProxy(
- self.factory.makeGitRepository(target=person))
+ self.factory.makeGitRepository(target=person)
+ )
return [
valid_branch_url,
@@ -61,18 +64,15 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
def make_invalid_branch_links(self):
return [
- self.BRANCH_URL_TEMPLATE % 'foo',
- self.BRANCH_URL_TEMPLATE % 'bar',
- ]
+ self.BRANCH_URL_TEMPLATE % "foo",
+ self.BRANCH_URL_TEMPLATE % "bar",
+ ]
def make_valid_bug_links(self):
bug1 = self.factory.makeBug()
bug2 = self.factory.makeBug()
self.factory.makeBugTask(bug=bug2)
- return [
- '/bugs/%d' % (bug1.id),
- '/bugs/%d' % (bug2.id)
- ]
+ return ["/bugs/%d" % (bug1.id), "/bugs/%d" % (bug2.id)]
def make_invalid_bug_links(self):
"""
@@ -80,12 +80,17 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
currently authenticated user
"""
bug_private = self.factory.makeBug(
- information_type=InformationType.USERDATA)
- return ['/bugs/%d' % (bug_private.id)]
+ information_type=InformationType.USERDATA
+ )
+ return ["/bugs/%d" % (bug_private.id)]
def invoke_link_checker(
- self, valid_branch_urls=None, invalid_branch_urls=None,
- valid_bug_urls=None, invalid_bug_urls=None):
+ self,
+ valid_branch_urls=None,
+ invalid_branch_urls=None,
+ valid_bug_urls=None,
+ invalid_bug_urls=None,
+ ):
if valid_branch_urls is None:
valid_branch_urls = {}
if invalid_branch_urls is None:
@@ -110,11 +115,10 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
link_checker = LinkCheckerAPI(object(), request)
result_json = link_checker()
self.check_invalid_links(
- result_json, 'branch_links', invalid_branch_urls)
- self.check_invalid_links(
- result_json, 'bug_links', invalid_bug_urls)
- self.check_valid_links(
- result_json, 'bug_links', valid_bug_urls)
+ result_json, "branch_links", invalid_branch_urls
+ )
+ self.check_invalid_links(result_json, "bug_links", invalid_bug_urls)
+ self.check_valid_links(result_json, "bug_links", valid_bug_urls)
def test_with_no_data(self):
request = LaunchpadTestRequest()
@@ -127,13 +131,15 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
branch_urls = self.make_valid_branch_links()
bug_urls = self.make_valid_bug_links()
self.invoke_link_checker(
- valid_branch_urls=branch_urls, valid_bug_urls=bug_urls)
+ valid_branch_urls=branch_urls, valid_bug_urls=bug_urls
+ )
def test_only_invalid_links(self):
branch_urls = self.make_invalid_branch_links()
bug_urls = self.make_invalid_bug_links()
self.invoke_link_checker(
- invalid_branch_urls=branch_urls, invalid_bug_urls=bug_urls)
+ invalid_branch_urls=branch_urls, invalid_bug_urls=bug_urls
+ )
def test_valid_and_invald_links(self):
valid_branch_urls = self.make_valid_branch_links()
@@ -144,4 +150,5 @@ class TestLinkCheckerAPI(TestCaseWithFactory):
valid_branch_urls=valid_branch_urls,
invalid_branch_urls=invalid_branch_urls,
valid_bug_urls=valid_bug_urls,
- invalid_bug_urls=invalid_bug_urls)
+ invalid_bug_urls=invalid_bug_urls,
+ )
diff --git a/lib/lp/app/browser/tests/test_macro_view.py b/lib/lp/app/browser/tests/test_macro_view.py
index 0d26beb..dc97cbe 100644
--- a/lib/lp/app/browser/tests/test_macro_view.py
+++ b/lib/lp/app/browser/tests/test_macro_view.py
@@ -18,32 +18,32 @@ class TestMacroNontraversability(TestCaseWithFactory):
# Names of some macros that are tested to ensure that they're not
# accessable via URL. This is not an exhaustive list.
macro_names = (
- 'feed-entry-atom',
- '+base-layout-macros',
- '+main-template-macros',
- 'launchpad_form',
- 'launchpad_widget_macros',
- '+forbidden-page-macros',
- '+search-form',
+ "feed-entry-atom",
+ "+base-layout-macros",
+ "+main-template-macros",
+ "launchpad_form",
+ "launchpad_widget_macros",
+ "+forbidden-page-macros",
+ "+search-form",
'+primary-search-form"',
- 'form-picker-macros',
- '+filebug-macros',
- '+bugtarget-macros-search',
- 'bugcomment-macros',
- 'bug-attachment-macros',
- '+portlet-malone-bugmail-filtering-faq',
- '+bugtask-macros-tableview',
- 'bugtask-macros-cve',
- '+bmp-macros',
- '+announcement-macros',
- '+person-macros',
- '+milestone-macros',
- '+distributionmirror-macros',
- '+timeline-macros',
- '+macros',
- '+translations-macros',
- '+object-reassignment',
- '+team-bugs-macro',
+ "form-picker-macros",
+ "+filebug-macros",
+ "+bugtarget-macros-search",
+ "bugcomment-macros",
+ "bug-attachment-macros",
+ "+portlet-malone-bugmail-filtering-faq",
+ "+bugtask-macros-tableview",
+ "bugtask-macros-cve",
+ "+bmp-macros",
+ "+announcement-macros",
+ "+person-macros",
+ "+milestone-macros",
+ "+distributionmirror-macros",
+ "+timeline-macros",
+ "+macros",
+ "+translations-macros",
+ "+object-reassignment",
+ "+team-bugs-macro",
)
@staticmethod
@@ -51,6 +51,7 @@ class TestMacroNontraversability(TestCaseWithFactory):
def traverse_and_call():
view = test_traverse(path)[1]
view()
+
try:
traverse_and_call()
except NotFound:
@@ -60,5 +61,7 @@ class TestMacroNontraversability(TestCaseWithFactory):
def test_macro_names_not_traversable(self):
for name in self.macro_names:
- self.assertTrue(self.is_not_found('http://launchpad.test/' + name),
- 'macro name %r should not be URL accessable' % name)
+ self.assertTrue(
+ self.is_not_found("http://launchpad.test/" + name),
+ "macro name %r should not be URL accessable" % name,
+ )
diff --git a/lib/lp/app/browser/tests/test_mixed_visibility.py b/lib/lp/app/browser/tests/test_mixed_visibility.py
index ee7bcce..b0661f5 100644
--- a/lib/lp/app/browser/tests/test_mixed_visibility.py
+++ b/lib/lp/app/browser/tests/test_mixed_visibility.py
@@ -3,10 +3,7 @@
from lp.app.browser.tales import TeamFormatterAPI
from lp.registry.interfaces.person import PersonVisibility
-from lp.testing import (
- person_logged_in,
- TestCaseWithFactory,
- )
+from lp.testing import TestCaseWithFactory, person_logged_in
from lp.testing.layers import DatabaseFunctionalLayer
@@ -21,7 +18,7 @@ class TestMixedVisibility(TestCaseWithFactory):
viewer = self.factory.makePerson()
with person_logged_in(viewer):
self.assertEqual(
- '<hidden>', TeamFormatterAPI(team).displayname(None))
+ "<hidden>", TeamFormatterAPI(team).displayname(None)
+ )
self.assertEqual(1, len(self.oopses))
- self.assertTrue(
- 'MixedVisibilityError' in self.oopses[0]['tb_text'])
+ self.assertTrue("MixedVisibilityError" in self.oopses[0]["tb_text"])
diff --git a/lib/lp/app/browser/tests/test_page_macro.py b/lib/lp/app/browser/tests/test_page_macro.py
index fb8f1fd..599bb17 100644
--- a/lib/lp/app/browser/tests/test_page_macro.py
+++ b/lib/lp/app/browser/tests/test_page_macro.py
@@ -13,15 +13,12 @@ from lp.app.interfaces.launchpad import IPrivacy
from lp.app.security import AuthorizationBase
from lp.testing import (
FakeAdapterMixin,
- login_person,
- test_tales,
TestCase,
TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- FunctionalLayer,
- )
+ login_person,
+ test_tales,
+)
+from lp.testing.layers import DatabaseFunctionalLayer, FunctionalLayer
from lp.testing.views import create_view
@@ -31,23 +28,20 @@ class ITest(IPrivacy):
@implementer(ITest)
class TestObject:
-
def __init__(self):
self.private = False
class TestView:
-
def __init__(self, context, request):
self.context = context
self.request = request
class TestPageMacroDispatcherMixin(FakeAdapterMixin):
-
def _setUpView(self):
- self.registerBrowserViewAdapter(TestView, ITest, '+index')
- self.view = create_view(TestObject(), name='+index')
+ self.registerBrowserViewAdapter(TestView, ITest, "+index")
+ self.view = create_view(TestObject(), name="+index")
def _call_test_tales(self, path):
test_tales(path, view=self.view)
@@ -60,6 +54,7 @@ class PageMacroDispatcherTestCase(TestPageMacroDispatcherMixin, TestCase):
Templates should start by specifying the kind of pagetype they use.
<html metal:use-macro="view/macro:page/main_side" />
"""
+
layer = FunctionalLayer
def setUp(self):
@@ -68,62 +63,75 @@ class PageMacroDispatcherTestCase(TestPageMacroDispatcherMixin, TestCase):
def test_base_template(self):
# Requests on the launchpad.test vhost use the Launchpad base template.
- adapter = self.getAdapter([self.view], IPathAdapter, name='macro')
+ adapter = self.getAdapter([self.view], IPathAdapter, name="macro")
template_path = os.path.normpath(adapter.base.filename)
- self.assertIn('lp/app/templates', template_path)
+ self.assertIn("lp/app/templates", template_path)
# The base template defines a 'master' macro as the adapter expects.
- self.assertIn('master', adapter.base.macros.keys())
+ self.assertIn("master", adapter.base.macros.keys())
def test_page(self):
# A view can be adpated to a page macro object.
- page_macro = test_tales('view/macro:page/main_side', view=self.view)
- self.assertEqual('main_side', self.view.__pagetype__)
- self.assertEqual(('mode', 'html'), page_macro[1])
+ page_macro = test_tales("view/macro:page/main_side", view=self.view)
+ self.assertEqual("main_side", self.view.__pagetype__)
+ self.assertEqual(("mode", "html"), page_macro[1])
source_file = page_macro[3]
- self.assertEqual('setSourceFile', source_file[0])
+ self.assertEqual("setSourceFile", source_file[0])
self.assertEqual(
- '/templates/base-layout.pt', source_file[1].split('..')[1])
+ "/templates/base-layout.pt", source_file[1].split("..")[1]
+ )
def test_page_unknown_type(self):
# An error is raised of the pagetype is not defined.
self.assertRaisesWithContent(
- LocationError, "'unknown pagetype: not-defined'",
- self._call_test_tales, 'view/macro:page/not-defined')
+ LocationError,
+ "'unknown pagetype: not-defined'",
+ self._call_test_tales,
+ "view/macro:page/not-defined",
+ )
def test_pagetype(self):
# The pagetype is 'unset', until macro:page is called.
- self.assertIs(None, getattr(self.view, '__pagetype__', None))
+ self.assertIs(None, getattr(self.view, "__pagetype__", None))
self.assertEqual(
- 'unset', test_tales('view/macro:pagetype', view=self.view))
- test_tales('view/macro:page/main_side', view=self.view)
- self.assertEqual('main_side', self.view.__pagetype__)
+ "unset", test_tales("view/macro:pagetype", view=self.view)
+ )
+ test_tales("view/macro:page/main_side", view=self.view)
+ self.assertEqual("main_side", self.view.__pagetype__)
self.assertEqual(
- 'main_side', test_tales('view/macro:pagetype', view=self.view))
+ "main_side", test_tales("view/macro:pagetype", view=self.view)
+ )
def test_pagehas(self):
# After the page type is set, the page macro can be queried
# for what LayoutElements it supports supports.
- test_tales('view/macro:page/main_side', view=self.view)
+ test_tales("view/macro:page/main_side", view=self.view)
self.assertTrue(
- test_tales('view/macro:pagehas/portlets', view=self.view))
+ test_tales("view/macro:pagehas/portlets", view=self.view)
+ )
def test_pagehas_unset_pagetype(self):
# The page macro type must be set before the page macro can be
# queried for what LayoutElements it supports.
self.assertRaisesWithContent(
- KeyError, "'unset'",
- self._call_test_tales, 'view/macro:pagehas/fnord')
+ KeyError,
+ "'unset'",
+ self._call_test_tales,
+ "view/macro:pagehas/fnord",
+ )
def test_pagehas_unknown_attribute(self):
# An error is raised if the LayoutElement does not exist.
- test_tales('view/macro:page/main_side', view=self.view)
+ test_tales("view/macro:page/main_side", view=self.view)
self.assertRaisesWithContent(
- KeyError, "'fnord'",
- self._call_test_tales, 'view/macro:pagehas/fnord')
+ KeyError,
+ "'fnord'",
+ self._call_test_tales,
+ "view/macro:pagehas/fnord",
+ )
def test_has_watermark_default(self):
# All pages have a watermark if the view does not provide the attr.
- has_watermark = test_tales('view/macro:has-watermark', view=self.view)
+ has_watermark = test_tales("view/macro:has-watermark", view=self.view)
self.assertIs(True, has_watermark)
def test_has_watermark_false(self):
@@ -131,9 +139,9 @@ class PageMacroDispatcherTestCase(TestPageMacroDispatcherMixin, TestCase):
class NoWatermarkView(TestView):
has_watermark = False
- self.registerBrowserViewAdapter(NoWatermarkView, ITest, '+test')
- view = create_view(TestObject(), name='+test')
- has_watermark = test_tales('view/macro:has-watermark', view=view)
+ self.registerBrowserViewAdapter(NoWatermarkView, ITest, "+test")
+ view = create_view(TestObject(), name="+test")
+ has_watermark = test_tales("view/macro:has-watermark", view=view)
self.assertIs(False, has_watermark)
def test_has_watermark_true(self):
@@ -141,14 +149,15 @@ class PageMacroDispatcherTestCase(TestPageMacroDispatcherMixin, TestCase):
class NoWatermarkView(TestView):
has_watermark = True
- self.registerBrowserViewAdapter(NoWatermarkView, ITest, '+test')
- view = create_view(TestObject(), name='+test')
- has_watermark = test_tales('view/macro:has-watermark', view=view)
+ self.registerBrowserViewAdapter(NoWatermarkView, ITest, "+test")
+ view = create_view(TestObject(), name="+test")
+ has_watermark = test_tales("view/macro:has-watermark", view=view)
self.assertIs(True, has_watermark)
-class PageMacroDispatcherInteractionTestCase(TestPageMacroDispatcherMixin,
- TestCaseWithFactory):
+class PageMacroDispatcherInteractionTestCase(
+ TestPageMacroDispatcherMixin, TestCaseWithFactory
+):
layer = DatabaseFunctionalLayer
def setUp(self):
@@ -160,6 +169,7 @@ class PageMacroDispatcherInteractionTestCase(TestPageMacroDispatcherMixin,
# Setup a specific permission for the test object.
class FakeSecurityChecker(AuthorizationBase):
"""A class to instrument a specific permission."""
+
@classmethod
def __call__(adaptee):
return FakeSecurityChecker(adaptee)
@@ -174,23 +184,25 @@ class PageMacroDispatcherInteractionTestCase(TestPageMacroDispatcherMixin,
return has_permission
self.registerAuthorizationAdapter(
- FakeSecurityChecker, ITest, 'launchpad.View')
+ FakeSecurityChecker, ITest, "launchpad.View"
+ )
def test_is_page_contentless_public(self):
# Public objects always have content to be shown.
self.assertFalse(
- test_tales('view/macro:is-page-contentless', view=self.view))
+ test_tales("view/macro:is-page-contentless", view=self.view)
+ )
def test_is_page_contentless_private_with_view(self):
# Private objects the user can view have content to be shown.
self.view.context.private = True
self._setUpPermissions(has_permission=True)
- result = test_tales('view/macro:is-page-contentless', view=self.view)
+ result = test_tales("view/macro:is-page-contentless", view=self.view)
self.assertFalse(result)
def test_is_page_contentless_private_without_view(self):
# Private objects the view cannot view cannot show content.
self.view.context.private = True
self._setUpPermissions(has_permission=False)
- result = test_tales('view/macro:is-page-contentless', view=self.view)
+ result = test_tales("view/macro:is-page-contentless", view=self.view)
self.assertTrue(result)
diff --git a/lib/lp/app/browser/tests/test_stringformatter.py b/lib/lp/app/browser/tests/test_stringformatter.py
index 1663f49..5490b46 100644
--- a/lib/lp/app/browser/tests/test_stringformatter.py
+++ b/lib/lp/app/browser/tests/test_stringformatter.py
@@ -3,33 +3,24 @@
"""Unit tests for the string TALES formatter."""
+import unittest
from doctest import DocTestSuite
from textwrap import dedent
-import unittest
-from testtools.matchers import (
- Equals,
- Matcher,
- )
+from testtools.matchers import Equals, Matcher
from zope.component import getUtility
from lp.app.browser.stringformatter import (
FormattersAPI,
linkify_bug_numbers,
parse_diff,
- )
+)
from lp.services.config import config
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.interfaces import ILaunchBag
from lp.services.webapp.publisher import canonical_url
-from lp.testing import (
- TestCase,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- ZopelessLayer,
- )
+from lp.testing import TestCase, TestCaseWithFactory
+from lp.testing.layers import DatabaseFunctionalLayer, ZopelessLayer
from lp.testing.pages import find_tags_by_class
@@ -123,24 +114,23 @@ def test_break_long_words():
class TestLinkifyingBugs(TestCase):
-
def test_regular_bug_case_works(self):
test_strings = [
"bug 34434",
"bugnumber 34434",
"bug number 34434",
- ]
+ ]
expected_html = [
+ '<p><a href="/bugs/34434" ' 'class="bug-link">bug 34434</a></p>',
'<p><a href="/bugs/34434" '
- 'class="bug-link">bug 34434</a></p>',
+ 'class="bug-link">bugnumber 34434</a></p>',
'<p><a href="/bugs/34434" '
- 'class="bug-link">bugnumber 34434</a></p>',
- '<p><a href="/bugs/34434" '
- 'class="bug-link">bug number 34434</a></p>',
- ]
+ 'class="bug-link">bug number 34434</a></p>',
+ ]
self.assertEqual(
expected_html,
- [FormattersAPI(text).text_to_html() for text in test_strings])
+ [FormattersAPI(text).text_to_html() for text in test_strings],
+ )
def test_things_do_not_link_if_they_should_not(self):
test_strings = [
@@ -148,22 +138,24 @@ class TestLinkifyingBugs(TestCase):
"bug number.4",
"bugno.4",
"bug no.4",
- ]
+ ]
expected_html = [
"<p>bugnumber.4</p>",
"<p>bug number.4</p>",
"<p>bugno.4</p>",
"<p>bug no.4</p>",
- ]
+ ]
self.assertEqual(
expected_html,
- [FormattersAPI(text).text_to_html() for text in test_strings])
+ [FormattersAPI(text).text_to_html() for text in test_strings],
+ )
def test_explicit_bug_linkification(self):
- text = 'LP: #10'
+ text = "LP: #10"
self.assertEqual(
'LP: <a href="/bugs/10" class="bug-link">#10</a>',
- linkify_bug_numbers(text))
+ linkify_bug_numbers(text),
+ )
class TestLinkifyingProtocols(TestCaseWithFactory):
@@ -176,56 +168,76 @@ class TestLinkifyingProtocols(TestCaseWithFactory):
"http://example.com/",
"http://example.com/path",
"http://example.com/path/",
- ]
+ ]
expected_strings = [
- ('<p><a rel="nofollow" href="http://example.com">'
- 'http://<wbr />example.<wbr />com</a></p>'),
- ('<p><a rel="nofollow" href="http://example.com/">'
- 'http://<wbr />example.<wbr />com/</a></p>'),
- ('<p><a rel="nofollow" href="http://example.com/path">'
- 'http://<wbr />example.<wbr />com/path</a></p>'),
- ('<p><a rel="nofollow" href="http://example.com/path/">'
- 'http://<wbr />example.<wbr />com/path/</a></p>'),
- ]
+ (
+ '<p><a rel="nofollow" href="http://example.com">'
+ "http://<wbr />example.<wbr />com</a></p>"
+ ),
+ (
+ '<p><a rel="nofollow" href="http://example.com/">'
+ "http://<wbr />example.<wbr />com/</a></p>"
+ ),
+ (
+ '<p><a rel="nofollow" href="http://example.com/path">'
+ "http://<wbr />example.<wbr />com/path</a></p>"
+ ),
+ (
+ '<p><a rel="nofollow" href="http://example.com/path/">'
+ "http://<wbr />example.<wbr />com/path/</a></p>"
+ ),
+ ]
self.assertEqual(
expected_strings,
- [FormattersAPI(text).text_to_html() for text in test_strings])
+ [FormattersAPI(text).text_to_html() for text in test_strings],
+ )
def test_parens_handled_well(self):
test_strings = [
- '(http://example.com)',
- 'http://example.com/path_(with_parens)',
- '(http://example.com/path_(with_parens))',
- '(http://example.com/path_(with_parens)and_stuff)',
- 'http://example.com/path_(with_parens',
- ]
+ "(http://example.com)",
+ "http://example.com/path_(with_parens)",
+ "(http://example.com/path_(with_parens))",
+ "(http://example.com/path_(with_parens)and_stuff)",
+ "http://example.com/path_(with_parens",
+ ]
expected_html = [
- ('<p>(<a rel="nofollow" href="http://example.com">'
- 'http://<wbr />example.<wbr />com</a>)</p>'),
- ('<p><a rel="nofollow" '
- 'href="http://example.com/path_(with_parens)">'
- 'http://<wbr />example.<wbr />com/path_'
- '<wbr />(with_parens)</a></p>'),
- ('<p>(<a rel="nofollow" '
- 'href="http://example.com/path_(with_parens)">'
- 'http://<wbr />example.<wbr />com/path_'
- '<wbr />(with_parens)</a>)</p>'),
- ('<p>(<a rel="nofollow" '
- 'href="http://example.com/path_(with_parens)and_stuff">'
- 'http://<wbr />example.<wbr />com'
- '/path_<wbr />(with_parens)<wbr />and_stuff</a>)</p>'),
- ('<p><a rel="nofollow" '
- 'href="http://example.com/path_(with_parens">'
- 'http://<wbr />example.<wbr />com'
- '/path_<wbr />(with_parens</a></p>'),
- ]
+ (
+ '<p>(<a rel="nofollow" href="http://example.com">'
+ "http://<wbr />example.<wbr />com</a>)</p>"
+ ),
+ (
+ '<p><a rel="nofollow" '
+ 'href="http://example.com/path_(with_parens)">'
+ "http://<wbr />example.<wbr />com/path_"
+ "<wbr />(with_parens)</a></p>"
+ ),
+ (
+ '<p>(<a rel="nofollow" '
+ 'href="http://example.com/path_(with_parens)">'
+ "http://<wbr />example.<wbr />com/path_"
+ "<wbr />(with_parens)</a>)</p>"
+ ),
+ (
+ '<p>(<a rel="nofollow" '
+ 'href="http://example.com/path_(with_parens)and_stuff">'
+ "http://<wbr />example.<wbr />com"
+ "/path_<wbr />(with_parens)<wbr />and_stuff</a>)</p>"
+ ),
+ (
+ '<p><a rel="nofollow" '
+ 'href="http://example.com/path_(with_parens">'
+ "http://<wbr />example.<wbr />com"
+ "/path_<wbr />(with_parens</a></p>"
+ ),
+ ]
self.assertEqual(
expected_html,
- [FormattersAPI(text).text_to_html() for text in test_strings])
+ [FormattersAPI(text).text_to_html() for text in test_strings],
+ )
def test_protocol_alone_does_not_link(self):
test_string = "This doesn't link: apt:"
@@ -239,21 +251,23 @@ class TestLinkifyingProtocols(TestCaseWithFactory):
self.assertEqual(expected_html, html)
def test_apt_is_linked(self):
- test_string = 'This becomes a link: apt:some-package'
+ test_string = "This becomes a link: apt:some-package"
html = FormattersAPI(test_string).text_to_html()
expected_html = (
- '<p>This becomes a link: '
+ "<p>This becomes a link: "
'<a rel="nofollow" '
- 'href="apt:some-package">apt:some-<wbr />package</a></p>')
+ 'href="apt:some-package">apt:some-<wbr />package</a></p>'
+ )
self.assertEqual(expected_html, html)
# Do it again for apt://
- test_string = 'This becomes a link: apt://some-package'
+ test_string = "This becomes a link: apt://some-package"
html = FormattersAPI(test_string).text_to_html()
expected_html = (
- '<p>This becomes a link: '
+ "<p>This becomes a link: "
'<a rel="nofollow" '
- 'href="apt://some-package">apt://some-<wbr />package</a></p>')
+ 'href="apt://some-package">apt://some-<wbr />package</a></p>'
+ )
self.assertEqual(expected_html, html)
def test_file_is_not_linked(self):
@@ -261,44 +275,50 @@ class TestLinkifyingProtocols(TestCaseWithFactory):
html = FormattersAPI(test_string).text_to_html()
expected_html = (
"<p>This doesn't become a link: "
- "file://<wbr />some/file.<wbr />txt</p>")
+ "file://<wbr />some/file.<wbr />txt</p>"
+ )
self.assertEqual(expected_html, html)
def test_no_link_with_linkify_text_false(self):
test_string = "This doesn't become a link: http://www.example.com/"
html = FormattersAPI(test_string).text_to_html(linkify_text=False)
expected_html = (
- "<p>This doesn't become a link: http://www.example.com/</p>")
+ "<p>This doesn't become a link: http://www.example.com/</p>"
+ )
self.assertEqual(expected_html, html)
def test_no_link_html_code_with_linkify_text_false(self):
test_string = '<a href="http://example.com/">http://example.com/</a>'
html = FormattersAPI(test_string).text_to_html(linkify_text=False)
expected_html = (
- '<p><a href="http://example.com/">'
- 'http://example.com/</a></p>')
+ "<p><a href="http://example.com/">"
+ "http://example.com/</a></p>"
+ )
self.assertEqual(expected_html, html)
def test_double_email_in_linkify_email(self):
- person = self.factory.makePerson(email='foo@xxxxxxxxxxx')
+ person = self.factory.makePerson(email="foo@xxxxxxxxxxx")
test_string = (
- ' * Foo. <foo@xxxxxxxxxxx>\n * Bar <foo@xxxxxxxxxxx>')
+ " * Foo. <foo@xxxxxxxxxxx>\n * Bar <foo@xxxxxxxxxxx>"
+ )
html = FormattersAPI(test_string).linkify_email()
url = canonical_url(person)
expected_html = (
' * Foo. <<a href="%s" class="sprite person">foo@xxxxxxxxxxx'
'</a>>\n * Bar <<a href="%s" class="sprite person">'
- 'foo@xxxxxxxxxxx</a>>' % (url, url))
+ "foo@xxxxxxxxxxx</a>>" % (url, url)
+ )
self.assertEqual(expected_html, html)
class TestLastParagraphClass(TestCase):
-
def test_last_paragraph_class(self):
self.assertEqual(
'<p>Foo</p>\n<p class="last">Bar</p>',
FormattersAPI("Foo\n\nBar").text_to_html(
- last_paragraph_class="last"))
+ last_paragraph_class="last"
+ ),
+ )
class TestParseDiff(TestCase):
@@ -306,28 +326,33 @@ class TestParseDiff(TestCase):
def test_emptyString(self):
# An empty string yields no lines.
- self.assertEqual([], list(parse_diff('')))
+ self.assertEqual([], list(parse_diff("")))
def test_almostEmptyString(self):
# White space yields a single line of text.
- self.assertEqual([('text', 1, 0, 0, ' ')], list(parse_diff(' ')))
+ self.assertEqual([("text", 1, 0, 0, " ")], list(parse_diff(" ")))
def test_unicode(self):
# Diffs containing Unicode work too.
self.assertEqual(
- [('text', 1, 0, 0, 'Unicode \u1010')],
- list(parse_diff('Unicode \u1010')))
+ [("text", 1, 0, 0, "Unicode \u1010")],
+ list(parse_diff("Unicode \u1010")),
+ )
def assertParses(self, expected, diff):
diff_lines = diff.splitlines()
self.assertEqual(
- [(css_class, row + 1, orig_row, mod_row, diff_lines[row])
- for row, (css_class, orig_row, mod_row) in enumerate(expected)],
- list(parse_diff(diff)))
+ [
+ (css_class, row + 1, orig_row, mod_row, diff_lines[row])
+ for row, (css_class, orig_row, mod_row) in enumerate(expected)
+ ],
+ list(parse_diff(diff)),
+ )
def test_basic_bzr(self):
# A basic Bazaar diff with a few different classes is parsed correctly.
- diff = dedent('''\
+ diff = dedent(
+ """\
=== modified file 'tales.py'
--- tales.py
+++ tales.py
@@ -341,27 +366,29 @@ class TestParseDiff(TestCase):
++++++++ a line of pluses
########
# A merge directive comment.
- ''')
+ """
+ )
expected = [
- ('diff-file text', None, None),
- ('diff-header text', None, None),
- ('diff-header text', None, None),
- ('diff-chunk text', None, None),
- ('text', 2435, 2439),
- ('diff-removed text', 2436, None),
- ('diff-added text', None, 2440),
- ('diff-added text', None, 2441),
- ('text', 2437, 2442),
- ('diff-removed text', 2438, None),
- ('diff-added text', None, 2443),
- ('diff-comment text', None, None),
- ('diff-comment text', None, None),
- ]
+ ("diff-file text", None, None),
+ ("diff-header text", None, None),
+ ("diff-header text", None, None),
+ ("diff-chunk text", None, None),
+ ("text", 2435, 2439),
+ ("diff-removed text", 2436, None),
+ ("diff-added text", None, 2440),
+ ("diff-added text", None, 2441),
+ ("text", 2437, 2442),
+ ("diff-removed text", 2438, None),
+ ("diff-added text", None, 2443),
+ ("diff-comment text", None, None),
+ ("diff-comment text", None, None),
+ ]
self.assertParses(expected, diff)
def test_basic_git(self):
# A basic Git diff with a few different classes is parsed correctly.
- diff = dedent('''\
+ diff = dedent(
+ """\
diff --git a/tales.py b/tales.py
index aaaaaaa..bbbbbbb 100644
--- a/tales.py
@@ -374,27 +401,29 @@ class TestParseDiff(TestCase):
something in between
-------- a sql style comment
++++++++ a line of pluses
- ''')
+ """
+ )
expected = [
- ('diff-file text', None, None),
- ('diff-file text', None, None),
- ('diff-header text', None, None),
- ('diff-header text', None, None),
- ('diff-chunk text', None, None),
- ('text', 2435, 2439),
- ('diff-removed text', 2436, None),
- ('diff-added text', None, 2440),
- ('diff-added text', None, 2441),
- ('text', 2437, 2442),
- ('diff-removed text', 2438, None),
- ('diff-added text', None, 2443),
- ]
+ ("diff-file text", None, None),
+ ("diff-file text", None, None),
+ ("diff-header text", None, None),
+ ("diff-header text", None, None),
+ ("diff-chunk text", None, None),
+ ("text", 2435, 2439),
+ ("diff-removed text", 2436, None),
+ ("diff-added text", None, 2440),
+ ("diff-added text", None, 2441),
+ ("text", 2437, 2442),
+ ("diff-removed text", 2438, None),
+ ("diff-added text", None, 2443),
+ ]
self.assertParses(expected, diff)
def test_config_value_limits_line_count(self):
# The config.diff.max_line_format contains the maximum number of
# lines to parse.
- diff = dedent('''\
+ diff = dedent(
+ """\
=== modified file 'tales.py'
--- tales.py
+++ tales.py
@@ -404,19 +433,21 @@ class TestParseDiff(TestCase):
+ added this line
########
# A merge directive comment.
- ''')
+ """
+ )
expected = [
- ('diff-file text', None, None),
- ('diff-header text', None, None),
- ('diff-header text', None, None),
- ]
+ ("diff-file text", None, None),
+ ("diff-header text", None, None),
+ ("diff-header text", None, None),
+ ]
self.pushConfig("diff", max_format_lines=3)
self.assertParses(expected, diff)
def test_multiple_hunks(self):
# Diffs containing multiple hunks are parsed reasonably, and the
# original and modified row numbers are adjusted for each hunk.
- diff = dedent('''\
+ diff = dedent(
+ """\
@@ -2,2 +2,3 @@
a
-b
@@ -427,19 +458,20 @@ class TestParseDiff(TestCase):
f
-g
+h
- ''')
+ """
+ )
expected = [
- ('diff-chunk text', None, None),
- ('text', 2, 2),
- ('diff-removed text', 3, None),
- ('diff-added text', None, 3),
- ('diff-added text', None, 4),
- ('diff-chunk text', None, None),
- ('diff-removed text', 10, None),
- ('text', 11, 11),
- ('diff-removed text', 12, None),
- ('diff-added text', None, 12),
- ]
+ ("diff-chunk text", None, None),
+ ("text", 2, 2),
+ ("diff-removed text", 3, None),
+ ("diff-added text", None, 3),
+ ("diff-added text", None, 4),
+ ("diff-chunk text", None, None),
+ ("diff-removed text", 10, None),
+ ("text", 11, 11),
+ ("diff-removed text", 12, None),
+ ("diff-added text", None, 12),
+ ]
self.assertParses(expected, diff)
@@ -450,23 +482,28 @@ class TestParseDiffErrors(TestCaseWithFactory):
def assertParses(self, expected, diff):
diff_lines = diff.splitlines()
self.assertEqual(
- [(css_class, row + 1, orig_row, mod_row, diff_lines[row])
- for row, (css_class, orig_row, mod_row) in enumerate(expected)],
- list(parse_diff(diff)))
+ [
+ (css_class, row + 1, orig_row, mod_row, diff_lines[row])
+ for row, (css_class, orig_row, mod_row) in enumerate(expected)
+ ],
+ list(parse_diff(diff)),
+ )
def test_bad_hunk_header(self):
# A bad hunk header causes the parser to record an OOPS but continue
# anyway.
- diff = dedent('''\
+ diff = dedent(
+ """\
@@ some nonsense @@
def method(self):
- ''')
+ """
+ )
expected = [
- ('diff-chunk text', None, None),
- ('text', 1, 1),
- ]
+ ("diff-chunk text", None, None),
+ ("text", 1, 1),
+ ]
self.assertParses(expected, diff)
- self.assertEqual('MalformedHunkHeader', self.oopses[0]['type'])
+ self.assertEqual("MalformedHunkHeader", self.oopses[0]["type"])
class TestDiffFormatter(TestCase):
@@ -474,28 +511,30 @@ class TestDiffFormatter(TestCase):
def test_emptyString(self):
# An empty string gives an empty string.
- self.assertEqual(
- '', FormattersAPI('').format_diff())
+ self.assertEqual("", FormattersAPI("").format_diff())
def test_almostEmptyString(self):
# White space doesn't count as empty, and is formatted.
self.assertEqual(
'<table class="diff unidiff"><tr id="diff-line-1">'
'<td class="line-no unselectable">1</td><td class="text"> '
- '</td></tr></table>',
- FormattersAPI(' ').format_diff())
+ "</td></tr></table>",
+ FormattersAPI(" ").format_diff(),
+ )
def test_format_unicode(self):
# Sometimes the strings contain unicode, those should work too.
self.assertEqual(
'<table class="diff unidiff"><tr id="diff-line-1">'
'<td class="line-no unselectable">1</td><td class="text">'
- 'Unicode \u1010</td></tr></table>',
- FormattersAPI('Unicode \u1010').format_diff())
+ "Unicode \u1010</td></tr></table>",
+ FormattersAPI("Unicode \u1010").format_diff(),
+ )
def test_cssClasses(self):
# Different parts of the diff have different css classes.
- diff = dedent('''\
+ diff = dedent(
+ """\
=== modified file 'tales.py'
--- tales.py
+++ tales.py
@@ -507,31 +546,37 @@ class TestDiffFormatter(TestCase):
++++++++ a line of pluses
########
# A merge directive comment.
- ''')
+ """
+ )
html = FormattersAPI(diff).format_diff()
- line_numbers = find_tags_by_class(html, 'line-no')
+ line_numbers = find_tags_by_class(html, "line-no")
self.assertEqual(
- ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'],
- [tag.decode_contents() for tag in line_numbers])
- text = find_tags_by_class(html, 'text')
+ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"],
+ [tag.decode_contents() for tag in line_numbers],
+ )
+ text = find_tags_by_class(html, "text")
self.assertEqual(
- [['diff-file', 'text'],
- ['diff-header', 'text'],
- ['diff-header', 'text'],
- ['diff-chunk', 'text'],
- ['text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['diff-comment', 'text'],
- ['diff-comment', 'text']],
- [tag['class'] for tag in text])
+ [
+ ["diff-file", "text"],
+ ["diff-header", "text"],
+ ["diff-header", "text"],
+ ["diff-chunk", "text"],
+ ["text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["diff-comment", "text"],
+ ["diff-comment", "text"],
+ ],
+ [tag["class"] for tag in text],
+ )
def test_cssClasses_git(self):
# Git diffs look slightly different, so check that they also end up
# with the correct CSS classes.
- diff = dedent('''\
+ diff = dedent(
+ """\
diff --git a/tales.py b/tales.py
index aaaaaaa..bbbbbbb 100644
--- a/tales.py
@@ -542,25 +587,30 @@ class TestDiffFormatter(TestCase):
+ added this line
-------- a sql style comment
++++++++ a line of pluses
- ''')
+ """
+ )
html = FormattersAPI(diff).format_diff()
- line_numbers = find_tags_by_class(html, 'line-no')
+ line_numbers = find_tags_by_class(html, "line-no")
self.assertEqual(
- ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
- [tag.decode_contents() for tag in line_numbers])
- text = find_tags_by_class(html, 'text')
+ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
+ [tag.decode_contents() for tag in line_numbers],
+ )
+ text = find_tags_by_class(html, "text")
self.assertEqual(
- [['diff-file', 'text'],
- ['diff-file', 'text'],
- ['diff-header', 'text'],
- ['diff-header', 'text'],
- ['diff-chunk', 'text'],
- ['text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text']],
- [tag['class'] for tag in text])
+ [
+ ["diff-file", "text"],
+ ["diff-file", "text"],
+ ["diff-header", "text"],
+ ["diff-header", "text"],
+ ["diff-chunk", "text"],
+ ["text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ],
+ [tag["class"] for tag in text],
+ )
class TestSideBySideDiffFormatter(TestCase):
@@ -568,8 +618,7 @@ class TestSideBySideDiffFormatter(TestCase):
def test_emptyString(self):
# An empty string gives an empty string.
- self.assertEqual(
- '', FormattersAPI('').format_ssdiff())
+ self.assertEqual("", FormattersAPI("").format_ssdiff())
def test_almostEmptyString(self):
# White space doesn't count as empty, and is formatted.
@@ -580,8 +629,9 @@ class TestSideBySideDiffFormatter(TestCase):
'<td class="text"></td>'
'<td class="ss-line-no unselectable">0</td>'
'<td class="text"></td>'
- '</tr></table>',
- FormattersAPI(' ').format_ssdiff())
+ "</tr></table>",
+ FormattersAPI(" ").format_ssdiff(),
+ )
def test_format_unicode(self):
# Sometimes the strings contain unicode, those should work too.
@@ -592,12 +642,14 @@ class TestSideBySideDiffFormatter(TestCase):
'<td class="text">Unicode \u1010</td>'
'<td class="ss-line-no unselectable">0</td>'
'<td class="text">Unicode \u1010</td>'
- '</tr></table>',
- FormattersAPI('Unicode \u1010').format_ssdiff())
+ "</tr></table>",
+ FormattersAPI("Unicode \u1010").format_ssdiff(),
+ )
def test_cssClasses(self):
# Different parts of the diff have different css classes.
- diff = dedent('''\
+ diff = dedent(
+ """\
=== modified file 'tales.py'
--- tales.py
+++ tales.py
@@ -611,41 +663,58 @@ class TestSideBySideDiffFormatter(TestCase):
++++++++ a line of pluses
########
# A merge directive comment.
- ''')
+ """
+ )
html = FormattersAPI(diff).format_ssdiff()
- line_numbers = find_tags_by_class(html, 'line-no')
+ line_numbers = find_tags_by_class(html, "line-no")
self.assertEqual(
- ['1', '2', '3', '4', '5', '7', '8', '9', '11', '12', '13'],
- [tag.decode_contents() for tag in line_numbers])
- ss_line_numbers = find_tags_by_class(html, 'ss-line-no')
+ ["1", "2", "3", "4", "5", "7", "8", "9", "11", "12", "13"],
+ [tag.decode_contents() for tag in line_numbers],
+ )
+ ss_line_numbers = find_tags_by_class(html, "ss-line-no")
self.assertEqual(
- ['2435', '2439', '2436', '2440', '', '2441', '2437', '2442',
- '2438', '2443'],
- [tag.decode_contents() for tag in ss_line_numbers])
- text = find_tags_by_class(html, 'text')
+ [
+ "2435",
+ "2439",
+ "2436",
+ "2440",
+ "",
+ "2441",
+ "2437",
+ "2442",
+ "2438",
+ "2443",
+ ],
+ [tag.decode_contents() for tag in ss_line_numbers],
+ )
+ text = find_tags_by_class(html, "text")
self.assertEqual(
- [['diff-file', 'text'],
- ['diff-header', 'text'],
- ['diff-header', 'text'],
- ['diff-chunk', 'text'],
- ['text'],
- ['text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['text'],
- ['text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['diff-comment', 'text'],
- ['diff-comment', 'text']],
- [tag['class'] for tag in text])
+ [
+ ["diff-file", "text"],
+ ["diff-header", "text"],
+ ["diff-header", "text"],
+ ["diff-chunk", "text"],
+ ["text"],
+ ["text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["text"],
+ ["text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["diff-comment", "text"],
+ ["diff-comment", "text"],
+ ],
+ [tag["class"] for tag in text],
+ )
def test_cssClasses_git(self):
# Git diffs look slightly different, so check that they also end up
# with the correct CSS classes.
- diff = dedent('''\
+ diff = dedent(
+ """\
diff --git a/tales.py b/tales.py
index aaaaaaa..bbbbbbb 100644
--- a/tales.py
@@ -658,35 +727,51 @@ class TestSideBySideDiffFormatter(TestCase):
something in between
-------- a sql style comment
++++++++ a line of pluses
- ''')
+ """
+ )
html = FormattersAPI(diff).format_ssdiff()
- line_numbers = find_tags_by_class(html, 'line-no')
+ line_numbers = find_tags_by_class(html, "line-no")
self.assertEqual(
- ['1', '2', '3', '4', '5', '6', '8', '9', '10', '12'],
- [tag.decode_contents() for tag in line_numbers])
- ss_line_numbers = find_tags_by_class(html, 'ss-line-no')
+ ["1", "2", "3", "4", "5", "6", "8", "9", "10", "12"],
+ [tag.decode_contents() for tag in line_numbers],
+ )
+ ss_line_numbers = find_tags_by_class(html, "ss-line-no")
self.assertEqual(
- ['2435', '2439', '2436', '2440', '', '2441', '2437', '2442',
- '2438', '2443'],
- [tag.decode_contents() for tag in ss_line_numbers])
- text = find_tags_by_class(html, 'text')
+ [
+ "2435",
+ "2439",
+ "2436",
+ "2440",
+ "",
+ "2441",
+ "2437",
+ "2442",
+ "2438",
+ "2443",
+ ],
+ [tag.decode_contents() for tag in ss_line_numbers],
+ )
+ text = find_tags_by_class(html, "text")
self.assertEqual(
- [['diff-file', 'text'],
- ['diff-file', 'text'],
- ['diff-header', 'text'],
- ['diff-header', 'text'],
- ['diff-chunk', 'text'],
- ['text'],
- ['text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text'],
- ['text'],
- ['text'],
- ['diff-removed', 'text'],
- ['diff-added', 'text']],
- [tag['class'] for tag in text])
+ [
+ ["diff-file", "text"],
+ ["diff-file", "text"],
+ ["diff-header", "text"],
+ ["diff-header", "text"],
+ ["diff-chunk", "text"],
+ ["text"],
+ ["text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ["text"],
+ ["text"],
+ ["diff-removed", "text"],
+ ["diff-added", "text"],
+ ],
+ [tag["class"] for tag in text],
+ )
class TestOOPSFormatter(TestCase):
@@ -701,55 +786,61 @@ class TestOOPSFormatter(TestCase):
def test_doesnt_linkify_for_non_developers(self):
# OOPS IDs won't be linkified for non-developers.
- oops_id = 'OOPS-12345TEST'
+ oops_id = "OOPS-12345TEST"
formatter = FormattersAPI(oops_id)
formatted_string = formatter.oops_id()
self.assertEqual(
- oops_id, formatted_string,
- "Formatted string should be '%s', was '%s'" % (
- oops_id, formatted_string))
+ oops_id,
+ formatted_string,
+ "Formatted string should be '%s', was '%s'"
+ % (oops_id, formatted_string),
+ )
def test_linkifies_for_developers(self):
# OOPS IDs will be linkified for Launchpad developers.
- oops_id = 'OOPS-12345TEST'
+ oops_id = "OOPS-12345TEST"
formatter = FormattersAPI(oops_id)
self._setDeveloper(True)
formatted_string = formatter.oops_id()
expected_string = '<a href="%s">%s</a>' % (
- config.launchpad.oops_root_url + oops_id, oops_id)
+ config.launchpad.oops_root_url + oops_id,
+ oops_id,
+ )
self.assertEqual(
- expected_string, formatted_string,
- "Formatted string should be '%s', was '%s'" % (
- expected_string, formatted_string))
+ expected_string,
+ formatted_string,
+ "Formatted string should be '%s', was '%s'"
+ % (expected_string, formatted_string),
+ )
class MarksDownAs(Matcher):
-
def __init__(self, expected_html):
self.expected_html = expected_html
def match(self, input_string):
return Equals(self.expected_html).match(
- FormattersAPI(input_string).markdown())
+ FormattersAPI(input_string).markdown()
+ )
class TestMarkdownDisabled(TestCase):
- """Feature flag can turn Markdown stuff off.
- """
+ """Feature flag can turn Markdown stuff off."""
layer = DatabaseFunctionalLayer # Fixtures need the database for now
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({'markdown.enabled': None}))
+ self.useFixture(FeatureFixture({"markdown.enabled": None}))
def test_plain_text(self):
self.assertThat(
- 'hello **simple** world',
- MarksDownAs('<p>hello **simple** world</p>'))
+ "hello **simple** world",
+ MarksDownAs("<p>hello **simple** world</p>"),
+ )
class TestMarkdown(TestCase):
@@ -763,12 +854,10 @@ class TestMarkdown(TestCase):
def setUp(self):
super().setUp()
- self.useFixture(FeatureFixture({'markdown.enabled': 'on'}))
+ self.useFixture(FeatureFixture({"markdown.enabled": "on"}))
def test_plain_text(self):
- self.assertThat(
- 'hello world',
- MarksDownAs('<p>hello world</p>'))
+ self.assertThat("hello world", MarksDownAs("<p>hello world</p>"))
def test_suite():
diff --git a/lib/lp/app/browser/tests/test_views.py b/lib/lp/app/browser/tests/test_views.py
index 1a8c266..6a3e4b4 100644
--- a/lib/lp/app/browser/tests/test_views.py
+++ b/lib/lp/app/browser/tests/test_views.py
@@ -8,19 +8,11 @@ import os
from lp.services.features.testing import FeatureFixture
from lp.services.testing import build_test_suite
-from lp.testing.layers import (
- BingLaunchpadFunctionalLayer,
- PageTestLayer,
- )
-from lp.testing.systemdocs import (
- LayeredDocFileSuite,
- setUp,
- tearDown,
- )
-
+from lp.testing.layers import BingLaunchpadFunctionalLayer, PageTestLayer
+from lp.testing.systemdocs import LayeredDocFileSuite, setUp, tearDown
here = os.path.dirname(os.path.realpath(__file__))
-bing_flag = FeatureFixture({'sitesearch.engine.name': 'bing'})
+bing_flag = FeatureFixture({"sitesearch.engine.name": "bing"})
def setUp_bing(test):
@@ -37,19 +29,23 @@ def tearDown_bing(test):
# that require something special like the librarian must run on a layer
# that sets those services up.
special = {
- 'launchpad-search-pages.txt(Bing)': LayeredDocFileSuite(
- '../doc/launchpad-search-pages.txt',
- id_extensions=['launchpad-search-pages.txt(Bing)'],
- setUp=setUp_bing, tearDown=tearDown_bing,
+ "launchpad-search-pages.txt(Bing)": LayeredDocFileSuite(
+ "../doc/launchpad-search-pages.txt",
+ id_extensions=["launchpad-search-pages.txt(Bing)"],
+ setUp=setUp_bing,
+ tearDown=tearDown_bing,
layer=BingLaunchpadFunctionalLayer,
- stdout_logging_level=logging.WARNING),
+ stdout_logging_level=logging.WARNING,
+ ),
# Run these doctests again with the default search engine.
- 'launchpad-search-pages.txt': LayeredDocFileSuite(
- '../doc/launchpad-search-pages.txt',
- setUp=setUp, tearDown=tearDown,
+ "launchpad-search-pages.txt": LayeredDocFileSuite(
+ "../doc/launchpad-search-pages.txt",
+ setUp=setUp,
+ tearDown=tearDown,
layer=PageTestLayer,
- stdout_logging_level=logging.WARNING),
- }
+ stdout_logging_level=logging.WARNING,
+ ),
+}
def test_suite():
diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py
index 8a04ba5..05e5fee 100644
--- a/lib/lp/app/browser/tests/test_vocabulary.py
+++ b/lib/lp/app/browser/tests/test_vocabulary.py
@@ -8,10 +8,7 @@ from urllib.parse import urlencode
import pytz
import simplejson
-from zope.component import (
- getSiteManager,
- getUtility,
- )
+from zope.component import getSiteManager, getUtility
from zope.formlib.interfaces import MissingInputError
from zope.interface import implementer
from zope.schema.interfaces import IVocabularyFactory
@@ -19,9 +16,9 @@ from zope.schema.vocabulary import SimpleTerm
from zope.security.proxy import removeSecurityProxy
from lp.app.browser.vocabulary import (
- IPickerEntrySource,
MAX_DESCRIPTION_LENGTH,
- )
+ IPickerEntrySource,
+)
from lp.app.errors import UnexpectedFormData
from lp.registry.interfaces.irc import IIrcIDSet
from lp.registry.interfaces.person import TeamMembershipPolicy
@@ -31,23 +28,17 @@ from lp.services.webapp.vocabulary import (
CountableIterator,
IHugeVocabulary,
VocabularyFilter,
- )
-from lp.testing import (
- celebrity_logged_in,
- login_person,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
+)
+from lp.testing import TestCaseWithFactory, celebrity_logged_in, login_person
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
from lp.testing.views import create_view
def get_picker_entry(item_subject, context_object, **kwargs):
"""Adapt `item_subject` to `IPickerEntrySource` and return its item."""
[entry] = IPickerEntrySource(item_subject).getPickerEntries(
- [item_subject], context_object, **kwargs)
+ [item_subject], context_object, **kwargs
+ )
return entry
@@ -67,13 +58,14 @@ class DefaultPickerEntrySourceAdapterTestCase(TestCaseWithFactory):
# sprite adapter rules, the generic sprite is used.
thing = object()
entry = get_picker_entry(thing, object())
- self.assertEqual('sprite bullet', entry.css)
+ self.assertEqual("sprite bullet", entry.css)
self.assertEqual(None, entry.image)
def test_css_image_entry_with_icon(self):
# When the context has a custom icon the URL is used.
icon = self.factory.makeLibraryFileAlias(
- filename='smurf.png', content_type='image/png')
+ filename="smurf.png", content_type="image/png"
+ )
product = self.factory.makeProduct(icon=icon)
entry = get_picker_entry(product, object())
self.assertEqual(None, entry.css)
@@ -92,29 +84,32 @@ class PersonPickerEntrySourceAdapterTestCase(TestCaseWithFactory):
def test_PersonPickerEntrySourceAdapter_email_anonymous(self):
# Anonymous users cannot see entry email addresses.
- person = self.factory.makePerson(email='snarf@xxxxxx')
+ person = self.factory.makePerson(email="snarf@xxxxxx")
self.assertEqual(
"<email address hidden>",
- get_picker_entry(person, None).description)
+ get_picker_entry(person, None).description,
+ )
def test_PersonPickerEntrySourceAdapter_visible_email_logged_in(self):
# Logged in users can see visible email addresses.
observer = self.factory.makePerson()
login_person(observer)
- person = self.factory.makePerson(email='snarf@xxxxxx')
+ person = self.factory.makePerson(email="snarf@xxxxxx")
self.assertEqual(
- 'snarf@xxxxxx', get_picker_entry(person, None).description)
+ "snarf@xxxxxx", get_picker_entry(person, None).description
+ )
def test_PersonPickerEntrySourceAdapter_hidden_email_logged_in(self):
# Logged in users cannot see hidden email addresses.
- person = self.factory.makePerson(email='snarf@xxxxxx')
+ person = self.factory.makePerson(email="snarf@xxxxxx")
login_person(person)
person.hide_email_addresses = True
observer = self.factory.makePerson()
login_person(observer)
self.assertEqual(
"<email address hidden>",
- get_picker_entry(person, None).description)
+ get_picker_entry(person, None).description,
+ )
def test_PersonPickerEntrySourceAdapter_no_email_logged_in(self):
# Teams without email address have no desriptions.
@@ -127,65 +122,73 @@ class PersonPickerEntrySourceAdapterTestCase(TestCaseWithFactory):
# Logged in users can see visible email addresses.
observer = self.factory.makePerson()
login_person(observer)
- person = self.factory.makePerson(
- email='snarf@xxxxxx', name='snarf')
+ person = self.factory.makePerson(email="snarf@xxxxxx", name="snarf")
entry = get_picker_entry(person, None)
- self.assertEqual('sprite person', entry.css)
- self.assertEqual('sprite new-window', entry.link_css)
+ self.assertEqual("sprite person", entry.css)
+ self.assertEqual("sprite new-window", entry.link_css)
def test_PersonPickerEntrySourceAdapter_user(self):
# The person picker provides more information for users.
- person = self.factory.makePerson(email='snarf@xxxxxx', name='snarf')
- creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, pytz.timezone('UTC'))
+ person = self.factory.makePerson(email="snarf@xxxxxx", name="snarf")
+ creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, pytz.timezone("UTC"))
removeSecurityProxy(person).datecreated = creation_date
- getUtility(IIrcIDSet).new(person, 'eg.dom', 'snarf')
- getUtility(IIrcIDSet).new(person, 'ex.dom', 'pting')
+ getUtility(IIrcIDSet).new(person, "eg.dom", "snarf")
+ getUtility(IIrcIDSet).new(person, "ex.dom", "pting")
entry = get_picker_entry(person, None, picker_expander_enabled=True)
- self.assertEqual('http://launchpad.test/~snarf', entry.alt_title_link)
+ self.assertEqual("http://launchpad.test/~snarf", entry.alt_title_link)
self.assertEqual(
- ['snarf on eg.dom, pting on ex.dom', 'Member since 2005-01-30'],
- entry.details)
+ ["snarf on eg.dom, pting on ex.dom", "Member since 2005-01-30"],
+ entry.details,
+ )
def test_PersonPickerEntrySourceAdapter_team(self):
# The person picker provides more information for teams.
- team = self.factory.makeTeam(email='fnord@xxxxxx', name='fnord')
+ team = self.factory.makeTeam(email="fnord@xxxxxx", name="fnord")
entry = get_picker_entry(team, None, picker_expander_enabled=True)
- self.assertEqual('http://launchpad.test/~fnord', entry.alt_title_link)
- self.assertEqual(['Team members: 1'], entry.details)
+ self.assertEqual("http://launchpad.test/~fnord", entry.alt_title_link)
+ self.assertEqual(["Team members: 1"], entry.details)
def test_PersonPickerEntryAdapter_badges(self):
# The person picker provides affiliation information.
- person = self.factory.makePerson(email='snarf@xxxxxx', name='snarf')
+ person = self.factory.makePerson(email="snarf@xxxxxx", name="snarf")
project = self.factory.makeProduct(
- name='fnord', owner=person, bug_supervisor=person)
+ name="fnord", owner=person, bug_supervisor=person
+ )
bugtask = self.factory.makeBugTask(target=project)
entry = get_picker_entry(
- person, bugtask, picker_expander_enabled=True,
- personpicker_affiliation_enabled=True)
+ person,
+ bugtask,
+ picker_expander_enabled=True,
+ personpicker_affiliation_enabled=True,
+ )
self.assertEqual(3, len(entry.badges))
- self.assertEqual('/@@/product-badge', entry.badges[0]['url'])
- self.assertEqual('Fnord', entry.badges[0]['label'])
- self.assertEqual('maintainer', entry.badges[0]['role'])
- self.assertEqual('/@@/product-badge', entry.badges[1]['url'])
- self.assertEqual('Fnord', entry.badges[1]['label'])
- self.assertEqual('driver', entry.badges[1]['role'])
- self.assertEqual('/@@/product-badge', entry.badges[2]['url'])
- self.assertEqual('Fnord', entry.badges[2]['label'])
- self.assertEqual('bug supervisor', entry.badges[2]['role'])
+ self.assertEqual("/@@/product-badge", entry.badges[0]["url"])
+ self.assertEqual("Fnord", entry.badges[0]["label"])
+ self.assertEqual("maintainer", entry.badges[0]["role"])
+ self.assertEqual("/@@/product-badge", entry.badges[1]["url"])
+ self.assertEqual("Fnord", entry.badges[1]["label"])
+ self.assertEqual("driver", entry.badges[1]["role"])
+ self.assertEqual("/@@/product-badge", entry.badges[2]["url"])
+ self.assertEqual("Fnord", entry.badges[2]["label"])
+ self.assertEqual("bug supervisor", entry.badges[2]["role"])
def test_PersonPickerEntryAdapter_badges_without_IHasAffiliation(self):
# The person picker handles objects that do not support
# IHasAffilliation.
- person = self.factory.makePerson(email='snarf@xxxxxx', name='snarf')
+ person = self.factory.makePerson(email="snarf@xxxxxx", name="snarf")
thing = object()
entry = get_picker_entry(
- person, thing, picker_expander_enabled=True,
- personpicker_affiliation_enabled=True)
+ person,
+ thing,
+ picker_expander_enabled=True,
+ personpicker_affiliation_enabled=True,
+ )
self.assertIsNot(None, entry)
class TestDistributionSourcePackagePickerEntrySourceAdapter(
- TestCaseWithFactory):
+ TestCaseWithFactory
+):
layer = DatabaseFunctionalLayer
@@ -201,12 +204,12 @@ class TestDistributionSourcePackagePickerEntrySourceAdapter(
dsp = self.factory.makeDistributionSourcePackage()
series = self.factory.makeDistroSeries(distribution=dsp.distribution)
release = self.factory.makeSourcePackageRelease(
- distroseries=series,
- sourcepackagename=dsp.sourcepackagename)
+ distroseries=series, sourcepackagename=dsp.sourcepackagename
+ )
self.factory.makeSourcePackagePublishingHistory(
- distroseries=series,
- sourcepackagerelease=release)
- self.assertEqual('package', self.getPickerEntry(dsp).target_type)
+ distroseries=series, sourcepackagerelease=release
+ )
+ self.assertEqual("package", self.getPickerEntry(dsp).target_type)
def test_dsp_provides_details_no_maintainer(self):
dsp = self.factory.makeDistributionSourcePackage(with_db=True)
@@ -215,47 +218,54 @@ class TestDistributionSourcePackagePickerEntrySourceAdapter(
def test_dsp_provides_summary_unbuilt(self):
dsp = self.factory.makeDistributionSourcePackage(with_db=True)
self.assertEqual(
- "Not yet built.", self.getPickerEntry(dsp).description)
+ "Not yet built.", self.getPickerEntry(dsp).description
+ )
def test_dsp_provides_summary_built(self):
dsp = self.factory.makeDistributionSourcePackage(with_db=True)
series = self.factory.makeDistroSeries(distribution=dsp.distribution)
release = self.factory.makeSourcePackageRelease(
- distroseries=series,
- sourcepackagename=dsp.sourcepackagename)
+ distroseries=series, sourcepackagename=dsp.sourcepackagename
+ )
self.factory.makeSourcePackagePublishingHistory(
- distroseries=series,
- sourcepackagerelease=release)
+ distroseries=series, sourcepackagerelease=release
+ )
archseries = self.factory.makeDistroArchSeries(distroseries=series)
- bpn = self.factory.makeBinaryPackageName(name='fnord')
+ bpn = self.factory.makeBinaryPackageName(name="fnord")
self.factory.makeBinaryPackagePublishingHistory(
binarypackagename=bpn,
source_package_release=release,
sourcepackagename=dsp.sourcepackagename,
- distroarchseries=archseries)
+ distroarchseries=archseries,
+ )
self.assertEqual("fnord", self.getPickerEntry(dsp).description)
def test_dsp_alt_title_is_none(self):
# DSP titles are contructed from the distro and package Launchapd Ids,
# alt_titles are redundant because they are also Launchpad Ids.
- distro = self.factory.makeDistribution(name='fnord')
+ distro = self.factory.makeDistribution(name="fnord")
series = self.factory.makeDistroSeries(
- name='pting', distribution=distro)
+ name="pting", distribution=distro
+ )
self.factory.makeSourcePackage(
- sourcepackagename='snarf', distroseries=series, publish=True)
- dsp = distro.getSourcePackage('snarf')
+ sourcepackagename="snarf", distroseries=series, publish=True
+ )
+ dsp = distro.getSourcePackage("snarf")
self.assertEqual(None, self.getPickerEntry(dsp).alt_title)
def test_dsp_provides_alt_title_link(self):
- distro = self.factory.makeDistribution(name='fnord')
+ distro = self.factory.makeDistribution(name="fnord")
series = self.factory.makeDistroSeries(
- name='pting', distribution=distro)
+ name="pting", distribution=distro
+ )
self.factory.makeSourcePackage(
- sourcepackagename='snarf', distroseries=series, publish=True)
- dsp = distro.getSourcePackage('snarf')
+ sourcepackagename="snarf", distroseries=series, publish=True
+ )
+ dsp = distro.getSourcePackage("snarf")
self.assertEqual(
- 'http://launchpad.test/fnord/+source/snarf',
- self.getPickerEntry(dsp).alt_title_link)
+ "http://launchpad.test/fnord/+source/snarf",
+ self.getPickerEntry(dsp).alt_title_link,
+ )
class TestProductPickerEntrySourceAdapter(TestCaseWithFactory):
@@ -278,60 +288,66 @@ class TestProductPickerEntrySourceAdapter(TestCaseWithFactory):
product = self.factory.makeProduct()
# We check for project, not product, because users don't see
# products.
- self.assertEqual('project', self.getPickerEntry(product).target_type)
+ self.assertEqual("project", self.getPickerEntry(product).target_type)
def test_product_provides_details(self):
product = self.factory.makeProduct()
self.assertEqual(
"Maintainer: %s" % product.owner.displayname,
- self.getPickerEntry(product).details[0])
+ self.getPickerEntry(product).details[0],
+ )
def test_product_provides_summary(self):
product = self.factory.makeProduct()
self.assertEqual(
- product.summary, self.getPickerEntry(product).description)
+ product.summary, self.getPickerEntry(product).description
+ )
def test_product_truncates_summary(self):
- summary = ("This is a deliberately, overly long summary. It goes on"
- "and on and on so as to break things up a good bit.")
+ summary = (
+ "This is a deliberately, overly long summary. It goes on"
+ "and on and on so as to break things up a good bit."
+ )
product = self.factory.makeProduct(summary=summary)
- index = summary.rfind(' ', 0, 45)
- expected_summary = summary[:index + 1]
+ index = summary.rfind(" ", 0, 45)
+ expected_summary = summary[: index + 1]
expected_details = summary[index:]
entry = self.getPickerEntry(product)
- self.assertEqual(
- expected_summary, entry.description)
- self.assertEqual(
- expected_details, entry.details[0])
+ self.assertEqual(expected_summary, entry.description)
+ self.assertEqual(expected_details, entry.details[0])
def test_product_provides_alt_title_link(self):
- product = self.factory.makeProduct(name='fnord')
+ product = self.factory.makeProduct(name="fnord")
self.assertEqual(
- 'http://launchpad.test/fnord',
- self.getPickerEntry(product).alt_title_link)
+ "http://launchpad.test/fnord",
+ self.getPickerEntry(product).alt_title_link,
+ )
def test_provides_commercial_subscription_none(self):
- product = self.factory.makeProduct(name='fnord')
+ product = self.factory.makeProduct(name="fnord")
self.assertEqual(
- 'Commercial Subscription: None',
- self.getPickerEntry(product).details[1])
+ "Commercial Subscription: None",
+ self.getPickerEntry(product).details[1],
+ )
def test_provides_commercial_subscription_active(self):
- product = self.factory.makeProduct(name='fnord')
+ product = self.factory.makeProduct(name="fnord")
self.factory.makeCommercialSubscription(product)
self.assertEqual(
- 'Commercial Subscription: Active',
- self.getPickerEntry(product).details[1])
+ "Commercial Subscription: Active",
+ self.getPickerEntry(product).details[1],
+ )
def test_provides_commercial_subscription_expired(self):
- product = self.factory.makeProduct(name='fnord')
+ product = self.factory.makeProduct(name="fnord")
self.factory.makeCommercialSubscription(product)
then = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
- with celebrity_logged_in('admin'):
+ with celebrity_logged_in("admin"):
product.commercial_subscription.date_expires = then
self.assertEqual(
- 'Commercial Subscription: Expired',
- self.getPickerEntry(product).details[1])
+ "Commercial Subscription: Expired",
+ self.getPickerEntry(product).details[1],
+ )
class TestProjectGroupPickerEntrySourceAdapter(TestCaseWithFactory):
@@ -349,43 +365,47 @@ class TestProjectGroupPickerEntrySourceAdapter(TestCaseWithFactory):
def test_projectgroup_provides_alt_title(self):
projectgroup = self.factory.makeProject()
self.assertEqual(
- projectgroup.name, self.getPickerEntry(projectgroup).alt_title)
+ projectgroup.name, self.getPickerEntry(projectgroup).alt_title
+ )
def test_projectgroup_target_type(self):
projectgroup = self.factory.makeProject()
self.assertEqual(
- 'project group', self.getPickerEntry(projectgroup).target_type)
+ "project group", self.getPickerEntry(projectgroup).target_type
+ )
def test_projectgroup_provides_details(self):
projectgroup = self.factory.makeProject()
self.assertEqual(
"Maintainer: %s" % projectgroup.owner.displayname,
- self.getPickerEntry(projectgroup).details[0])
+ self.getPickerEntry(projectgroup).details[0],
+ )
def test_projectgroup_provides_summary(self):
projectgroup = self.factory.makeProject()
self.assertEqual(
- projectgroup.summary,
- self.getPickerEntry(projectgroup).description)
+ projectgroup.summary, self.getPickerEntry(projectgroup).description
+ )
def test_projectgroup_truncates_summary(self):
- summary = ("This is a deliberately, overly long summary. It goes on"
- "and on and on so as to break things up a good bit.")
+ summary = (
+ "This is a deliberately, overly long summary. It goes on"
+ "and on and on so as to break things up a good bit."
+ )
projectgroup = self.factory.makeProject(summary=summary)
- index = summary.rfind(' ', 0, 45)
- expected_summary = summary[:index + 1]
+ index = summary.rfind(" ", 0, 45)
+ expected_summary = summary[: index + 1]
expected_details = summary[index:]
entry = self.getPickerEntry(projectgroup)
- self.assertEqual(
- expected_summary, entry.description)
- self.assertEqual(
- expected_details, entry.details[0])
+ self.assertEqual(expected_summary, entry.description)
+ self.assertEqual(expected_details, entry.details[0])
def test_projectgroup_provides_alt_title_link(self):
- projectgroup = self.factory.makeProject(name='fnord')
+ projectgroup = self.factory.makeProject(name="fnord")
self.assertEqual(
- 'http://launchpad.test/fnord',
- self.getPickerEntry(projectgroup).alt_title_link)
+ "http://launchpad.test/fnord",
+ self.getPickerEntry(projectgroup).alt_title_link,
+ )
class TestDistributionPickerEntrySourceAdapter(TestCaseWithFactory):
@@ -403,75 +423,85 @@ class TestDistributionPickerEntrySourceAdapter(TestCaseWithFactory):
def test_distribution_provides_alt_title(self):
distribution = self.factory.makeDistribution()
self.assertEqual(
- distribution.name, self.getPickerEntry(distribution).alt_title)
+ distribution.name, self.getPickerEntry(distribution).alt_title
+ )
def test_distribution_provides_details(self):
distribution = self.factory.makeDistribution()
self.factory.makeDistroSeries(
- distribution=distribution, status=SeriesStatus.CURRENT)
+ distribution=distribution, status=SeriesStatus.CURRENT
+ )
self.assertEqual(
"Maintainer: %s" % distribution.currentseries.owner.displayname,
- self.getPickerEntry(distribution).details[0])
+ self.getPickerEntry(distribution).details[0],
+ )
def test_distribution_provides_summary(self):
distribution = self.factory.makeDistribution()
self.assertEqual(
- distribution.summary,
- self.getPickerEntry(distribution).description)
+ distribution.summary, self.getPickerEntry(distribution).description
+ )
def test_distribution_target_type(self):
distribution = self.factory.makeDistribution()
self.assertEqual(
- 'distribution', self.getPickerEntry(distribution).target_type)
+ "distribution", self.getPickerEntry(distribution).target_type
+ )
def test_distribution_truncates_summary(self):
summary = (
"This is a deliberately, overly long summary. It goes on "
- "and on and on so as to break things up a good bit.")
+ "and on and on so as to break things up a good bit."
+ )
distribution = self.factory.makeDistribution(summary=summary)
- index = summary.rfind(' ', 0, 45)
- expected_summary = summary[:index + 1]
+ index = summary.rfind(" ", 0, 45)
+ expected_summary = summary[: index + 1]
expected_details = summary[index:]
entry = self.getPickerEntry(distribution)
- self.assertEqual(
- expected_summary, entry.description)
- self.assertEqual(
- expected_details, entry.details[0])
+ self.assertEqual(expected_summary, entry.description)
+ self.assertEqual(expected_details, entry.details[0])
def test_distribution_provides_alt_title_link(self):
- distribution = self.factory.makeDistribution(name='fnord')
+ distribution = self.factory.makeDistribution(name="fnord")
self.assertEqual(
- 'http://launchpad.test/fnord',
- self.getPickerEntry(distribution).alt_title_link)
+ "http://launchpad.test/fnord",
+ self.getPickerEntry(distribution).alt_title_link,
+ )
def test_provides_commercial_subscription_none(self):
distribution = self.factory.makeDistribution()
self.factory.makeDistroSeries(
- distribution=distribution, status=SeriesStatus.CURRENT)
+ distribution=distribution, status=SeriesStatus.CURRENT
+ )
self.assertEqual(
- 'Commercial Subscription: None',
- self.getPickerEntry(distribution).details[1])
+ "Commercial Subscription: None",
+ self.getPickerEntry(distribution).details[1],
+ )
def test_provides_commercial_subscription_active(self):
distribution = self.factory.makeDistribution()
self.factory.makeDistroSeries(
- distribution=distribution, status=SeriesStatus.CURRENT)
+ distribution=distribution, status=SeriesStatus.CURRENT
+ )
self.factory.makeCommercialSubscription(distribution)
self.assertEqual(
- 'Commercial Subscription: Active',
- self.getPickerEntry(distribution).details[1])
+ "Commercial Subscription: Active",
+ self.getPickerEntry(distribution).details[1],
+ )
def test_provides_commercial_subscription_expired(self):
distribution = self.factory.makeDistribution()
self.factory.makeDistroSeries(
- distribution=distribution, status=SeriesStatus.CURRENT)
+ distribution=distribution, status=SeriesStatus.CURRENT
+ )
self.factory.makeCommercialSubscription(distribution)
then = datetime(2005, 6, 15, 0, 0, 0, 0, pytz.UTC)
- with celebrity_logged_in('admin'):
+ with celebrity_logged_in("admin"):
distribution.commercial_subscription.date_expires = then
self.assertEqual(
- 'Commercial Subscription: Expired',
- self.getPickerEntry(distribution).details[1])
+ "Commercial Subscription: Expired",
+ self.getPickerEntry(distribution).details[1],
+ )
@implementer(IHugeVocabulary)
@@ -490,12 +520,14 @@ class TestPersonVocabulary:
def searchForTerms(self, query=None, vocab_filter=None):
if vocab_filter is None:
- filter_term = ''
+ filter_term = ""
else:
filter_term = vocab_filter.filter_terms[0]
found = [
- person for person in self.test_persons
- if query in person.name and filter_term in person.name]
+ person
+ for person in self.test_persons
+ if query in person.name and filter_term in person.name
+ ]
return CountableIterator(len(found), found, self.toTerm)
@@ -503,11 +535,11 @@ class TestVocabularyFilter(VocabularyFilter):
# A filter returning all objects.
def __new__(cls):
- return super().__new__(cls, 'FILTER', 'Test Filter', 'Test')
+ return super().__new__(cls, "FILTER", "Test Filter", "Test")
@property
def filter_terms(self):
- return ['xpting-person']
+ return ["xpting-person"]
class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
@@ -519,13 +551,18 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
test_persons = []
for name in range(1, 7):
test_persons.append(
- self.factory.makePerson(name='pting-%s' % name))
+ self.factory.makePerson(name="pting-%s" % name)
+ )
TestPersonVocabulary.setTestData(test_persons)
getSiteManager().registerUtility(
- TestPersonVocabulary, IVocabularyFactory, 'TestPerson')
+ TestPersonVocabulary, IVocabularyFactory, "TestPerson"
+ )
self.addCleanup(
getSiteManager().unregisterUtility,
- TestPersonVocabulary, IVocabularyFactory, 'TestPerson')
+ TestPersonVocabulary,
+ IVocabularyFactory,
+ "TestPerson",
+ )
self.addCleanup(TestPersonVocabulary.setTestData, [])
@staticmethod
@@ -534,136 +571,151 @@ class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
context = getUtility(ILaunchpadRoot)
query_string = urlencode(form)
return create_view(
- context, '+huge-vocabulary', form=form, query_string=query_string)
+ context, "+huge-vocabulary", form=form, query_string=query_string
+ )
def test_name_field_missing_error(self):
view = self.create_vocabulary_view({})
self.assertRaisesWithContent(
- MissingInputError, "('name', '', None)", view.__call__)
+ MissingInputError, "('name', '', None)", view.__call__
+ )
def test_search_text_field_missing_error(self):
- view = self.create_vocabulary_view({'name': 'TestPerson'})
+ view = self.create_vocabulary_view({"name": "TestPerson"})
self.assertRaisesWithContent(
- MissingInputError, "('search_text', '', None)", view.__call__)
+ MissingInputError, "('search_text', '', None)", view.__call__
+ )
def test_vocabulary_name_unknown_error(self):
- form = dict(name='snarf', search_text='pting')
+ form = dict(name="snarf", search_text="pting")
view = self.create_vocabulary_view(form)
self.assertRaisesWithContent(
- UnexpectedFormData, "Unknown vocabulary 'snarf'", view.__call__)
+ UnexpectedFormData, "Unknown vocabulary 'snarf'", view.__call__
+ )
def test_json_entries(self):
# The results are JSON encoded.
team = self.factory.makeTeam(
- name='xpting-team',
- membership_policy=TeamMembershipPolicy.RESTRICTED)
- person = self.factory.makePerson(name='xpting-person')
- creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, pytz.timezone('UTC'))
+ name="xpting-team",
+ membership_policy=TeamMembershipPolicy.RESTRICTED,
+ )
+ person = self.factory.makePerson(name="xpting-person")
+ creation_date = datetime(2005, 1, 30, 0, 0, 0, 0, pytz.timezone("UTC"))
removeSecurityProxy(person).datecreated = creation_date
TestPersonVocabulary.test_persons.extend([team, person])
product = self.factory.makeProduct(owner=team)
bugtask = self.factory.makeBugTask(target=product)
- form = dict(name='TestPerson', search_text='xpting')
+ form = dict(name="TestPerson", search_text="xpting")
view = self.create_vocabulary_view(form, context=bugtask)
result = simplejson.loads(view())
- expected = [{
- "alt_title": team.name,
- "alt_title_link": "http://launchpad.test/~%s" % team.name,
- "api_uri": "/~%s" % team.name,
- "badges":
- [{"label": product.displayname,
- "role": "maintainer",
- "url": "/@@/product-badge"},
- {"label": product.displayname,
- "role": "driver",
- "url": "/@@/product-badge"}],
- "css": "sprite team",
- "details": ['Team members: 1'],
- "link_css": "sprite new-window",
- "metadata": "team",
- "title": team.displayname,
- "value": team.name
+ expected = [
+ {
+ "alt_title": team.name,
+ "alt_title_link": "http://launchpad.test/~%s" % team.name,
+ "api_uri": "/~%s" % team.name,
+ "badges": [
+ {
+ "label": product.displayname,
+ "role": "maintainer",
+ "url": "/@@/product-badge",
+ },
+ {
+ "label": product.displayname,
+ "role": "driver",
+ "url": "/@@/product-badge",
+ },
+ ],
+ "css": "sprite team",
+ "details": ["Team members: 1"],
+ "link_css": "sprite new-window",
+ "metadata": "team",
+ "title": team.displayname,
+ "value": team.name,
},
{
- "alt_title": person.name,
- "alt_title_link": "http://launchpad.test/~%s" % person.name,
- "api_uri": "/~%s" % person.name,
- "css": "sprite person",
- "description": "<email address hidden>",
- "details": ['Member since 2005-01-30'],
- "link_css": "sprite new-window",
- "metadata": "person",
- "title": person.displayname,
- "value": person.name
- }]
- self.assertTrue('entries' in result)
+ "alt_title": person.name,
+ "alt_title_link": "http://launchpad.test/~%s" % person.name,
+ "api_uri": "/~%s" % person.name,
+ "css": "sprite person",
+ "description": "<email address hidden>",
+ "details": ["Member since 2005-01-30"],
+ "link_css": "sprite new-window",
+ "metadata": "person",
+ "title": person.displayname,
+ "value": person.name,
+ },
+ ]
+ self.assertTrue("entries" in result)
self.assertContentEqual(
- expected[0].items(), result['entries'][0].items())
+ expected[0].items(), result["entries"][0].items()
+ )
self.assertContentEqual(
- expected[1].items(), result['entries'][1].items())
+ expected[1].items(), result["entries"][1].items()
+ )
def test_vocab_filter(self):
# The vocab filter is used to filter results.
team = self.factory.makeTeam(
- name='xpting-team',
- membership_policy=TeamMembershipPolicy.RESTRICTED)
- person = self.factory.makePerson(name='xpting-person')
+ name="xpting-team",
+ membership_policy=TeamMembershipPolicy.RESTRICTED,
+ )
+ person = self.factory.makePerson(name="xpting-person")
TestPersonVocabulary.test_persons.extend([team, person])
product = self.factory.makeProduct(owner=team)
vocab_filter = TestVocabularyFilter()
- form = dict(name='TestPerson',
- search_text='xpting', search_filter=vocab_filter)
+ form = dict(
+ name="TestPerson", search_text="xpting", search_filter=vocab_filter
+ )
view = self.create_vocabulary_view(form, context=product)
result = simplejson.loads(view())
- entries = result['entries']
+ entries = result["entries"]
self.assertEqual(1, len(entries))
- self.assertEqual('xpting-person', entries[0]['value'])
+ self.assertEqual("xpting-person", entries[0]["value"])
def test_max_description_size(self):
# Descriptions over 120 characters are truncated and ellipsised.
- email = 'pting-' * 19 + '@example.dom'
- person = self.factory.makePerson(name='pting-n', email=email)
+ email = "pting-" * 19 + "@example.dom"
+ person = self.factory.makePerson(name="pting-n", email=email)
TestPersonVocabulary.test_persons.append(person)
# Login to gain permission to know the email address that used
# for the description
login_person(person)
- form = dict(name='TestPerson', search_text='pting-n')
+ form = dict(name="TestPerson", search_text="pting-n")
view = self.create_vocabulary_view(form)
result = simplejson.loads(view())
- expected = (email[:MAX_DESCRIPTION_LENGTH - 3] + '...')
- self.assertEqual(
- 'pting-n', result['entries'][0]['value'])
- self.assertEqual(
- expected, result['entries'][0]['description'])
+ expected = email[: MAX_DESCRIPTION_LENGTH - 3] + "..."
+ self.assertEqual("pting-n", result["entries"][0]["value"])
+ self.assertEqual(expected, result["entries"][0]["description"])
def test_default_batch_size(self):
# The results are batched.
- form = dict(name='TestPerson', search_text='pting')
+ form = dict(name="TestPerson", search_text="pting")
view = self.create_vocabulary_view(form)
result = simplejson.loads(view())
- total_size = result['total_size']
- entries = len(result['entries'])
+ total_size = result["total_size"]
+ entries = len(result["entries"])
self.assertTrue(
total_size > entries,
- 'total_size: %d is less than entries: %d' % (total_size, entries))
+ "total_size: %d is less than entries: %d" % (total_size, entries),
+ )
def test_batch_size(self):
# A The batch size can be specified with the batch param.
form = dict(
- name='TestPerson', search_text='pting',
- start='0', batch='1')
+ name="TestPerson", search_text="pting", start="0", batch="1"
+ )
view = self.create_vocabulary_view(form)
result = simplejson.loads(view())
- self.assertEqual(6, result['total_size'])
- self.assertEqual(1, len(result['entries']))
+ self.assertEqual(6, result["total_size"])
+ self.assertEqual(1, len(result["entries"]))
def test_start_offset(self):
# The offset of the batch is specified with the start param.
form = dict(
- name='TestPerson', search_text='pting',
- start='1', batch='1')
+ name="TestPerson", search_text="pting", start="1", batch="1"
+ )
view = self.create_vocabulary_view(form)
result = simplejson.loads(view())
- self.assertEqual(6, result['total_size'])
- self.assertEqual(1, len(result['entries']))
- self.assertEqual('pting-2', result['entries'][0]['value'])
+ self.assertEqual(6, result["total_size"])
+ self.assertEqual(1, len(result["entries"]))
+ self.assertEqual("pting-2", result["entries"][0]["value"])
diff --git a/lib/lp/app/browser/tests/test_webservice.py b/lib/lp/app/browser/tests/test_webservice.py
index 7af01a7..2f3850d 100644
--- a/lib/lp/app/browser/tests/test_webservice.py
+++ b/lib/lp/app/browser/tests/test_webservice.py
@@ -23,28 +23,31 @@ class TestXHTMLRepresentations(TestCaseWithFactory):
eric = self.factory.makePerson()
# We need something that has an IPersonChoice, a project will do.
product = self.factory.makeProduct(owner=eric)
- field = IProduct['owner']
+ field = IProduct["owner"]
request = get_current_web_service_request()
renderer = getMultiAdapter(
- (product, field, request), IFieldHTMLRenderer)
+ (product, field, request), IFieldHTMLRenderer
+ )
# The representation of a person is the same as a tales
# PersonFormatter.
self.assertEqual(format_link(eric), renderer(eric))
def test_text(self):
# Test the XHTML representation of a text field.
- text = '\N{SNOWMAN} snowman@xxxxxxxxxxx bug 1'
+ text = "\N{SNOWMAN} snowman@xxxxxxxxxxx bug 1"
# We need something that has an IPersonChoice, a project will do.
product = self.factory.makeProduct()
- field = IProduct['description']
+ field = IProduct["description"]
request = get_current_web_service_request()
renderer = getMultiAdapter(
- (product, field, request), IFieldHTMLRenderer)
+ (product, field, request), IFieldHTMLRenderer
+ )
# The representation is linkified html.
self.assertEqual(
- '<p>\N{SNOWMAN} snowman@xxxxxxxxxxx '
+ "<p>\N{SNOWMAN} snowman@xxxxxxxxxxx "
'<a href="/bugs/1" class="bug-link">bug 1</a></p>',
- renderer(text))
+ renderer(text),
+ )
class BaseMissingObjectWebService:
@@ -56,123 +59,126 @@ class BaseMissingObjectWebService:
def test_object_not_found(self):
"""Missing top-level objects generate 404s but not OOPS."""
webservice = LaunchpadWebServiceCaller(
- 'launchpad-library', 'salgado-change-anything')
- response = webservice.get('/%s/123456789' % self.object_type)
+ "launchpad-library", "salgado-change-anything"
+ )
+ response = webservice.get("/%s/123456789" % self.object_type)
self.assertEqual(response.status, 404)
- self.assertEqual(response.getheader('x-lazr-oopsid'), None)
+ self.assertEqual(response.getheader("x-lazr-oopsid"), None)
class TestMissingBranches(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice branches requests."""
- object_type = 'branches'
+ object_type = "branches"
-class TestMissingBugTrackers(
- BaseMissingObjectWebService, TestCaseWithFactory):
+class TestMissingBugTrackers(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice bug_trackers requests."""
- object_type = 'bug_trackers'
+ object_type = "bug_trackers"
class TestMissingBugs(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice bugs requests."""
- object_type = 'bugs'
+ object_type = "bugs"
class TestMissingBuilders(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice builders requests."""
- object_type = 'builders'
+ object_type = "builders"
class TestMissingCountries(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice countries requests."""
- object_type = 'countries'
+ object_type = "countries"
class TestMissingCves(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice cves requests."""
- object_type = 'cves'
+ object_type = "cves"
class TestMissingDistributions(
- BaseMissingObjectWebService, TestCaseWithFactory):
+ BaseMissingObjectWebService, TestCaseWithFactory
+):
"""Test NotFound for webservice distributions requests."""
- object_type = 'distributions'
+ object_type = "distributions"
class TestMissingLanguages(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice launguages requests."""
- object_type = 'languages'
+ object_type = "languages"
class TestMissingLiveFSes(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice livefses requests."""
- object_type = 'livefses'
+ object_type = "livefses"
-class TestMissingPackagesets(
- BaseMissingObjectWebService, TestCaseWithFactory):
+class TestMissingPackagesets(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice packagesets requests."""
- object_type = 'packagesets'
+ object_type = "packagesets"
class TestMissingPeople(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice branches requests."""
- object_type = 'people'
+ object_type = "people"
class TestMissingProjectGroups(
- BaseMissingObjectWebService, TestCaseWithFactory):
+ BaseMissingObjectWebService, TestCaseWithFactory
+):
"""Test NotFound for webservice project_groups requests."""
- object_type = 'project_groups'
+ object_type = "project_groups"
class TestMissingProjects(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice projects requests."""
- object_type = 'projects'
+ object_type = "projects"
class TestMissingQuestions(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice questions requests."""
- object_type = 'questions'
+ object_type = "questions"
class TestMissingSnaps(BaseMissingObjectWebService, TestCaseWithFactory):
"""Test NotFound for webservice snaps requests."""
- object_type = '+snaps'
+ object_type = "+snaps"
class TestMissingTemporaryBlobs(
- BaseMissingObjectWebService, TestCaseWithFactory):
+ BaseMissingObjectWebService, TestCaseWithFactory
+):
"""Test NotFound for webservice temporary_blobs requests."""
- object_type = 'temporary_blobs'
+ object_type = "temporary_blobs"
class TestMissingTranslationGroups(
- BaseMissingObjectWebService, TestCaseWithFactory):
+ BaseMissingObjectWebService, TestCaseWithFactory
+):
"""Test NotFound for webservice translation_groups requests."""
- object_type = 'translation_groups'
+ object_type = "translation_groups"
class TestMissingTranslationImportQueueEntries(
- BaseMissingObjectWebService, TestCaseWithFactory):
- """Test NotFound for webservice translation_import_queue_entries requests.
- """
+ BaseMissingObjectWebService, TestCaseWithFactory
+):
+ """Test NotFound for webservice translation_import_queue_entries."""
- object_type = 'translation_import_queue_entries'
+ object_type = "translation_import_queue_entries"
diff --git a/lib/lp/app/browser/vocabulary.py b/lib/lp/app/browser/vocabulary.py
index bf4cdd6..6d11b8a 100644
--- a/lib/lp/app/browser/vocabulary.py
+++ b/lib/lp/app/browser/vocabulary.py
@@ -4,41 +4,35 @@
"""Views which export vocabularies as JSON for widgets."""
__all__ = [
- 'HugeVocabularyJSONView',
- 'IPickerEntrySource',
- 'get_person_picker_entry_metadata',
- 'vocabulary_filters',
- ]
+ "HugeVocabularyJSONView",
+ "IPickerEntrySource",
+ "get_person_picker_entry_metadata",
+ "vocabulary_filters",
+]
+
+# This registers the registry.
+import zope.vocabularyregistry.registry # noqa: F401 # isort: split
-from lazr.restful.interfaces import IWebServiceClientRequest
import simplejson
-from zope.component import (
- adapter,
- getUtility,
- )
+from lazr.restful.interfaces import IWebServiceClientRequest
+from zope.component import adapter, getUtility
from zope.formlib.interfaces import MissingInputError
-from zope.interface import (
- Attribute,
- implementer,
- Interface,
- )
+from zope.interface import Attribute, Interface, implementer
from zope.interface.interfaces import ComponentLookupError
from zope.schema.interfaces import IVocabularyFactory
from zope.security.interfaces import Unauthorized
-# This registers the registry.
-import zope.vocabularyregistry.registry # noqa: F401
from lp.app.browser.tales import (
DateTimeFormatterAPI,
IRCNicknameFormatterAPI,
ObjectImageDisplayAPI,
- )
+)
from lp.app.errors import UnexpectedFormData
from lp.code.interfaces.branch import IBranch
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
- )
+)
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.projectgroup import IProjectGroup
@@ -51,7 +45,6 @@ from lp.services.webapp.publisher import canonical_url
from lp.services.webapp.vocabulary import IHugeVocabulary
from lp.soyuz.interfaces.archive import IArchive
-
# XXX: EdwinGrubbs 2009-07-27 bug=405476
# This limits the output to one line of text, since the sprite class
# cannot clip the background image effectively for vocabulary items
@@ -63,27 +56,38 @@ class IPickerEntry(Interface):
"""Additional fields that the vocabulary doesn't provide.
These fields are needed by the Picker Ajax widget."""
- description = Attribute('Description')
- image = Attribute('Image URL')
- css = Attribute('CSS Class')
- alt_title = Attribute('Alternative title')
- title_link = Attribute('URL used for anchor on title')
- details = Attribute('An optional list of information about the entry')
- alt_title_link = Attribute('URL used for anchor on alt title')
- link_css = Attribute('CSS Class for links')
- badges = Attribute('List of badge img attributes')
- metadata = Attribute('Metadata about the entry')
- target_type = Attribute('Target data for target picker entries.')
+
+ description = Attribute("Description")
+ image = Attribute("Image URL")
+ css = Attribute("CSS Class")
+ alt_title = Attribute("Alternative title")
+ title_link = Attribute("URL used for anchor on title")
+ details = Attribute("An optional list of information about the entry")
+ alt_title_link = Attribute("URL used for anchor on alt title")
+ link_css = Attribute("CSS Class for links")
+ badges = Attribute("List of badge img attributes")
+ metadata = Attribute("Metadata about the entry")
+ target_type = Attribute("Target data for target picker entries.")
@implementer(IPickerEntry)
class PickerEntry:
"""See `IPickerEntry`."""
- def __init__(self, description=None, image=None, css=None, alt_title=None,
- title_link=None, details=None, alt_title_link=None,
- link_css='sprite new-window', badges=None, metadata=None,
- target_type=None):
+ def __init__(
+ self,
+ description=None,
+ image=None,
+ css=None,
+ alt_title=None,
+ title_link=None,
+ details=None,
+ alt_title_link=None,
+ link_css="sprite new-window",
+ badges=None,
+ metadata=None,
+ target_type=None,
+ ):
self.description = description
self.image = image
self.css = css
@@ -124,11 +128,11 @@ class DefaultPickerEntrySourceAdapter:
entries = []
for term_value in term_values:
extra = PickerEntry()
- if hasattr(term_value, 'summary'):
+ if hasattr(term_value, "summary"):
extra.description = term_value.summary
display_api = ObjectImageDisplayAPI(term_value)
image_url = display_api.custom_icon_url() or None
- css = display_api.sprite_css() or 'sprite bullet'
+ css = display_api.sprite_css() or "sprite bullet"
if image_url is not None:
extra.image = image_url
else:
@@ -161,21 +165,24 @@ class PersonPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
picker_entry.badges = []
for badge_info in badges:
picker_entry.badges.append(
- dict(url=badge_info.url,
- label=badge_info.label,
- role=badge_info.role))
+ dict(
+ url=badge_info.url,
+ label=badge_info.label,
+ role=badge_info.role,
+ )
+ )
for person, picker_entry in zip(term_values, picker_entries):
picker_entry.details = []
if person.preferredemail is not None:
if person.hide_email_addresses:
- picker_entry.description = '<email address hidden>'
+ picker_entry.description = "<email address hidden>"
else:
try:
picker_entry.description = person.preferredemail.email
except Unauthorized:
- picker_entry.description = '<email address hidden>'
+ picker_entry.description = "<email address hidden>"
picker_entry.metadata = get_person_picker_entry_metadata(person)
# We will display the person's name (launchpad id) after their
@@ -184,23 +191,29 @@ class PersonPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
# We will linkify the person's name so it can be clicked to
# open the page for that person.
picker_entry.alt_title_link = canonical_url(
- person, rootsite='mainsite')
+ person, rootsite="mainsite"
+ )
# We will display the person's irc nick(s) after their email
# address in the description text.
irc_nicks = None
if person.ircnicknames:
irc_nicks = ", ".join(
- [IRCNicknameFormatterAPI(ircid).displayname()
- for ircid in person.ircnicknames])
+ [
+ IRCNicknameFormatterAPI(ircid).displayname()
+ for ircid in person.ircnicknames
+ ]
+ )
if irc_nicks:
picker_entry.details.append(irc_nicks)
if person.is_team:
picker_entry.details.append(
- 'Team members: %s' % person.all_member_count)
+ "Team members: %s" % person.all_member_count
+ )
else:
picker_entry.details.append(
- 'Member since %s' % DateTimeFormatterAPI(
- person.datecreated).date())
+ "Member since %s"
+ % DateTimeFormatterAPI(person.datecreated).date()
+ )
return picker_entries
@@ -210,8 +223,9 @@ class BranchPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
def getPickerEntries(self, term_values, context_object, **kwarg):
"""See `IPickerEntrySource`"""
- entries = (
- super().getPickerEntries(term_values, context_object, **kwarg))
+ entries = super().getPickerEntries(
+ term_values, context_object, **kwarg
+ )
for branch, picker_entry in zip(term_values, entries):
picker_entry.description = branch.bzr_identity
return entries
@@ -236,60 +250,66 @@ class TargetPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
def getPickerEntries(self, term_values, context_object, **kwarg):
"""See `IPickerEntrySource`"""
- entries = (
- super().getPickerEntries(term_values, context_object, **kwarg))
+ entries = super().getPickerEntries(
+ term_values, context_object, **kwarg
+ )
for target, picker_entry in zip(term_values, entries):
picker_entry.description = self.getDescription(target)
picker_entry.details = []
summary = picker_entry.description
if len(summary) > 45:
- index = summary.rfind(' ', 0, 45)
- first_line = summary[0:index + 1]
+ index = summary.rfind(" ", 0, 45)
+ first_line = summary[0 : index + 1]
second_line = summary[index:]
else:
first_line = summary
- second_line = ''
+ second_line = ""
if len(second_line) > 90:
- index = second_line.rfind(' ', 0, 90)
- second_line = second_line[0:index + 1]
+ index = second_line.rfind(" ", 0, 90)
+ second_line = second_line[0 : index + 1]
picker_entry.description = first_line
if second_line:
picker_entry.details.append(second_line)
picker_entry.alt_title = target.name
picker_entry.alt_title_link = canonical_url(
- target, rootsite='mainsite')
+ target, rootsite="mainsite"
+ )
picker_entry.target_type = self.target_type
maintainer = self.getMaintainer(target)
if maintainer is not None:
- picker_entry.details.append(
- 'Maintainer: %s' % maintainer)
+ picker_entry.details.append("Maintainer: %s" % maintainer)
commercial_subscription = self.getCommercialSubscription(target)
if commercial_subscription is not None:
picker_entry.details.append(
- 'Commercial Subscription: %s' % commercial_subscription)
+ "Commercial Subscription: %s" % commercial_subscription
+ )
return entries
@adapter(ISourcePackageName)
class SourcePackageNamePickerEntrySourceAdapter(
- DefaultPickerEntrySourceAdapter):
+ DefaultPickerEntrySourceAdapter
+):
"""Adapts ISourcePackageName to IPickerEntrySource."""
def getPickerEntries(self, term_values, context_object, **kwarg):
"""See `IPickerEntrySource`"""
- entries = (
- super().getPickerEntries(term_values, context_object, **kwarg))
+ entries = super().getPickerEntries(
+ term_values, context_object, **kwarg
+ )
for sourcepackagename, picker_entry in zip(term_values, entries):
descriptions = getSourcePackageDescriptions([sourcepackagename])
picker_entry.description = descriptions.get(
- sourcepackagename.name, "Not yet built")
+ sourcepackagename.name, "Not yet built"
+ )
return entries
@adapter(IDistributionSourcePackage)
class DistributionSourcePackagePickerEntrySourceAdapter(
- TargetPickerEntrySourceAdapter):
+ TargetPickerEntrySourceAdapter
+):
"""Adapts IDistributionSourcePackage to IPickerEntrySource."""
target_type = "package"
@@ -301,9 +321,9 @@ class DistributionSourcePackagePickerEntrySourceAdapter(
def getDescription(self, target):
"""See `TargetPickerEntrySource`"""
if target.binary_names:
- description = ', '.join(target.binary_names)
+ description = ", ".join(target.binary_names)
else:
- description = 'Not yet built.'
+ description = "Not yet built."
return description
def getPickerEntries(self, term_values, context_object, **kwarg):
@@ -347,11 +367,11 @@ class ProductPickerEntrySourceAdapter(TargetPickerEntrySourceAdapter):
"""See `TargetPickerEntrySource`"""
if target.commercial_subscription:
if target.has_current_commercial_subscription:
- return 'Active'
+ return "Active"
else:
- return 'Expired'
+ return "Expired"
else:
- return 'None'
+ return "None"
@adapter(IDistribution)
@@ -374,11 +394,11 @@ class DistributionPickerEntrySourceAdapter(TargetPickerEntrySourceAdapter):
"""See `TargetPickerEntrySource`"""
if target.commercial_subscription:
if target.has_current_commercial_subscription:
- return 'Active'
+ return "Active"
else:
- return 'Expired'
+ return "Expired"
else:
- return 'None'
+ return "None"
@adapter(IArchive)
@@ -387,8 +407,9 @@ class ArchivePickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
def getPickerEntries(self, term_values, context_object, **kwarg):
"""See `IPickerEntrySource`"""
- entries = (
- super().getPickerEntries(term_values, context_object, **kwarg))
+ entries = super().getPickerEntries(
+ term_values, context_object, **kwarg
+ )
for archive, picker_entry in zip(term_values, entries):
picker_entry.description = archive.reference
return entries
@@ -400,6 +421,7 @@ class HugeVocabularyJSONView:
This was needed by the Picker widget, but could be
useful for other AJAX widgets.
"""
+
DEFAULT_BATCH_SIZE = 10
def __init__(self, context, request):
@@ -407,20 +429,19 @@ class HugeVocabularyJSONView:
self.request = request
def __call__(self):
- name = self.request.form.get('name')
+ name = self.request.form.get("name")
if name is None:
- raise MissingInputError('name', '')
+ raise MissingInputError("name", "")
- search_text = self.request.form.get('search_text')
+ search_text = self.request.form.get("search_text")
if search_text is None:
- raise MissingInputError('search_text', '')
- search_filter = self.request.form.get('search_filter')
+ raise MissingInputError("search_text", "")
+ search_filter = self.request.form.get("search_filter")
try:
factory = getUtility(IVocabularyFactory, name)
except ComponentLookupError:
- raise UnexpectedFormData(
- 'Unknown vocabulary %r' % name)
+ raise UnexpectedFormData("Unknown vocabulary %r" % name)
vocabulary = factory(self.context)
@@ -460,8 +481,8 @@ class HugeVocabularyJSONView:
# corresponding picker entries by calling the adapter.
for adapter_class, term_values in picker_entry_terms.items():
picker_entries = adapter_cache[adapter_class].getPickerEntries(
- term_values,
- self.context)
+ term_values, self.context
+ )
for term_value, picker_entry in zip(term_values, picker_entries):
picker_term_entries[term_value] = picker_entry
@@ -472,45 +493,46 @@ class HugeVocabularyJSONView:
# be passed directly into the REST PATCH call.
api_request = IWebServiceClientRequest(self.request)
try:
- entry['api_uri'] = canonical_url(
- term.value, request=api_request,
- path_only_if_possible=True)
+ entry["api_uri"] = canonical_url(
+ term.value, request=api_request, path_only_if_possible=True
+ )
except NoCanonicalUrl:
# The exception is caught, because the api_url is only
# needed for inplace editing via a REST call. The
# form picker doesn't need the api_url.
- entry['api_uri'] = 'Could not find canonical url.'
+ entry["api_uri"] = "Could not find canonical url."
picker_entry = picker_term_entries[term.value]
if picker_entry.description is not None:
if len(picker_entry.description) > MAX_DESCRIPTION_LENGTH:
- entry['description'] = (
- picker_entry.description[:MAX_DESCRIPTION_LENGTH - 3]
- + '...')
+ entry["description"] = (
+ picker_entry.description[: MAX_DESCRIPTION_LENGTH - 3]
+ + "..."
+ )
else:
- entry['description'] = picker_entry.description
+ entry["description"] = picker_entry.description
if picker_entry.image is not None:
- entry['image'] = picker_entry.image
+ entry["image"] = picker_entry.image
if picker_entry.css is not None:
- entry['css'] = picker_entry.css
+ entry["css"] = picker_entry.css
if picker_entry.alt_title is not None:
- entry['alt_title'] = picker_entry.alt_title
+ entry["alt_title"] = picker_entry.alt_title
if picker_entry.title_link is not None:
- entry['title_link'] = picker_entry.title_link
+ entry["title_link"] = picker_entry.title_link
if picker_entry.details is not None:
- entry['details'] = picker_entry.details
+ entry["details"] = picker_entry.details
if picker_entry.alt_title_link is not None:
- entry['alt_title_link'] = picker_entry.alt_title_link
+ entry["alt_title_link"] = picker_entry.alt_title_link
if picker_entry.link_css is not None:
- entry['link_css'] = picker_entry.link_css
+ entry["link_css"] = picker_entry.link_css
if picker_entry.badges:
- entry['badges'] = picker_entry.badges
+ entry["badges"] = picker_entry.badges
if picker_entry.metadata is not None:
- entry['metadata'] = picker_entry.metadata
+ entry["metadata"] = picker_entry.metadata
if picker_entry.target_type is not None:
- entry['target_type'] = picker_entry.target_type
+ entry["target_type"] = picker_entry.target_type
result.append(entry)
- self.request.response.setHeader('Content-type', 'application/json')
+ self.request.response.setHeader("Content-type", "application/json")
return simplejson.dumps(dict(total_size=total_size, entries=result))
@@ -522,14 +544,16 @@ def vocabulary_filters(vocabulary):
# If we have no filters or just the ALL filter, then no filtering
# support is required.
filters = []
- if (len(supported_filters) == 0 or
- (len(supported_filters) == 1
- and supported_filters[0].name == 'ALL')):
+ if len(supported_filters) == 0 or (
+ len(supported_filters) == 1 and supported_filters[0].name == "ALL"
+ ):
return filters
for filter in supported_filters:
- filters.append({
- 'name': filter.name,
- 'title': filter.title,
- 'description': filter.description,
- })
+ filters.append(
+ {
+ "name": filter.name,
+ "title": filter.title,
+ "description": filter.description,
+ }
+ )
return filters
diff --git a/lib/lp/app/browser/webservice.py b/lib/lp/app/browser/webservice.py
index a0cb712..8439f7e 100644
--- a/lib/lp/app/browser/webservice.py
+++ b/lib/lp/app/browser/webservice.py
@@ -9,12 +9,9 @@ from lazr.restful.interfaces import (
IFieldHTMLRenderer,
IReference,
IWebServiceClientRequest,
- )
+)
from zope import component
-from zope.interface import (
- implementer,
- Interface,
- )
+from zope.interface import Interface, implementer
from zope.schema.interfaces import IText
from lp.app.browser.stringformatter import FormattersAPI
@@ -30,9 +27,10 @@ def reference_xhtml_representation(context, field, request):
# The value is a webservice link to the object, we want field value.
obj = getattr(context, field.__name__, None)
try:
- return format_link(obj, empty_value='')
+ return format_link(obj, empty_value="")
except NotImplementedError:
return value
+
return render
@@ -41,5 +39,7 @@ def reference_xhtml_representation(context, field, request):
def text_xhtml_representation(context, field, request):
"""Render text as XHTML using the webservice."""
return lambda text: (
- '' if text is None
- else FormattersAPI(text).text_to_html(linkify_text=True))
+ ""
+ if text is None
+ else FormattersAPI(text).text_to_html(linkify_text=True)
+ )
diff --git a/lib/lp/app/enums.py b/lib/lp/app/enums.py
index c9ebe8f..32cf371 100644
--- a/lib/lp/app/enums.py
+++ b/lib/lp/app/enums.py
@@ -4,24 +4,21 @@
"""Enumerations and related utilities used in the lp/app modules."""
__all__ = [
- 'FREE_INFORMATION_TYPES',
- 'FREE_PRIVATE_INFORMATION_TYPES',
- 'InformationType',
- 'NON_EMBARGOED_INFORMATION_TYPES',
- 'PRIVATE_INFORMATION_TYPES',
- 'PROPRIETARY_INFORMATION_TYPES',
- 'PILLAR_INFORMATION_TYPES',
- 'PUBLIC_INFORMATION_TYPES',
- 'PUBLIC_PROPRIETARY_INFORMATION_TYPES',
- 'SECURITY_INFORMATION_TYPES',
- 'ServiceUsage',
- 'service_uses_launchpad',
- ]
-
-from lazr.enum import (
- DBEnumeratedType,
- DBItem,
- )
+ "FREE_INFORMATION_TYPES",
+ "FREE_PRIVATE_INFORMATION_TYPES",
+ "InformationType",
+ "NON_EMBARGOED_INFORMATION_TYPES",
+ "PRIVATE_INFORMATION_TYPES",
+ "PROPRIETARY_INFORMATION_TYPES",
+ "PILLAR_INFORMATION_TYPES",
+ "PUBLIC_INFORMATION_TYPES",
+ "PUBLIC_PROPRIETARY_INFORMATION_TYPES",
+ "SECURITY_INFORMATION_TYPES",
+ "ServiceUsage",
+ "service_uses_launchpad",
+]
+
+from lazr.enum import DBEnumeratedType, DBItem
class InformationType(DBEnumeratedType):
@@ -31,73 +28,107 @@ class InformationType(DBEnumeratedType):
Launchpad artifacts, including bugs and branches.
"""
- PUBLIC = DBItem(1, """
+ PUBLIC = DBItem(
+ 1,
+ """
Public
Everyone can see this information.
- """)
+ """,
+ )
- PUBLICSECURITY = DBItem(2, """
+ PUBLICSECURITY = DBItem(
+ 2,
+ """
Public Security
Everyone can see this security related information.
- """)
+ """,
+ )
- PRIVATESECURITY = DBItem(3, """
+ PRIVATESECURITY = DBItem(
+ 3,
+ """
Private Security
Only the security group can see this information.
- """)
+ """,
+ )
- USERDATA = DBItem(4, """
+ USERDATA = DBItem(
+ 4,
+ """
Private
Only shared with users permitted to see private user information.
- """)
+ """,
+ )
- PROPRIETARY = DBItem(5, """
+ PROPRIETARY = DBItem(
+ 5,
+ """
Proprietary
Only shared with users permitted to see proprietary information.
- """)
+ """,
+ )
- EMBARGOED = DBItem(6, """
+ EMBARGOED = DBItem(
+ 6,
+ """
Embargoed
Only shared with users permitted to see embargoed information.
- """)
+ """,
+ )
+
PUBLIC_INFORMATION_TYPES = (
- InformationType.PUBLIC, InformationType.PUBLICSECURITY)
+ InformationType.PUBLIC,
+ InformationType.PUBLICSECURITY,
+)
PRIVATE_INFORMATION_TYPES = (
- InformationType.PRIVATESECURITY, InformationType.USERDATA,
- InformationType.PROPRIETARY, InformationType.EMBARGOED)
+ InformationType.PRIVATESECURITY,
+ InformationType.USERDATA,
+ InformationType.PROPRIETARY,
+ InformationType.EMBARGOED,
+)
-NON_EMBARGOED_INFORMATION_TYPES = (
- PUBLIC_INFORMATION_TYPES +
- (InformationType.PRIVATESECURITY, InformationType.USERDATA,
- InformationType.PROPRIETARY))
+NON_EMBARGOED_INFORMATION_TYPES = PUBLIC_INFORMATION_TYPES + (
+ InformationType.PRIVATESECURITY,
+ InformationType.USERDATA,
+ InformationType.PROPRIETARY,
+)
SECURITY_INFORMATION_TYPES = (
- InformationType.PUBLICSECURITY, InformationType.PRIVATESECURITY)
+ InformationType.PUBLICSECURITY,
+ InformationType.PRIVATESECURITY,
+)
FREE_PRIVATE_INFORMATION_TYPES = (
- InformationType.PRIVATESECURITY, InformationType.USERDATA)
+ InformationType.PRIVATESECURITY,
+ InformationType.USERDATA,
+)
FREE_INFORMATION_TYPES = (
- PUBLIC_INFORMATION_TYPES + FREE_PRIVATE_INFORMATION_TYPES)
+ PUBLIC_INFORMATION_TYPES + FREE_PRIVATE_INFORMATION_TYPES
+)
PROPRIETARY_INFORMATION_TYPES = (
- InformationType.PROPRIETARY, InformationType.EMBARGOED)
+ InformationType.PROPRIETARY,
+ InformationType.EMBARGOED,
+)
# The information types unrelated to user data or security
PUBLIC_PROPRIETARY_INFORMATION_TYPES = (
- (InformationType.PUBLIC,) + PROPRIETARY_INFORMATION_TYPES
-)
+ InformationType.PUBLIC,
+) + PROPRIETARY_INFORMATION_TYPES
PILLAR_INFORMATION_TYPES = (
- InformationType.PUBLIC, InformationType.PROPRIETARY)
+ InformationType.PUBLIC,
+ InformationType.PROPRIETARY,
+)
class ServiceUsage(DBEnumeratedType):
@@ -107,30 +138,42 @@ class ServiceUsage(DBEnumeratedType):
bug tracking, translations, code hosting, blueprint, and answers.
"""
- UNKNOWN = DBItem(10, """
+ UNKNOWN = DBItem(
+ 10,
+ """
Unknown
The maintainers have not indicated usage. This value is the default for
new pillars.
- """)
+ """,
+ )
- LAUNCHPAD = DBItem(20, """
+ LAUNCHPAD = DBItem(
+ 20,
+ """
Launchpad
Launchpad is used to provide this service.
- """)
+ """,
+ )
- EXTERNAL = DBItem(30, """
+ EXTERNAL = DBItem(
+ 30,
+ """
External
The service is provided external to Launchpad.
- """)
+ """,
+ )
- NOT_APPLICABLE = DBItem(40, """
+ NOT_APPLICABLE = DBItem(
+ 40,
+ """
Not Applicable
The pillar does not use this type of service in Launchpad or externally.
- """)
+ """,
+ )
def service_uses_launchpad(usage_enum):
diff --git a/lib/lp/app/errors.py b/lib/lp/app/errors.py
index 9c16fa0..f3e5c91 100644
--- a/lib/lp/app/errors.py
+++ b/lib/lp/app/errors.py
@@ -4,26 +4,23 @@
"""Cross application type errors for launchpad."""
__all__ = [
- 'GoneError',
- 'IncompatibleArguments',
- 'NameLookupFailed',
- 'NotFoundError',
- 'POSTToNonCanonicalURL',
- 'ServiceUsageForbidden',
- 'SubscriptionPrivacyViolation',
- 'TeamAccountNotClosable',
- 'TranslationUnavailable',
- 'UnexpectedFormData',
- 'UserCannotUnsubscribePerson',
- ]
+ "GoneError",
+ "IncompatibleArguments",
+ "NameLookupFailed",
+ "NotFoundError",
+ "POSTToNonCanonicalURL",
+ "ServiceUsageForbidden",
+ "SubscriptionPrivacyViolation",
+ "TeamAccountNotClosable",
+ "TranslationUnavailable",
+ "UnexpectedFormData",
+ "UserCannotUnsubscribePerson",
+]
import http.client
from lazr.restful.declarations import error_status
-from zope.security.interfaces import (
- ForbiddenAttribute,
- Unauthorized,
- )
+from zope.security.interfaces import ForbiddenAttribute, Unauthorized
class TranslationUnavailable(Exception):
diff --git a/lib/lp/app/interfaces/headings.py b/lib/lp/app/interfaces/headings.py
index 5c50232..e3ca547 100644
--- a/lib/lp/app/interfaces/headings.py
+++ b/lib/lp/app/interfaces/headings.py
@@ -4,9 +4,9 @@
"""Marker interfaces that define how to generate headings for a view."""
__all__ = [
- 'IHeadingBreadcrumb',
- 'IMajorHeadingView',
- ]
+ "IHeadingBreadcrumb",
+ "IMajorHeadingView",
+]
from zope.interface import Interface
diff --git a/lib/lp/app/interfaces/informationtype.py b/lib/lp/app/interfaces/informationtype.py
index dc100fd..5ea95df 100644
--- a/lib/lp/app/interfaces/informationtype.py
+++ b/lib/lp/app/interfaces/informationtype.py
@@ -2,8 +2,8 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
__all__ = [
- 'IInformationType',
- ]
+ "IInformationType",
+]
from lazr.restful.declarations import exported
from zope.schema import Choice
@@ -15,9 +15,11 @@ from lp.app.interfaces.launchpad import IPrivacy
class IInformationType(IPrivacy):
- information_type = exported(Choice(
- title=_('Information Type'),
- vocabulary=InformationType,
- required=True,
- description=_('The type of data contained in this item.')
- ))
+ information_type = exported(
+ Choice(
+ title=_("Information Type"),
+ vocabulary=InformationType,
+ required=True,
+ description=_("The type of data contained in this item."),
+ )
+ )
diff --git a/lib/lp/app/interfaces/launchpad.py b/lib/lp/app/interfaces/launchpad.py
index b140b6b..767f45a 100644
--- a/lib/lp/app/interfaces/launchpad.py
+++ b/lib/lp/app/interfaces/launchpad.py
@@ -7,25 +7,19 @@ Note that these are not interfaces to application content objects.
"""
__all__ = [
- 'IHasIcon',
- 'IHasLogo',
- 'IHasMugshot',
- 'IHeadingContext',
- 'ILaunchpadCelebrities',
- 'ILaunchpadUsage',
- 'IPrivacy',
- 'IServiceUsage',
- ]
+ "IHasIcon",
+ "IHasLogo",
+ "IHasMugshot",
+ "IHeadingContext",
+ "ILaunchpadCelebrities",
+ "ILaunchpadUsage",
+ "IPrivacy",
+ "IServiceUsage",
+]
from lazr.restful.declarations import exported
-from zope.interface import (
- Attribute,
- Interface,
- )
-from zope.schema import (
- Bool,
- Choice,
- )
+from zope.interface import Attribute, Interface
+from zope.schema import Bool, Choice
from lp import _
from lp.app.enums import ServiceUsage
@@ -36,6 +30,7 @@ class ILaunchpadCelebrities(Interface):
Celebrities are SQLBase instances that have a well known name.
"""
+
admin = Attribute("The 'admins' team.")
software_center_agent = Attribute("The Software Center Agent.")
bug_importer = Attribute("The bug importer.")
@@ -67,8 +62,7 @@ class ILaunchpadCelebrities(Interface):
vcs_imports = Attribute("The 'vcs-imports' team.")
def isCelebrityPerson(name):
- """Return true if there is an IPerson celebrity with the given name.
- """
+ """Return true if there is an IPerson celebrity with the given name."""
def clearCache():
"""Clear any cached celebrities."""
@@ -82,57 +76,74 @@ class IServiceUsage(Interface):
# implies an actual location not an answer of "Launchpad, externally, or
# neither."
answers_usage = Choice(
- title=_('Type of service for answers application'),
+ title=_("Type of service for answers application"),
description=_("Where does this pillar have an Answers forum?"),
default=ServiceUsage.UNKNOWN,
- vocabulary=ServiceUsage)
+ vocabulary=ServiceUsage,
+ )
blueprints_usage = Choice(
- title=_('Type of service for blueprints application'),
+ title=_("Type of service for blueprints application"),
description=_("Where does this pillar host blueprints?"),
default=ServiceUsage.UNKNOWN,
-