launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28486
[Merge] ~cjwatson/launchpad:black-answers-browser into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:black-answers-browser into launchpad:master.
Commit message:
lp.answers.browser: Apply black
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/423202
This puts the necessary structure in place to be able to apply this progressively rather than having to change the whole codebase at once. The required isort configuration is incompatible with unblackened files, so we need to split that pre-commit hook and run it twice with different configuration on different subsets of files.
This is an RFC to see what people on the team think. There are lots of bits of this I don't like - in particular I really dislike dropping force-grid-wrap for `from` imports - but I probably dislike it less than having to have even one more debate about formatting.
If we go ahead with this I expect we'd want to make changes in bigger chunks so that it doesn't take us all year, but I wanted to start with something that fit within Launchpad's code review diff limit so that the effects were properly visible.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:black-answers-browser into launchpad:master.
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 6c1e034..c258618 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -52,3 +52,5 @@ d61c2ad002c2997a132a1580ce6ee82bb03de11d
afcfc15adcf3267d3fd07d7679df00231853c908
# apply pyupgrade --py3-plus to lp.translations
3f3ea0f7799093f93740d3a1db3a11965d0b25cb
+# apply black to lp.answers.browser
+c606443bdb2f342593c9a7c9437cb70c01f85f29
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 80c13da..df17562 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -23,11 +23,6 @@ repos:
exclude: systemdocs\.py
- id: no-commit-to-branch
args: [--branch, master, --branch, db-devel]
-- repo: https://github.com/PyCQA/flake8
- rev: 3.9.2
- hooks:
- - id: flake8
- exclude: ^lib/contrib/
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
hooks:
@@ -40,10 +35,45 @@ repos:
|utilities/community-contributions\.py
|utilities/update-sourcecode
)$
+- repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+ files: |
+ (?x)^lib/lp/(
+ answers/browser
+ )/
- repo: https://github.com/PyCQA/isort
rev: 5.9.2
hooks:
- id: isort
+ name: isort (old-style)
+ args:
+ - --combine-as
+ - --force-grid-wrap=2
+ - --force-sort-within-sections
+ - --trailing-comma
+ - --line-length=78
+ - --lines-after-imports=2
+ - --multi-line=8
+ - --dont-order-by-type
+ exclude: |
+ (?x)^lib/lp/(
+ answers/browser
+ )/
+ - id: isort
+ alias: isort-black
+ name: isort (black)
+ args: [--profile, black]
+ files: |
+ (?x)^lib/lp/(
+ answers/browser
+ )/
+- repo: https://github.com/PyCQA/flake8
+ rev: 3.9.2
+ hooks:
+ - id: flake8
+ exclude: ^lib/contrib/
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v4.2.0
hooks:
diff --git a/lib/lp/answers/browser/faq.py b/lib/lp/answers/browser/faq.py
index dbca64d..732bd2d 100644
--- a/lib/lp/answers/browser/faq.py
+++ b/lib/lp/answers/browser/faq.py
@@ -4,25 +4,22 @@
"""`IFAQ` browser views."""
__all__ = [
- 'FAQBreadcrumb',
- 'FAQNavigationMenu',
- 'FAQEditView',
- 'FAQView',
- ]
+ "FAQBreadcrumb",
+ "FAQNavigationMenu",
+ "FAQEditView",
+ "FAQView",
+]
from lp import _
from lp.answers.interfaces.faq import IFAQ
from lp.answers.interfaces.faqcollection import IFAQCollection
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadEditFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadEditFormView, action
from lp.services.webapp import (
- canonical_url,
- enabled_with_permission,
Link,
NavigationMenu,
- )
+ canonical_url,
+ enabled_with_permission,
+)
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.publisher import LaunchpadView
@@ -31,14 +28,14 @@ class FAQNavigationMenu(NavigationMenu):
"""Context menu of actions that can be performed upon a FAQ."""
usedfor = IFAQ
- title = 'Edit FAQ'
- facet = 'answers'
- links = ['edit', 'list_all']
+ title = "Edit FAQ"
+ facet = "answers"
+ links = ["edit", "list_all"]
- @enabled_with_permission('launchpad.Edit')
+ @enabled_with_permission("launchpad.Edit")
def edit(self):
"""Return a Link to the edit view."""
- return Link('+edit', _('Edit FAQ'), icon='edit')
+ return Link("+edit", _("Edit FAQ"), icon="edit")
def list_all(self):
"""Return a Link to list all FAQs."""
@@ -46,8 +43,8 @@ class FAQNavigationMenu(NavigationMenu):
# on objects which don't provide `IFAQCollection` directly, but for
# which an adapter exists that gives the proper context.
collection = IFAQCollection(self.context)
- url = canonical_url(collection, rootsite='answers') + '/+faqs'
- return Link(url, 'List all FAQs', icon='info')
+ url = canonical_url(collection, rootsite="answers") + "/+faqs"
+ return Link(url, "List all FAQs", icon="info")
class FAQBreadcrumb(Breadcrumb):
@@ -55,7 +52,7 @@ class FAQBreadcrumb(Breadcrumb):
@property
def text(self):
- return 'FAQ #%d' % self.context.id
+ return "FAQ #%d" % self.context.id
class FAQView(LaunchpadView):
@@ -70,14 +67,14 @@ class FAQEditView(LaunchpadEditFormView):
"""View to change the FAQ details."""
schema = IFAQ
- label = _('Edit FAQ')
+ label = _("Edit FAQ")
field_names = ["title", "keywords", "content"]
@property
def page_title(self):
- return 'Edit FAQ #%s details' % self.context.id
+ return "Edit FAQ #%s details" % self.context.id
- @action(_('Save'), name="save")
+ @action(_("Save"), name="save")
def save_action(self, action, data):
"""Update the FAQ details."""
self.updateContextFromData(data)
diff --git a/lib/lp/answers/browser/faqcollection.py b/lib/lp/answers/browser/faqcollection.py
index a2fb420..f00b3b2 100644
--- a/lib/lp/answers/browser/faqcollection.py
+++ b/lib/lp/answers/browser/faqcollection.py
@@ -4,34 +4,23 @@
"""IFAQCollection browser views."""
__all__ = [
- 'FAQCollectionMenu',
- 'SearchFAQsView',
- ]
+ "FAQCollectionMenu",
+ "SearchFAQsView",
+]
from urllib.parse import urlencode
from lp import _
-from lp.answers.enums import (
- QUESTION_STATUS_DEFAULT_SEARCH,
- QuestionSort,
- )
+from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH, QuestionSort
from lp.answers.interfaces.faqcollection import (
FAQSort,
IFAQCollection,
ISearchFAQsForm,
- )
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- safe_action,
- )
+)
+from lp.app.browser.launchpadform import LaunchpadFormView, action, safe_action
from lp.registry.interfaces.projectgroup import IProjectGroup
from lp.services.propertycache import cachedproperty
-from lp.services.webapp import (
- canonical_url,
- Link,
- NavigationMenu,
- )
+from lp.services.webapp import Link, NavigationMenu, canonical_url
from lp.services.webapp.batching import BatchNavigator
from lp.services.webapp.menu import enabled_with_permission
@@ -40,8 +29,8 @@ class FAQCollectionMenu(NavigationMenu):
"""Base menu definition for `IFAQCollection`."""
usedfor = IFAQCollection
- facet = 'answers'
- links = ['list_all', 'create_faq']
+ facet = "answers"
+ links = ["list_all", "create_faq"]
def list_all(self):
"""Return a Link to list all FAQs."""
@@ -49,21 +38,22 @@ class FAQCollectionMenu(NavigationMenu):
# on objects which don't provide `IFAQCollection` directly, but for
# which an adapter exists that gives the proper context.
collection = IFAQCollection(self.context)
- url = canonical_url(collection, rootsite='answers') + '/+faqs'
- return Link(url, 'All FAQs', icon='info')
+ url = canonical_url(collection, rootsite="answers") + "/+faqs"
+ return Link(url, "All FAQs", icon="info")
- @enabled_with_permission('launchpad.Append')
+ @enabled_with_permission("launchpad.Append")
def create_faq(self):
"""Return a Link to create a new FAQ."""
collection = IFAQCollection(self.context)
if IProjectGroup.providedBy(self.context):
- url = ''
+ url = ""
enabled = False
else:
url = canonical_url(
- collection, view_name='+createfaq', rootsite='answers')
+ collection, view_name="+createfaq", rootsite="answers"
+ )
enabled = True
- return Link(url, 'Create a new FAQ', icon='add', enabled=enabled)
+ return Link(url, "Create a new FAQ", icon="add", enabled=enabled)
class SearchFAQsView(LaunchpadFormView):
@@ -82,13 +72,15 @@ class SearchFAQsView(LaunchpadFormView):
def page_title(self):
"""Return the page_title that should be used for the listing."""
replacements = dict(
- displayname=self.context.displayname,
- search_text=self.search_text)
+ displayname=self.context.displayname, search_text=self.search_text
+ )
if self.search_text:
- return _('FAQs matching \u201c${search_text}\u201d for '
- '$displayname', mapping=replacements)
+ return _(
+ "FAQs matching \u201c${search_text}\u201d for " "$displayname",
+ mapping=replacements,
+ )
else:
- return _('FAQs for $displayname', mapping=replacements)
+ return _("FAQs for $displayname", mapping=replacements)
label = page_title
@@ -96,14 +88,18 @@ class SearchFAQsView(LaunchpadFormView):
def empty_listing_message(self):
"""Return the message to render when there are no FAQs to display."""
replacements = dict(
- displayname=self.context.displayname,
- search_text=self.search_text)
+ displayname=self.context.displayname, search_text=self.search_text
+ )
if self.search_text:
- return _('There are no FAQs for $displayname matching '
- '\u201c${search_text}\u201d.', mapping=replacements)
+ return _(
+ "There are no FAQs for $displayname matching "
+ "\u201c${search_text}\u201d.",
+ mapping=replacements,
+ )
else:
- return _('There are no FAQs for $displayname.',
- mapping=replacements)
+ return _(
+ "There are no FAQs for $displayname.", mapping=replacements
+ )
def getMatchingFAQs(self):
"""Return a BatchNavigator of the matching FAQs."""
@@ -114,7 +110,8 @@ class SearchFAQsView(LaunchpadFormView):
def portlet_action(self):
"""The action URL of the portlet form."""
return canonical_url(
- self.context, view_name='+faqs', rootsite='answers')
+ self.context, view_name="+faqs", rootsite="answers"
+ )
@cachedproperty
def latest_faqs(self):
@@ -124,26 +121,38 @@ class SearchFAQsView(LaunchpadFormView):
"""
quantity = 5
faqs = self.context.searchFAQs(
- search_text=self.search_text, sort=FAQSort.NEWEST_FIRST)
+ search_text=self.search_text, sort=FAQSort.NEWEST_FIRST
+ )
return list(faqs[:quantity])
@safe_action
- @action(_('Search'), name='search')
+ @action(_("Search"), name="search")
def search_action(self, action, data):
"""Filter the search results by keywords."""
- self.search_text = data.get('search_text', None)
+ self.search_text = data.get("search_text", None)
if self.search_text:
matching_questions = self.context.searchQuestions(
- search_text=self.search_text)
+ search_text=self.search_text
+ )
self.matching_questions_count = matching_questions.count()
@property
def matching_questions_url(self):
"""Return the URL to the questions matching the same keywords."""
- return canonical_url(self.context) + '/+questions?' + urlencode(
- {'field.status': [
- status.title for status in QUESTION_STATUS_DEFAULT_SEARCH],
- 'field.search_text': self.search_text,
- 'field.actions.search': 'Search',
- 'field.sort': QuestionSort.RELEVANCY.title,
- 'field.language-empty-marker': 1}, doseq=True)
+ return (
+ canonical_url(self.context)
+ + "/+questions?"
+ + urlencode(
+ {
+ "field.status": [
+ status.title
+ for status in QUESTION_STATUS_DEFAULT_SEARCH
+ ],
+ "field.search_text": self.search_text,
+ "field.actions.search": "Search",
+ "field.sort": QuestionSort.RELEVANCY.title,
+ "field.language-empty-marker": 1,
+ },
+ doseq=True,
+ )
+ )
diff --git a/lib/lp/answers/browser/faqtarget.py b/lib/lp/answers/browser/faqtarget.py
index c433007..7f37282 100644
--- a/lib/lp/answers/browser/faqtarget.py
+++ b/lib/lp/answers/browser/faqtarget.py
@@ -4,28 +4,22 @@
"""`IFAQTarget` browser views."""
__all__ = [
- 'FAQTargetNavigationMixin',
- 'FAQCreateView',
- ]
+ "FAQTargetNavigationMixin",
+ "FAQCreateView",
+]
from lp import _
from lp.answers.interfaces.faq import IFAQ
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
from lp.app.errors import NotFoundError
from lp.app.widgets.textwidgets import TokensTextWidget
-from lp.services.webapp import (
- canonical_url,
- stepthrough,
- )
+from lp.services.webapp import canonical_url, stepthrough
class FAQTargetNavigationMixin:
"""Navigation mixin for `IFAQTarget`."""
- @stepthrough('+faq')
+ @stepthrough("+faq")
def traverse_faq(self, name):
"""Return the FAQ by ID."""
try:
@@ -39,19 +33,22 @@ class FAQCreateView(LaunchpadFormView):
"""A view to create a new FAQ."""
schema = IFAQ
- label = _('Create a new FAQ')
- field_names = ['title', 'keywords', 'content']
+ label = _("Create a new FAQ")
+ field_names = ["title", "keywords", "content"]
custom_widget_keywords = TokensTextWidget
@property
def page_title(self):
- return 'Create a FAQ for %s' % self.context.displayname
+ return "Create a FAQ for %s" % self.context.displayname
- @action(_('Create'), name='create')
+ @action(_("Create"), name="create")
def create__action(self, action, data):
"""Creates the FAQ."""
faq = self.context.newFAQ(
- self.user, data['title'], data['content'],
- keywords=data['keywords'])
+ self.user,
+ data["title"],
+ data["content"],
+ keywords=data["keywords"],
+ )
self.next_url = canonical_url(faq)
diff --git a/lib/lp/answers/browser/person.py b/lib/lp/answers/browser/person.py
index 04d0b4d..6d06aa1 100644
--- a/lib/lp/answers/browser/person.py
+++ b/lib/lp/answers/browser/person.py
@@ -4,17 +4,17 @@
"""Person-related answer listing classes."""
__all__ = [
- 'PersonAnswerContactForView',
- 'PersonAnswersMenu',
- 'PersonLatestQuestionsView',
- 'PersonSearchQuestionsView',
- 'SearchAnsweredQuestionsView',
- 'SearchAssignedQuestionsView',
- 'SearchCommentedQuestionsView',
- 'SearchCreatedQuestionsView',
- 'SearchNeedAttentionQuestionsView',
- 'SearchSubscribedQuestionsView',
- ]
+ "PersonAnswerContactForView",
+ "PersonAnswersMenu",
+ "PersonLatestQuestionsView",
+ "PersonSearchQuestionsView",
+ "SearchAnsweredQuestionsView",
+ "SearchAssignedQuestionsView",
+ "SearchCommentedQuestionsView",
+ "SearchCreatedQuestionsView",
+ "SearchNeedAttentionQuestionsView",
+ "SearchSubscribedQuestionsView",
+]
from operator import attrgetter
@@ -26,10 +26,7 @@ from lp.answers.interfaces.questionsperson import IQuestionsPerson
from lp.app.browser.launchpadform import LaunchpadFormView
from lp.registry.interfaces.person import IPerson
from lp.services.propertycache import cachedproperty
-from lp.services.webapp import (
- Link,
- NavigationMenu,
- )
+from lp.services.webapp import Link, NavigationMenu
from lp.services.webapp.publisher import LaunchpadView
@@ -40,9 +37,10 @@ class PersonLatestQuestionsView(LaunchpadFormView):
@cachedproperty
def getLatestQuestions(self, quantity=5):
- """Return <quantity> latest questions created for this target. """
+ """Return <quantity> latest questions created for this target."""
return IQuestionsPerson(self.context).searchQuestions(
- participation=QuestionParticipation.OWNER)[:quantity]
+ participation=QuestionParticipation.OWNER
+ )[:quantity]
class PersonSearchQuestionsView(SearchQuestionsView):
@@ -58,15 +56,19 @@ class PersonSearchQuestionsView(SearchQuestionsView):
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
- return _('Questions involving $name',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "Questions involving $name",
+ mapping=dict(name=self.context.displayname),
+ )
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- return _('No questions involving $name found with the '
- 'requested statuses.',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "No questions involving $name found with the "
+ "requested statuses.",
+ mapping=dict(name=self.context.displayname),
+ )
class SearchAnsweredQuestionsView(PersonSearchQuestionsView):
@@ -79,15 +81,19 @@ class SearchAnsweredQuestionsView(PersonSearchQuestionsView):
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
- return _('Questions answered by $name',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "Questions answered by $name",
+ mapping=dict(name=self.context.displayname),
+ )
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- return _('No questions answered by $name found with the '
- 'requested statuses.',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "No questions answered by $name found with the "
+ "requested statuses.",
+ mapping=dict(name=self.context.displayname),
+ )
class SearchAssignedQuestionsView(PersonSearchQuestionsView):
@@ -100,15 +106,19 @@ class SearchAssignedQuestionsView(PersonSearchQuestionsView):
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
- return _('Questions assigned to $name',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "Questions assigned to $name",
+ mapping=dict(name=self.context.displayname),
+ )
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- return _('No questions assigned to $name found with the '
- 'requested statuses.',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "No questions assigned to $name found with the "
+ "requested statuses.",
+ mapping=dict(name=self.context.displayname),
+ )
class SearchCommentedQuestionsView(PersonSearchQuestionsView):
@@ -121,15 +131,19 @@ class SearchCommentedQuestionsView(PersonSearchQuestionsView):
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
- return _('Questions commented on by $name ',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "Questions commented on by $name ",
+ mapping=dict(name=self.context.displayname),
+ )
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- return _('No questions commented on by $name found with the '
- 'requested statuses.',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "No questions commented on by $name found with the "
+ "requested statuses.",
+ mapping=dict(name=self.context.displayname),
+ )
class SearchCreatedQuestionsView(PersonSearchQuestionsView):
@@ -142,15 +156,19 @@ class SearchCreatedQuestionsView(PersonSearchQuestionsView):
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
- return _('Questions asked by $name',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "Questions asked by $name",
+ mapping=dict(name=self.context.displayname),
+ )
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- return _('No questions asked by $name found with the '
- 'requested statuses.',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "No questions asked by $name found with the "
+ "requested statuses.",
+ mapping=dict(name=self.context.displayname),
+ )
class SearchNeedAttentionQuestionsView(PersonSearchQuestionsView):
@@ -163,14 +181,18 @@ class SearchNeedAttentionQuestionsView(PersonSearchQuestionsView):
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
- return _("Questions needing $name's attention",
- mapping=dict(name=self.context.displayname))
+ return _(
+ "Questions needing $name's attention",
+ mapping=dict(name=self.context.displayname),
+ )
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- return _("No questions need $name's attention.",
- mapping=dict(name=self.context.displayname))
+ return _(
+ "No questions need $name's attention.",
+ mapping=dict(name=self.context.displayname),
+ )
class SearchSubscribedQuestionsView(PersonSearchQuestionsView):
@@ -183,15 +205,19 @@ class SearchSubscribedQuestionsView(PersonSearchQuestionsView):
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
- return _('Questions $name is subscribed to',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "Questions $name is subscribed to",
+ mapping=dict(name=self.context.displayname),
+ )
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- return _('No questions subscribed to by $name found with the '
- 'requested statuses.',
- mapping=dict(name=self.context.displayname))
+ return _(
+ "No questions subscribed to by $name found with the "
+ "requested statuses.",
+ mapping=dict(name=self.context.displayname),
+ )
class PersonAnswerContactForView(LaunchpadView):
@@ -201,8 +227,9 @@ class PersonAnswerContactForView(LaunchpadView):
@property
def label(self):
- return 'Projects for which %s is an answer contact' % (
- self.context.displayname)
+ return "Projects for which %s is an answer contact" % (
+ self.context.displayname
+ )
page_title = label
@@ -214,7 +241,8 @@ class PersonAnswerContactForView(LaunchpadView):
"""
return sorted(
IQuestionsPerson(self.context).getDirectAnswerQuestionTargets(),
- key=attrgetter('title'))
+ key=attrgetter("title"),
+ )
@cachedproperty
def team_question_targets(self):
@@ -224,7 +252,8 @@ class PersonAnswerContactForView(LaunchpadView):
"""
return sorted(
IQuestionsPerson(self.context).getTeamAnswerQuestionTargets(),
- key=attrgetter('title'))
+ key=attrgetter("title"),
+ )
def showRemoveYourselfLink(self):
"""The link is shown when the page is in the user's own profile."""
@@ -234,44 +263,53 @@ class PersonAnswerContactForView(LaunchpadView):
class PersonAnswersMenu(NavigationMenu):
usedfor = IPerson
- facet = 'answers'
- links = ['answered', 'assigned', 'created', 'commented', 'need_attention',
- 'subscribed', 'answer_contact_for']
+ facet = "answers"
+ links = [
+ "answered",
+ "assigned",
+ "created",
+ "commented",
+ "need_attention",
+ "subscribed",
+ "answer_contact_for",
+ ]
def answer_contact_for(self):
summary = "Projects for which %s is an answer contact" % (
- self.context.displayname)
+ self.context.displayname
+ )
return Link(
- '+answer-contact-for', 'Answer contact for', summary, icon='edit')
+ "+answer-contact-for", "Answer contact for", summary, icon="edit"
+ )
def answered(self):
- summary = 'Questions answered by %s' % self.context.displayname
- return Link(
- '+answeredquestions', 'Answered', summary, icon='question')
+ summary = "Questions answered by %s" % self.context.displayname
+ return Link("+answeredquestions", "Answered", summary, icon="question")
def assigned(self):
- summary = 'Questions assigned to %s' % self.context.displayname
- return Link(
- '+assignedquestions', 'Assigned', summary, icon='question')
+ summary = "Questions assigned to %s" % self.context.displayname
+ return Link("+assignedquestions", "Assigned", summary, icon="question")
def created(self):
- summary = 'Questions asked by %s' % self.context.displayname
- return Link('+createdquestions', 'Asked', summary, icon='question')
+ summary = "Questions asked by %s" % self.context.displayname
+ return Link("+createdquestions", "Asked", summary, icon="question")
def commented(self):
- summary = 'Questions commented on by %s' % (
- self.context.displayname)
+ summary = "Questions commented on by %s" % (self.context.displayname)
return Link(
- '+commentedquestions', 'Commented', summary, icon='question')
+ "+commentedquestions", "Commented", summary, icon="question"
+ )
def need_attention(self):
- summary = 'Questions needing %s attention' % (
- self.context.displayname)
- return Link('+needattentionquestions', 'Need attention', summary,
- icon='question')
+ summary = "Questions needing %s attention" % (self.context.displayname)
+ return Link(
+ "+needattentionquestions",
+ "Need attention",
+ summary,
+ icon="question",
+ )
def subscribed(self):
- text = 'Subscribed'
- summary = 'Questions subscribed to by %s' % (
- self.context.displayname)
- return Link('+subscribedquestions', text, summary, icon='question')
+ text = "Subscribed"
+ summary = "Questions subscribed to by %s" % (self.context.displayname)
+ return Link("+subscribedquestions", text, summary, icon="question")
diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
index a72083e..32d63dc 100644
--- a/lib/lp/answers/browser/question.py
+++ b/lib/lp/answers/browser/question.py
@@ -4,62 +4,46 @@
"""Question views."""
__all__ = [
- 'SearchAllQuestionsView',
- 'QuestionAddView',
- 'QuestionBreadcrumb',
- 'QuestionChangeStatusView',
- 'QuestionConfirmAnswerView',
- 'QuestionCreateFAQView',
- 'QuestionEditMenu',
- 'QuestionEditView',
- 'QuestionExtrasMenu',
- 'QuestionHistoryView',
- 'QuestionLinkFAQView',
- 'QuestionMessageDisplayView',
- 'QuestionSetContextMenu',
- 'QuestionSetNavigation',
- 'QuestionRejectView',
- 'QuestionSetView',
- 'QuestionSubscriptionView',
- 'QuestionWorkflowView',
- ]
+ "SearchAllQuestionsView",
+ "QuestionAddView",
+ "QuestionBreadcrumb",
+ "QuestionChangeStatusView",
+ "QuestionConfirmAnswerView",
+ "QuestionCreateFAQView",
+ "QuestionEditMenu",
+ "QuestionEditView",
+ "QuestionExtrasMenu",
+ "QuestionHistoryView",
+ "QuestionLinkFAQView",
+ "QuestionMessageDisplayView",
+ "QuestionSetContextMenu",
+ "QuestionSetNavigation",
+ "QuestionRejectView",
+ "QuestionSetView",
+ "QuestionSubscriptionView",
+ "QuestionWorkflowView",
+]
-from operator import attrgetter
import re
+from operator import attrgetter
+import zope.security
from lazr.restful.interface import copy_field
from lazr.restful.utils import smartquote
from zope.browserpage import ViewPageTemplateFile
from zope.component import getUtility
from zope.formlib import form
from zope.formlib.interfaces import IWidgetFactory
-from zope.formlib.widget import (
- CustomWidgetFactory,
- renderElement,
- )
-from zope.formlib.widgets import (
- TextAreaWidget,
- TextWidget,
- )
-from zope.interface import (
- alsoProvides,
- implementer,
- )
+from zope.formlib.widget import CustomWidgetFactory, renderElement
+from zope.formlib.widgets import TextAreaWidget, TextWidget
+from zope.interface import alsoProvides, implementer
from zope.schema import Choice
from zope.schema.interfaces import IContextSourceBinder
-from zope.schema.vocabulary import (
- SimpleTerm,
- SimpleVocabulary,
- )
-import zope.security
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
from lp import _
from lp.answers.browser.questiontarget import SearchQuestionsView
-from lp.answers.enums import (
- QuestionAction,
- QuestionSort,
- QuestionStatus,
- )
+from lp.answers.enums import QuestionAction, QuestionSort, QuestionStatus
from lp.answers.interfaces.faq import IFAQ
from lp.answers.interfaces.faqtarget import IFAQTarget
from lp.answers.interfaces.question import (
@@ -67,30 +51,24 @@ from lp.answers.interfaces.question import (
IQuestionAddMessageForm,
IQuestionChangeStatusForm,
IQuestionLinkFAQForm,
- )
+)
from lp.answers.interfaces.questioncollection import IQuestionSet
from lp.answers.interfaces.questionmessage import IQuestionMessage
from lp.answers.interfaces.questiontarget import (
IAnswersFrontPageSearchForm,
IQuestionTarget,
- )
+)
from lp.answers.vocabulary import UsesAnswersDistributionVocabulary
from lp.app.browser.launchpadform import (
- action,
LaunchpadEditFormView,
LaunchpadFormView,
+ action,
safe_action,
- )
+)
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.enums import ServiceUsage
-from lp.app.errors import (
- NotFoundError,
- UnexpectedFormData,
- )
-from lp.app.interfaces.launchpad import (
- ILaunchpadCelebrities,
- IServiceUsage,
- )
+from lp.app.errors import NotFoundError, UnexpectedFormData
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities, IServiceUsage
from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
from lp.app.widgets.launchpadtarget import LaunchpadTargetWidget
from lp.app.widgets.project import ProjectScopeWidget
@@ -101,15 +79,15 @@ from lp.services.propertycache import cachedproperty
from lp.services.statistics.interfaces.statistic import ILaunchpadStatisticSet
from lp.services.webapp import (
ApplicationMenu,
- canonical_url,
ContextMenu,
- enabled_with_permission,
LaunchpadView,
Link,
Navigation,
NavigationMenu,
+ canonical_url,
+ enabled_with_permission,
stepthrough,
- )
+)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.escaping import structured
@@ -118,7 +96,7 @@ from lp.services.webapp.snapshot import notify_modified
from lp.services.worlddata.helpers import (
is_english_variant,
preferred_or_request_languages,
- )
+)
class QuestionLinksMixin:
@@ -127,117 +105,139 @@ class QuestionLinksMixin:
def subscription(self):
"""Return a Link to the subscription view."""
if self.user is not None and self.context.isSubscribed(self.user):
- text = 'Unsubscribe'
- icon = 'remove'
- summary = ('You will stop receiving email notifications about '
- 'updates to this question')
+ text = "Unsubscribe"
+ icon = "remove"
+ summary = (
+ "You will stop receiving email notifications about "
+ "updates to this question"
+ )
else:
- text = 'Subscribe'
- icon = 'add'
- summary = ('You will receive email notifications about updates '
- 'to this question')
- return Link('+subscribe', text, icon=icon, summary=summary)
+ text = "Subscribe"
+ icon = "add"
+ summary = (
+ "You will receive email notifications about updates "
+ "to this question"
+ )
+ return Link("+subscribe", text, icon=icon, summary=summary)
def addsubscriber(self):
"""Return the 'Subscribe someone else' Link."""
- text = 'Subscribe someone else'
+ text = "Subscribe someone else"
return Link(
- '+addsubscriber', text, icon='add', summary=(
- 'Launchpad will email that person whenever this question '
- 'changes'))
+ "+addsubscriber",
+ text,
+ icon="add",
+ summary=(
+ "Launchpad will email that person whenever this question "
+ "changes"
+ ),
+ )
def edit(self):
"""Return a Link to the edit view."""
- text = 'Edit question'
- return Link('+edit', text, icon='edit')
+ text = "Edit question"
+ return Link("+edit", text, icon="edit")
class QuestionEditMenu(NavigationMenu, QuestionLinksMixin):
"""A menu for different aspects of editing a object."""
usedfor = IQuestion
- facet = 'answers'
- title = 'Edit question'
- links = ['edit', 'reject']
+ facet = "answers"
+ title = "Edit question"
+ links = ["edit", "reject"]
def reject(self):
"""Return a Link to the reject view."""
enabled = self.user is not None and self.context.canReject(self.user)
- text = 'Reject question'
- return Link('+reject', text, icon='edit', enabled=enabled)
+ text = "Reject question"
+ return Link("+reject", text, icon="edit", enabled=enabled)
class QuestionExtrasMenu(ApplicationMenu, QuestionLinksMixin):
"""Context menu of actions that can be performed upon a Question."""
+
usedfor = IQuestion
- facet = 'answers'
+ facet = "answers"
links = [
- 'history', 'linkbug', 'unlinkbug', 'makebug', 'linkfaq',
- 'createfaq', 'edit', 'changestatus', 'subscription', 'addsubscriber']
+ "history",
+ "linkbug",
+ "unlinkbug",
+ "makebug",
+ "linkfaq",
+ "createfaq",
+ "edit",
+ "changestatus",
+ "subscription",
+ "addsubscriber",
+ ]
def initialize(self):
"""Initialize the menu from the Question's state."""
self.has_bugs = bool(self.context.bugs)
- @enabled_with_permission('launchpad.Admin')
+ @enabled_with_permission("launchpad.Admin")
def changestatus(self):
"""Return a Link to the change status view."""
- return Link('+change-status', _('Change status'), icon='edit')
+ return Link("+change-status", _("Change status"), icon="edit")
def history(self):
"""Return a Link to the history view."""
- text = 'History'
- return Link('+history', text, icon='list',
- enabled=bool(self.context.messages))
+ text = "History"
+ return Link(
+ "+history", text, icon="list", enabled=bool(self.context.messages)
+ )
def linkbug(self):
"""Return a Link to the link bug view."""
- text = 'Link existing bug'
- return Link('+linkbug', text, icon='add')
+ text = "Link existing bug"
+ return Link("+linkbug", text, icon="add")
def unlinkbug(self):
"""Return a Link to the unlink bug view."""
- text = 'Remove bug link'
- return Link('+unlinkbug', text, icon='remove', enabled=self.has_bugs)
+ text = "Remove bug link"
+ return Link("+unlinkbug", text, icon="remove", enabled=self.has_bugs)
def makebug(self):
"""Return a Link to the make bug view."""
- text = 'Create bug report'
- summary = 'Create a bug report from this question.'
- return Link('+makebug', text, summary, icon='add',
- enabled=not self.has_bugs)
+ text = "Create bug report"
+ summary = "Create a bug report from this question."
+ return Link(
+ "+makebug", text, summary, icon="add", enabled=not self.has_bugs
+ )
def linkfaq(self):
"""Link for linking to a FAQ."""
- text = 'Link to a FAQ'
- summary = 'Link this question to a FAQ.'
+ text = "Link to a FAQ"
+ summary = "Link this question to a FAQ."
if self.context.faq is None:
- icon = 'add'
+ icon = "add"
else:
- icon = 'edit'
- return Link('+linkfaq', text, summary, icon=icon)
+ icon = "edit"
+ return Link("+linkfaq", text, summary, icon=icon)
def createfaq(self):
"""LInk for creating a FAQ."""
- text = 'Create a new FAQ'
- summary = 'Create a new FAQ from this question.'
- return Link('+createfaq', text, summary, icon='add')
+ text = "Create a new FAQ"
+ summary = "Create a new FAQ from this question."
+ return Link("+createfaq", text, summary, icon="add")
class QuestionSetContextMenu(ContextMenu):
"""Context menu of actions that can be preformed upon a QuestionSet."""
+
usedfor = IQuestionSet
- links = ['findproduct', 'finddistro']
+ links = ["findproduct", "finddistro"]
def findproduct(self):
"""Return a Link to the find product view."""
- text = 'Find upstream project'
- return Link('/projects', text, icon='search')
+ text = "Find upstream project"
+ return Link("/projects", text, icon="search")
def finddistro(self):
"""Return a Link to the find distribution view."""
- text = 'Find distribution'
- return Link('/distros', text, icon='search')
+ text = "Find distribution"
+ return Link("/distros", text, icon="search")
class QuestionSetNavigation(Navigation):
@@ -254,7 +254,8 @@ class QuestionSetNavigation(Navigation):
if question is None:
raise NotFoundError(name)
return self.redirectSubTree(
- canonical_url(question, self.request), status=301)
+ canonical_url(question, self.request), status=301
+ )
class QuestionNavigation(Navigation):
@@ -262,7 +263,7 @@ class QuestionNavigation(Navigation):
usedfor = IQuestion
- @stepthrough('messages')
+ @stepthrough("messages")
def traverse_messages(self, index):
try:
index = int(index) - 1
@@ -279,7 +280,7 @@ class QuestionMessageNavigation(Navigation):
usedfor = IQuestionMessage
- @stepthrough('revisions')
+ @stepthrough("revisions")
def traverse_revisions(self, revision):
try:
revision = int(revision)
@@ -293,7 +294,7 @@ class QuestionBreadcrumb(Breadcrumb):
@property
def text(self):
- return 'Question #%d' % self.context.id
+ return "Question #%d" % self.context.id
class QuestionSetView(LaunchpadFormView):
@@ -302,67 +303,73 @@ class QuestionSetView(LaunchpadFormView):
schema = IAnswersFrontPageSearchForm
custom_widget_scope = ProjectScopeWidget
- page_title = 'Launchpad Answers'
- label = 'Questions and Answers'
+ page_title = "Launchpad Answers"
+ label = "Questions and Answers"
@property
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")
@safe_action
- @action('Find Answers', name="search")
+ @action("Find Answers", name="search")
def search_action(self, action, data):
"""Redirect to the proper search page based on the scope widget."""
# For the scope to be absent from the form, the user must
# build the query string themselves - most likely because they
# are a bot. In that case we just assume they want to search
# all projects.
- scope = self.widgets['scope'].getScope()
- if scope is None or scope == 'all':
+ scope = self.widgets["scope"].getScope()
+ if scope is None or scope == "all":
# Use 'All projects' scope.
scope = self.context
else:
- scope = self.widgets['scope'].getInputValue()
+ scope = self.widgets["scope"].getInputValue()
self.next_url = "%s/+tickets?%s" % (
- canonical_url(scope), self.request['QUERY_STRING'])
+ canonical_url(scope),
+ self.request["QUERY_STRING"],
+ )
@property
def question_count(self):
"""Return the number of questions in the system."""
- return getUtility(ILaunchpadStatisticSet).value('question_count')
+ return getUtility(ILaunchpadStatisticSet).value("question_count")
@property
def answered_question_count(self):
"""Return the number of answered questions in the system."""
return getUtility(ILaunchpadStatisticSet).value(
- 'answered_question_count')
+ "answered_question_count"
+ )
@property
def solved_question_count(self):
"""Return the number of solved questions in the system."""
return getUtility(ILaunchpadStatisticSet).value(
- 'solved_question_count')
+ "solved_question_count"
+ )
@property
def projects_with_questions_count(self):
"""Return the number of projects with questions in the system."""
return getUtility(ILaunchpadStatisticSet).value(
- 'projects_with_questions_count')
+ "projects_with_questions_count"
+ )
@property
def latest_questions_asked(self):
"""Return the 5 latest questions asked."""
return self.context.searchQuestions(
- status=QuestionStatus.OPEN, sort=QuestionSort.NEWEST_FIRST)[:5]
+ status=QuestionStatus.OPEN, sort=QuestionSort.NEWEST_FIRST
+ )[:5]
@property
def latest_questions_solved(self):
@@ -370,7 +377,8 @@ class QuestionSetView(LaunchpadFormView):
# XXX flacoste 2006-11-28: We should probably define a new
# QuestionSort value allowing us to sort on dateanswered descending.
return self.context.searchQuestions(
- status=QuestionStatus.SOLVED, sort=QuestionSort.NEWEST_FIRST)[:5]
+ status=QuestionStatus.SOLVED, sort=QuestionSort.NEWEST_FIRST
+ )[:5]
@property
def most_active_projects(self):
@@ -394,30 +402,32 @@ class QuestionSubscriptionView(LaunchpadView):
with notify_modified(self.context, modified_fields):
response = self.request.response
# Establish if a subscription form was posted.
- newsub = self.request.form.get('subscribe', None)
+ newsub = self.request.form.get("subscribe", None)
if newsub is not None:
- if newsub == 'Subscribe':
+ if newsub == "Subscribe":
self.context.subscribe(self.user)
response.addNotification(
- _("You have subscribed to this question."))
- modified_fields.add('subscribers')
- elif newsub == 'Unsubscribe':
+ _("You have subscribed to this question.")
+ )
+ modified_fields.add("subscribers")
+ elif newsub == "Unsubscribe":
self.context.unsubscribe(self.user, self.user)
response.addNotification(
- _("You have unsubscribed from this question."))
- modified_fields.add('subscribers')
+ _("You have unsubscribed from this question.")
+ )
+ modified_fields.add("subscribers")
response.redirect(canonical_url(self.context))
@property
def page_title(self):
- return 'Subscription'
+ return "Subscription"
@property
def label(self):
if self.subscription:
- return 'Unsubscribe from question'
+ return "Unsubscribe from question"
else:
- return 'Subscribe to question'
+ return "Subscribe to question"
@property
def subscription(self):
@@ -468,8 +478,10 @@ class QuestionLanguageVocabularyFactory:
if context is not None and not IProjectGroup.providedBy(context):
question_target = IQuestionTarget(context)
supported_languages = question_target.getSupportedLanguages()
- elif (IProjectGroup.providedBy(context) and
- self.view.question_target is not None):
+ elif (
+ IProjectGroup.providedBy(context)
+ and self.view.question_target is not None
+ ):
# ProjectGroups do not implement IQuestionTarget--the user must
# choose a product while asking a question.
question_target = IQuestionTarget(self.view.question_target)
@@ -498,13 +510,14 @@ class QuestionSupportLanguageMixin:
"""
supported_languages_macros = ViewPageTemplateFile(
- '../templates/question-supported-languages-macros.pt')
+ "../templates/question-supported-languages-macros.pt"
+ )
@property
def chosen_language(self):
"""Return the language chosen by the user."""
- if self.widgets['language'].hasInput():
- return self.widgets['language'].getInputValue()
+ if self.widgets["language"].hasInput():
+ return self.widgets["language"].getInputValue()
else:
return self.context.language
@@ -512,7 +525,7 @@ class QuestionSupportLanguageMixin:
def unsupported_languages_warning(self):
"""Macro displaying a warning in case of unsupported languages."""
macros = self.supported_languages_macros.macros
- return macros['unsupported_languages_warning']
+ return macros["unsupported_languages_warning"]
@property
def question_target(self):
@@ -524,7 +537,8 @@ class QuestionSupportLanguageMixin:
"""Return the list of supported languages ordered by name."""
return sorted(
self.question_target.getSupportedLanguages(),
- key=attrgetter('englishname'))
+ key=attrgetter("englishname"),
+ )
def createLanguageField(self):
"""Create a field with a vocabulary to edit a question language.
@@ -533,16 +547,19 @@ class QuestionSupportLanguageMixin:
:return: A form.Fields instance containing the language field.
"""
return form.Fields(
- Choice(
- __name__='language',
- source=QuestionLanguageVocabularyFactory(view=self),
- title=_('Language'),
- description=_(
- "The language in which this question is written. "
- "The languages marked with a star (*) are the "
- "languages spoken by at least one answer contact in "
- "the community.")),
- render_context=self.render_context)
+ Choice(
+ __name__="language",
+ source=QuestionLanguageVocabularyFactory(view=self),
+ title=_("Language"),
+ description=_(
+ "The language in which this question is written. "
+ "The languages marked with a star (*) are the "
+ "languages spoken by at least one answer contact in "
+ "the community."
+ ),
+ ),
+ render_context=self.render_context,
+ )
def shouldWarnAboutUnsupportedLanguage(self):
"""Test if the warning about unsupported language should be displayed.
@@ -552,11 +569,13 @@ class QuestionSupportLanguageMixin:
will only be displayed one time, except if the user changes the
request language to another unsupported value.
"""
- if (self.chosen_language in
- self.question_target.getSupportedLanguages()):
+ if (
+ self.chosen_language
+ in self.question_target.getSupportedLanguages()
+ ):
return False
- old_chosen_language = self.request.form.get('chosen_language')
+ old_chosen_language = self.request.form.get("chosen_language")
return self.chosen_language.code != old_chosen_language
@@ -565,7 +584,7 @@ class QuestionHistoryView(LaunchpadView):
@property
def page_title(self):
- return 'History of question #%s' % self.context.id
+ return "History of question #%s" % self.context.id
label = page_title
@@ -576,22 +595,25 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
The user enters first their question summary and then they are shown a
list of similar results before adding the question.
"""
- label = _('Ask a question')
+
+ label = _("Ask a question")
schema = IQuestion
- field_names = ['title', 'description']
+ field_names = ["title", "description"]
# The fields displayed on the search page.
- search_field_names = ['language', 'title']
+ search_field_names = ["language", "title"]
custom_widget_title = CustomWidgetFactory(
- TextWidget, displayWidth=40, displayMaxWidth=250)
+ TextWidget, displayWidth=40, displayMaxWidth=250
+ )
search_template = ViewPageTemplateFile(
- '../templates/question-add-search.pt')
+ "../templates/question-add-search.pt"
+ )
- add_template = ViewPageTemplateFile('../templates/question-add.pt')
+ add_template = ViewPageTemplateFile("../templates/question-add.pt")
template = search_template
@@ -629,7 +651,7 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
else:
fields = self.form_fields
for field in fields:
- widget = getattr(self, 'custom_widget_%s' % field.__name__, None)
+ widget = getattr(self, "custom_widget_%s" % field.__name__, None)
if widget is not None:
if IWidgetFactory.providedBy(widget):
field.custom_widget = widget
@@ -642,32 +664,42 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
"""Set up the widgets using the view's form fields and the context."""
fields = self._getFieldsForWidgets()
self.widgets = form.setUpWidgets(
- fields, self.prefix, self.context, self.request,
- data=self.initial_values, ignore_request=False)
+ fields,
+ self.prefix,
+ self.context,
+ self.request,
+ data=self.initial_values,
+ ignore_request=False,
+ )
def validate(self, data):
"""Validate hook.
This validation method sets the chosen_language attribute.
"""
- if 'title' not in data:
+ if "title" not in data:
self.setFieldError(
- 'title', _('You must enter a summary of your problem.'))
+ "title", _("You must enter a summary of your problem.")
+ )
else:
- if len(data['title']) > 250:
+ if len(data["title"]) > 250:
self.setFieldError(
- 'title', _('The summary cannot exceed 250 characters.'))
- if self.widgets.get('description'):
- if 'description' not in data:
+ "title", _("The summary cannot exceed 250 characters.")
+ )
+ if self.widgets.get("description"):
+ if "description" not in data:
self.setFieldError(
- 'description',
- _('You must provide details about your problem.'))
+ "description",
+ _("You must provide details about your problem."),
+ )
@property
def page_title(self):
"""The current page title."""
- return _('Ask a question about ${context}',
- mapping=dict(context=self.context.displayname))
+ return _(
+ "Ask a question about ${context}",
+ mapping=dict(context=self.context.displayname),
+ )
@property
def has_similar_items(self):
@@ -683,21 +715,25 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
else:
return False
- @action(_('Continue'))
+ @action(_("Continue"))
def continue_action(self, action, data):
"""Search for questions and FAQs similar to the entered summary."""
# If the description widget wasn't setup, add it here
- if self.widgets.get('description') is None:
+ if self.widgets.get("description") is None:
self.widgets += form.setUpWidgets(
- self.form_fields.select('description'), self.prefix,
- self.context, self.request, data=self.initial_values,
- ignore_request=False)
+ self.form_fields.select("description"),
+ self.prefix,
+ self.context,
+ self.request,
+ data=self.initial_values,
+ ignore_request=False,
+ )
- faqs = IFAQTarget(self.question_target).findSimilarFAQs(data['title'])
- self.similar_faqs = list(faqs[:self._MAX_SIMILAR_FAQS])
+ faqs = IFAQTarget(self.question_target).findSimilarFAQs(data["title"])
+ self.similar_faqs = list(faqs[: self._MAX_SIMILAR_FAQS])
- questions = self.question_target.findSimilarQuestions(data['title'])
- self.similar_questions = list(questions[:self._MAX_SIMILAR_QUESTIONS])
+ questions = self.question_target.findSimilarQuestions(data["title"])
+ self.similar_questions = list(questions[: self._MAX_SIMILAR_QUESTIONS])
return self.add_template()
@@ -706,15 +742,16 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
to the search template when the summary is missing or delegate to
the continue action handler to do the search.
"""
- if 'title' not in data:
+ if "title" not in data:
# Remove the description widget.
- widgets = [(True, self.widgets[name])
- for name in self.search_field_names]
+ widgets = [
+ (True, self.widgets[name]) for name in self.search_field_names
+ ]
self.widgets = form.Widgets(widgets, len(self.prefix) + 1)
return self.search_template()
return self.continue_action.success(data)
- @action(_('Post Question'), name='add', failure='handleAddError')
+ @action(_("Post Question"), name="add", failure="handleAddError")
def add_action(self, action, data):
"""Add a Question to an `IQuestionTarget`."""
if self.shouldWarnAboutUnsupportedLanguage():
@@ -723,41 +760,42 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
return self.continue_action.success(data)
question = self.question_target.newQuestion(
- self.user, data['title'], data['description'], data['language'])
+ self.user, data["title"], data["description"], data["language"]
+ )
self.request.response.redirect(canonical_url(question))
- return ''
+ return ""
class QuestionChangeStatusView(LaunchpadFormView):
"""View for changing a question status."""
+
schema = IQuestionChangeStatusForm
- label = 'Change question status'
+ label = "Change question status"
@property
def page_title(self):
- return 'Change status of question #%s' % self.context.id
+ return "Change status of question #%s" % self.context.id
def validate(self, data):
"""Check that the status and message are valid."""
- if data.get('status') == self.context.status:
+ if data.get("status") == self.context.status:
+ self.setFieldError("status", _("You didn't change the status."))
+ if not data.get("message"):
self.setFieldError(
- 'status', _("You didn't change the status."))
- if not data.get('message'):
- self.setFieldError(
- 'message', _('You must provide an explanation message.'))
+ "message", _("You must provide an explanation message.")
+ )
@property
def initial_values(self):
"""Return the initial view values."""
- return {'status': self.context.status}
+ return {"status": self.context.status}
- @action(_('Change Status'), name='change-status')
+ @action(_("Change Status"), name="change-status")
def change_status_action(self, action, data):
"""Change the Question status."""
- self.context.setStatus(self.user, data['status'], data['message'])
- self.request.response.addNotification(
- _('Question status updated.'))
+ self.context.setStatus(self.user, data["status"], data["message"])
+ self.request.response.addNotification(_("Question status updated."))
@property
def next_url(self):
@@ -770,7 +808,7 @@ class QuestionTargetWidget(LaunchpadTargetWidget):
"""A targeting widget that is aware of pillars that use Answers."""
def getProductVocabulary(self):
- return 'UsesAnswersProduct'
+ return "UsesAnswersProduct"
def getDistributionVocabulary(self):
distro = self.context.context.distribution
@@ -780,11 +818,17 @@ class QuestionTargetWidget(LaunchpadTargetWidget):
class QuestionEditView(LaunchpadEditFormView):
"""View for editing a Question."""
+
schema = IQuestion
- label = 'Edit question'
+ label = "Edit question"
field_names = [
- "language", "title", "description", "target", "assignee",
- "whiteboard"]
+ "language",
+ "title",
+ "description",
+ "target",
+ "assignee",
+ "whiteboard",
+ ]
custom_widget_title = CustomWidgetFactory(TextWidget, displayWidth=40)
custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
@@ -792,7 +836,7 @@ class QuestionEditView(LaunchpadEditFormView):
@property
def page_title(self):
- return 'Edit question #%s details' % self.context.id
+ return "Edit question #%s details" % self.context.id
label = page_title
@@ -803,8 +847,9 @@ class QuestionEditView(LaunchpadEditFormView):
"""
LaunchpadEditFormView.setUpFields(self)
- self.form_fields = self.form_fields.omit("distribution",
- "sourcepackagename", "product")
+ self.form_fields = self.form_fields.omit(
+ "distribution", "sourcepackagename", "product"
+ )
editable_fields = []
for field in self.form_fields:
@@ -817,12 +862,12 @@ class QuestionEditView(LaunchpadEditFormView):
"""Update the Question from the request form data."""
# Target must be the last field processed because it implicitly
# changes the user's permissions.
- target_data = {'target': self.context.target}
- if 'target' in data:
- target_data['target'] = data['target']
- del data['target']
+ target_data = {"target": self.context.target}
+ if "target" in data:
+ target_data["target"] = data["target"]
+ del data["target"]
self.updateContextFromData(data)
- if target_data['target'] != self.context.target:
+ if target_data["target"] != self.context.target:
self.updateContextFromData(target_data)
@property
@@ -834,26 +879,29 @@ class QuestionEditView(LaunchpadEditFormView):
class QuestionRejectView(LaunchpadFormView):
"""View for rejecting a question."""
+
schema = IQuestionChangeStatusForm
- field_names = ['message']
- label = 'Reject question'
+ field_names = ["message"]
+ label = "Reject question"
@property
def page_title(self):
- return 'Reject question #%s' % self.context.id
+ return "Reject question #%s" % self.context.id
def validate(self, data):
"""Check that required information was provided."""
- if 'message' not in data:
+ if "message" not in data:
self.setFieldError(
- 'message', _('You must provide an explanation message.'))
+ "message", _("You must provide an explanation message.")
+ )
- @action(_('Reject Question'), name="reject")
+ @action(_("Reject Question"), name="reject")
def reject_action(self, action, data):
"""Reject the Question."""
- self.context.reject(self.user, data['message'])
+ self.context.reject(self.user, data["message"])
self.request.response.addNotification(
- _('You have rejected this question.'))
+ _("You have rejected this question.")
+ )
def initialize(self):
"""See `LaunchpadFormView`.
@@ -862,7 +910,8 @@ class QuestionRejectView(LaunchpadFormView):
"""
if self.context.status == QuestionStatus.INVALID:
self.request.response.addNotification(
- _('The question is already rejected.'))
+ _("The question is already rejected.")
+ )
self.request.response.redirect(canonical_url(self.context))
return
@@ -886,8 +935,9 @@ class LinkFAQMixin:
@property
def default_message(self):
"""The default link message to use."""
- return '%s suggests this article as an answer to your question:' % (
- self.user.displayname)
+ return "%s suggests this article as an answer to your question:" % (
+ self.user.displayname
+ )
def getFAQMessageReference(self, faq):
"""Return the reference for the FAQ to use in the linking message."""
@@ -898,6 +948,7 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
"""View managing the question workflow action, i.e. action changing
its status.
"""
+
schema = IQuestionAddMessageForm
# Do not autofocus the message widget.
@@ -912,18 +963,19 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
return smartquote('%s question #%d: "%s"') % (
self.context.target.displayname,
self.context.id,
- self.context.title)
+ self.context.title,
+ )
def setUpFields(self):
"""See `LaunchpadFormView`."""
LaunchpadFormView.setUpFields(self)
if self.context.isSubscribed(self.user):
- self.form_fields = self.form_fields.omit('subscribe_me')
+ self.form_fields = self.form_fields.omit("subscribe_me")
def setUpWidgets(self):
"""See `LaunchpadFormView`."""
LaunchpadFormView.setUpWidgets(self)
- alsoProvides(self.widgets['message'], IAlwaysSubmittedWidget)
+ alsoProvides(self.widgets["message"], IAlwaysSubmittedWidget)
def validate(self, data):
"""Form validatation hook.
@@ -935,8 +987,8 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
if self.confirm_action.submitted():
self.validateConfirmAnswer(data)
else:
- if not data.get('message'):
- self.setFieldError('message', _('Please enter a message.'))
+ if not data.get("message"):
+ self.setFieldError("message", _("Please enter a message."))
@property
def lang(self):
@@ -969,8 +1021,10 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
strip_invisible = not (role.in_admin or role.in_registry_experts)
if strip_invisible:
messages = [
- message for message in messages
- if message.visible or message.owner == self.user]
+ message
+ for message in messages
+ if message.visible or message.owner == self.user
+ ]
return messages
def canAddComment(self, action):
@@ -981,79 +1035,92 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
"""
return self.user is not None
- @action(_('Just Add a Comment'), name='comment', condition=canAddComment)
+ @action(_("Just Add a Comment"), name="comment", condition=canAddComment)
def comment_action(self, action, data):
"""Add a comment to a resolved question."""
- self.context.addComment(self.user, data['message'])
+ self.context.addComment(self.user, data["message"])
self._addNotificationAndHandlePossibleSubscription(
- _('Thanks for your comment.'), data)
+ _("Thanks for your comment."), data
+ )
@property
def show_call_to_answer(self):
"""Return whether the call to answer should be displayed."""
- return (self.user != self.context.owner and
- self.context.can_give_answer)
+ return self.user != self.context.owner and self.context.can_give_answer
def canAddAnswer(self, action):
"""Return whether the answer action should be displayed."""
- return (self.user is not None and
- self.user != self.context.owner and
- self.context.can_give_answer)
+ return (
+ self.user is not None
+ and self.user != self.context.owner
+ and self.context.can_give_answer
+ )
- @action(_('Propose Answer'), name='answer', condition=canAddAnswer)
+ @action(_("Propose Answer"), name="answer", condition=canAddAnswer)
def answer_action(self, action, data):
"""Add an answer to the question."""
- self.context.giveAnswer(self.user, data['message'])
+ self.context.giveAnswer(self.user, data["message"])
self._addNotificationAndHandlePossibleSubscription(
- _('Thanks for your answer.'), data)
+ _("Thanks for your answer."), data
+ )
def canSelfAnswer(self, action):
"""Return whether the selfanswer action should be displayed."""
- return (self.user == self.context.owner and
- self.context.can_give_answer)
+ return self.user == self.context.owner and self.context.can_give_answer
- @action(_('Problem Solved'), name="selfanswer",
- condition=canSelfAnswer)
+ @action(_("Problem Solved"), name="selfanswer", condition=canSelfAnswer)
def selfanswer_action(self, action, data):
"""Action called when the owner provides the solution."""
- self.context.giveAnswer(self.user, data['message'])
+ self.context.giveAnswer(self.user, data["message"])
# Owners frequently solve their questions, but their messages imply
# that another user provided an answer. When a question has answers
# that can be confirmed, suggest to the owner that they use the
# confirmation button.
if self.context.can_confirm_answer:
- msgid = _("Your question is solved. If a particular message "
- "helped you solve the problem, use the <em>'This "
- "solved my problem'</em> button.")
+ msgid = _(
+ "Your question is solved. If a particular message "
+ "helped you solve the problem, use the <em>'This "
+ "solved my problem'</em> button."
+ )
self._addNotificationAndHandlePossibleSubscription(
- structured(msgid), data)
+ structured(msgid), data
+ )
def canRequestInfo(self, action):
"""Return if the requestinfo action should be displayed."""
- return (self.user is not None and
- self.user != self.context.owner and
- self.context.can_request_info)
-
- @action(_('Add Information Request'), name='requestinfo',
- condition=canRequestInfo)
+ return (
+ self.user is not None
+ and self.user != self.context.owner
+ and self.context.can_request_info
+ )
+
+ @action(
+ _("Add Information Request"),
+ name="requestinfo",
+ condition=canRequestInfo,
+ )
def requestinfo_action(self, action, data):
"""Add a request for more information to the question."""
- self.context.requestInfo(self.user, data['message'])
+ self.context.requestInfo(self.user, data["message"])
self._addNotificationAndHandlePossibleSubscription(
- _('Thanks for your information request.'), data)
+ _("Thanks for your information request."), data
+ )
def canGiveInfo(self, action):
"""Return whether the giveinfo action should be displayed."""
- return (self.user == self.context.owner and
- self.context.can_give_info)
+ return self.user == self.context.owner and self.context.can_give_info
- @action(_("I'm Providing More Information"), name='giveinfo',
- condition=canGiveInfo)
+ @action(
+ _("I'm Providing More Information"),
+ name="giveinfo",
+ condition=canGiveInfo,
+ )
def giveinfo_action(self, action, data):
"""Give additional informatin on the request."""
- self.context.giveInfo(data['message'])
+ self.context.giveInfo(data["message"])
self._addNotificationAndHandlePossibleSubscription(
- _('Thanks for adding more information to your question.'), data)
+ _("Thanks for adding more information to your question."), data
+ )
def validateConfirmAnswer(self, data):
"""Make sure that a valid message id was provided as the confirmed
@@ -1061,47 +1128,48 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
# No widget is used for the answer, we are using hidden fields
# in the template for that. So, if the answer is missing, it's
# either a programming error or an invalid handcrafted URL
- msgid = self.request.form.get('answer_id')
+ msgid = self.request.form.get("answer_id")
if msgid is None:
- raise UnexpectedFormData('missing answer_id')
+ raise UnexpectedFormData("missing answer_id")
try:
- data['answer'] = self.context.messages[int(msgid)]
+ data["answer"] = self.context.messages[int(msgid)]
except ValueError:
- raise UnexpectedFormData('invalid answer_id: %s' % msgid)
+ raise UnexpectedFormData("invalid answer_id: %s" % msgid)
except IndexError:
raise UnexpectedFormData("unknown answer: %s" % msgid)
def canConfirm(self, action):
"""Return whether the confirm action should be displayed."""
- return (self.user == self.context.owner and
- self.context.can_confirm_answer)
+ return (
+ self.user == self.context.owner and self.context.can_confirm_answer
+ )
- @action(_("This Solved My Problem"), name='confirm',
- condition=canConfirm)
+ @action(_("This Solved My Problem"), name="confirm", condition=canConfirm)
def confirm_action(self, action, data):
"""Confirm that an answer solved the request."""
# The confirmation message is not given by the user when the
# 'This Solved My Problem' button on the main question view.
- if not data['message']:
- data['message'] = 'Thanks %s, that solved my question.' % (
- data['answer'].owner.displayname)
- self.context.confirmAnswer(data['message'], answer=data['answer'])
+ if not data["message"]:
+ data["message"] = "Thanks %s, that solved my question." % (
+ data["answer"].owner.displayname
+ )
+ self.context.confirmAnswer(data["message"], answer=data["answer"])
self._addNotificationAndHandlePossibleSubscription(
- _('Thanks for your feedback.'), data)
+ _("Thanks for your feedback."), data
+ )
def canReopen(self, action):
"""Return whether the reopen action should be displayed."""
- return (self.user == self.context.owner and
- self.context.can_reopen)
+ return self.user == self.context.owner and self.context.can_reopen
- @action(_("I Still Need an Answer"), name='reopen',
- condition=canReopen)
+ @action(_("I Still Need an Answer"), name="reopen", condition=canReopen)
def reopen_action(self, action, data):
"""State that the problem is still occuring and provide new
information about it."""
- self.context.reopen(data['message'])
+ self.context.reopen(data["message"])
self._addNotificationAndHandlePossibleSubscription(
- _('Your question was reopened.'), data)
+ _("Your question was reopened."), data
+ )
def _addNotificationAndHandlePossibleSubscription(self, message, data):
"""Post-processing work common to all workflow actions.
@@ -1111,26 +1179,30 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
"""
self.request.response.addNotification(message)
- if data.get('subscribe_me'):
+ if data.get("subscribe_me"):
self.context.subscribe(self.user)
self.request.response.addNotification(
- _("You have subscribed to this question."))
+ _("You have subscribed to this question.")
+ )
self.next_url = canonical_url(self.context)
@property
def new_question_url(self):
"""Return a URL to add a new question for the QuestionTarget."""
- return '%s/+addquestion' % canonical_url(self.context.target,
- rootsite='answers')
+ return "%s/+addquestion" % canonical_url(
+ self.context.target, rootsite="answers"
+ )
@property
def original_bug(self):
"""Return the bug that the question was created from or None."""
for bug in self.context.bugs:
- if (check_permission('launchpad.View', bug)
+ if (
+ check_permission("launchpad.View", bug)
and bug.owner == self.context.owner
- and bug.datecreated == self.context.datecreated):
+ and bug.datecreated == self.context.datecreated
+ ):
return bug
return None
@@ -1145,9 +1217,12 @@ class QuestionConfirmAnswerView(QuestionWorkflowView):
"""Initialize the view from the Question state."""
# This page is only accessible when a confirmation is possible.
if not self.context.can_confirm_answer:
- self.request.response.addErrorNotification(_(
- "The question is not in a state where you can confirm "
- "an answer."))
+ self.request.response.addErrorNotification(
+ _(
+ "The question is not in a state where you can confirm "
+ "an answer."
+ )
+ )
self.request.response.redirect(canonical_url(self.context))
return
@@ -1157,7 +1232,7 @@ class QuestionConfirmAnswerView(QuestionWorkflowView):
"""Return the message that should be confirmed."""
data = {}
self.validateConfirmAnswer(data)
- return data['answer']
+ return data["answer"]
class QuestionMessageDisplayView(LaunchpadView):
@@ -1172,14 +1247,17 @@ class QuestionMessageDisplayView(LaunchpadView):
@cachedproperty
def isBestAnswer(self):
"""Return True when this message is marked as solving the question."""
- return (self.context == self.question.answer
- and self.context.action in [
- QuestionAction.ANSWER, QuestionAction.CONFIRM])
+ return (
+ self.context == self.question.answer
+ and self.context.action
+ in [QuestionAction.ANSWER, QuestionAction.CONFIRM]
+ )
def renderAnswerIdFormElement(self):
"""Return the hidden form element to refer to that message."""
return '<input type="hidden" name="answer_id" value="%d" />' % list(
- self.context.question.messages).index(self.context)
+ self.context.question.messages
+ ).index(self.context)
def getBodyCSSClass(self):
"""Return the CSS class to use for this message's body."""
@@ -1190,7 +1268,7 @@ class QuestionMessageDisplayView(LaunchpadView):
@cachedproperty
def canSeeSpamControls(self):
- return check_permission('launchpad.Moderate', self.context)
+ return check_permission("launchpad.Moderate", self.context)
def getBoardCommentCSSClass(self):
css_classes = ["boardComment", "editable-message"]
@@ -1202,14 +1280,16 @@ class QuestionMessageDisplayView(LaunchpadView):
@property
def can_edit(self):
- return check_permission('launchpad.Edit', self.context)
+ return check_permission("launchpad.Edit", self.context)
def canConfirmAnswer(self):
"""Return True if the user can confirm this answer."""
- return (self.display_confirm_button and
- self.user == self.question.owner and
- self.question.can_confirm_answer and
- self.context.action == QuestionAction.ANSWER)
+ return (
+ self.display_confirm_button
+ and self.user == self.question.owner
+ and self.question.can_confirm_answer
+ and self.context.action == QuestionAction.ANSWER
+ )
def renderWithoutConfirmButton(self):
"""Display the message without any confirm button."""
@@ -1222,29 +1302,33 @@ class SearchAllQuestionsView(SearchQuestionsView):
display_target_column = True
# Match contiguous digits, optionally prefixed with a '#'.
- id_pattern = re.compile(r'^#?(\d+)$')
+ id_pattern = re.compile(r"^#?(\d+)$")
@property
def pageheading(self):
"""See `SearchQuestionsView`."""
if self.search_text:
- return _('Questions matching "${search_text}"',
- mapping=dict(search_text=self.search_text))
+ return _(
+ 'Questions matching "${search_text}"',
+ mapping=dict(search_text=self.search_text),
+ )
else:
- return _('Search all questions')
+ return _("Search all questions")
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
if self.search_text:
- return _("There are no questions matching "
- '"${search_text}" with the requested statuses.',
- mapping=dict(search_text=self.search_text))
+ return _(
+ "There are no questions matching "
+ '"${search_text}" with the requested statuses.',
+ mapping=dict(search_text=self.search_text),
+ )
else:
- return _('There are no questions with the requested statuses.')
+ return _("There are no questions with the requested statuses.")
@safe_action
- @action(_('Search'), name='search')
+ @action(_("Search"), name="search")
def search_action(self, action, data):
"""Action executed when the user clicked the 'Find Answers' button.
@@ -1266,13 +1350,13 @@ class QuestionCreateFAQView(LinkFAQMixin, LaunchpadFormView):
"""View to create a new FAQ."""
schema = IFAQ
- label = _('Create a new FAQ')
+ label = _("Create a new FAQ")
@property
def page_title(self):
- return 'Create a FAQ for %s' % self.context.product.displayname
+ return "Create a FAQ for %s" % self.context.product.displayname
- field_names = ['title', 'keywords', 'content']
+ field_names = ["title", "keywords", "content"]
custom_widget_keywords = TokensTextWidget
custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
@@ -1282,10 +1366,10 @@ class QuestionCreateFAQView(LinkFAQMixin, LaunchpadFormView):
"""Fill title and content based on the question."""
question = self.context
return {
- 'title': question.title,
- 'content': question.description,
- 'message': self.default_message,
- }
+ "title": question.title,
+ "content": question.description,
+ "message": self.default_message,
+ }
def setUpFields(self):
"""See `LaunchpadFormView`.
@@ -1294,23 +1378,28 @@ class QuestionCreateFAQView(LinkFAQMixin, LaunchpadFormView):
"""
super().setUpFields()
self.form_fields += form.Fields(
- copy_field(IQuestionLinkFAQForm['message']))
- self.form_fields['message'].field.title = _(
- 'Additional comment for question #%s' % self.context.id)
- self.form_fields['message'].custom_widget = self.custom_widget_message
-
- @action(_('Create and Link'), name='create_and_link')
+ copy_field(IQuestionLinkFAQForm["message"])
+ )
+ self.form_fields["message"].field.title = _(
+ "Additional comment for question #%s" % self.context.id
+ )
+ self.form_fields["message"].custom_widget = self.custom_widget_message
+
+ @action(_("Create and Link"), name="create_and_link")
def create_and_link_action(self, action, data):
"""Creates the FAQ and link it to the question."""
faq = self.faq_target.newFAQ(
- self.user, data['title'], data['content'],
- keywords=data['keywords'])
+ self.user,
+ data["title"],
+ data["content"],
+ keywords=data["keywords"],
+ )
# Append FAQ link to message.
- data['message'] += '\n' + self.getFAQMessageReference(faq)
+ data["message"] += "\n" + self.getFAQMessageReference(faq)
- self.context.linkFAQ(self.user, faq, data['message'])
+ self.context.linkFAQ(self.user, faq, data["message"])
# Redirect to the question.
self.next_url = canonical_url(self.context)
@@ -1323,21 +1412,21 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
select an element from this set using the radio buttons.
"""
- _messageNoValue = _('No existing FAQs are relevant')
+ _messageNoValue = _("No existing FAQs are relevant")
searchDisplayWidth = 30
- searchButtonLabel = _('Search')
+ searchButtonLabel = _("Search")
@property
def search_field_name(self):
"""Return the name to use for the search field."""
- return self.name + '-query'
+ return self.name + "-query"
@property
def search_button_name(self):
"""Return the name to use for the search button."""
- return self.name + '-search'
+ return self.name + "-search"
def renderValue(self, value):
"""Render the widget with the value."""
@@ -1383,11 +1472,13 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
else:
render = self.renderItem
- missing_item = render(count,
+ missing_item = render(
+ count,
self.translate(self._messageNoValue),
missing,
self.name,
- self.cssClass)
+ self.cssClass,
+ )
rendered_items.insert(0, missing_item)
count += 1
@@ -1396,7 +1487,8 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
def getSearchQuery(self):
"""Return the search query."""
return self.request.form_ng.getOne(
- self.search_field_name, self.default_query)
+ self.search_field_name, self.default_query
+ )
def renderTerm(self, index, term, selected):
"""Render a term as a radio button.
@@ -1404,47 +1496,51 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
The term's token is used as the radio button label. A link to the
term's value is added beside the button.
"""
- id = '%s.%s' % (self.name, index)
+ id = "%s.%s" % (self.name, index)
attributes = dict(
value=term.token,
id=id,
name=self.name,
cssClass=self.cssClass,
- type='radio')
+ type="radio",
+ )
if selected:
- attributes['checked'] = 'checked'
- input = renderElement('input', **attributes)
+ attributes["checked"] = "checked"
+ input = renderElement("input", **attributes)
button = structured(
'<label style="font-weight: normal">%s %s:</label>',
- structured(input), term.token)
+ structured(input),
+ term.token,
+ )
link = structured(
- '<a href="%s">%s</a>', canonical_url(term.value), term.title)
+ '<a href="%s">%s</a>', canonical_url(term.value), term.title
+ )
return "\n".join([button.escapedtext, link.escapedtext])
def renderSearchWidget(self):
"""Render the search entry field and the button."""
- return " ".join([
- self.renderSearchField(),
- self.renderSearchButton()])
+ return " ".join([self.renderSearchField(), self.renderSearchButton()])
def renderSearchField(self):
"""Render the search field."""
return renderElement(
- 'input',
+ "input",
type="text",
cssClass=self.cssClass,
value=self.getSearchQuery(),
name=self.search_field_name,
- size=self.searchDisplayWidth)
+ size=self.searchDisplayWidth,
+ )
def renderSearchButton(self):
"""Render the search button."""
return renderElement(
- 'input',
- type='submit',
+ "input",
+ type="submit",
name=self.search_button_name,
- value=self.searchButtonLabel)
+ value=self.searchButtonLabel,
+ )
class QuestionLinkFAQView(LinkFAQMixin, LaunchpadFormView):
@@ -1456,36 +1552,36 @@ class QuestionLinkFAQView(LinkFAQMixin, LaunchpadFormView):
custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
- label = _('Is this a FAQ?')
+ label = _("Is this a FAQ?")
@property
def page_title(self):
- return _('Is question #%s a FAQ?' % self.context.id)
+ return _("Is question #%s a FAQ?" % self.context.id)
@property
def initial_values(self):
"""Sets initial form values."""
return {
- 'faq': self.context.faq,
- 'message': self.default_message,
- }
+ "faq": self.context.faq,
+ "message": self.default_message,
+ }
def setUpWidgets(self):
"""Set the query on the search widget to the question title."""
super().setUpWidgets()
- self.widgets['faq'].default_query = self.context.title
+ self.widgets["faq"].default_query = self.context.title
def validate(self, data):
"""Make sure that the FAQ link was changed."""
- if self.context.faq == data.get('faq'):
- self.setFieldError('faq', _("You didn't modify the linked FAQ."))
+ if self.context.faq == data.get("faq"):
+ self.setFieldError("faq", _("You didn't modify the linked FAQ."))
- @action(_('Link to FAQ'), name="link")
+ @action(_("Link to FAQ"), name="link")
def link_action(self, action, data):
"""Link the selected FAQ to the question."""
- if data['faq'] is not None:
- data['message'] += '\n' + self.getFAQMessageReference(data['faq'])
- self.context.linkFAQ(self.user, data['faq'], data['message'])
+ if data["faq"] is not None:
+ data["message"] += "\n" + self.getFAQMessageReference(data["faq"])
+ self.context.linkFAQ(self.user, data["faq"], data["message"])
@property
def next_url(self):
diff --git a/lib/lp/answers/browser/questionsubscription.py b/lib/lp/answers/browser/questionsubscription.py
index 8883f85..cc2ea58 100644
--- a/lib/lp/answers/browser/questionsubscription.py
+++ b/lib/lp/answers/browser/questionsubscription.py
@@ -4,8 +4,8 @@
"""Views for QuestionSubscription."""
__all__ = [
- 'QuestionPortletSubscribersWithDetails',
- ]
+ "QuestionPortletSubscribersWithDetails",
+]
from lazr.delegates import delegate_to
from lazr.restful.interfaces import IWebServiceClientRequest
@@ -15,10 +15,7 @@ from zope.traversing.browser import absoluteURL
from lp.answers.interfaces.question import IQuestion
from lp.answers.interfaces.questionsubscription import IQuestionSubscription
from lp.services.propertycache import cachedproperty
-from lp.services.webapp import (
- canonical_url,
- LaunchpadView,
- )
+from lp.services.webapp import LaunchpadView, canonical_url
class QuestionPortletSubscribersWithDetails(LaunchpadView):
@@ -43,17 +40,17 @@ class QuestionPortletSubscribersWithDetails(LaunchpadView):
continue
subscriber = {
- 'name': person.name,
- 'display_name': person.displayname,
- 'web_link': canonical_url(person, rootsite='mainsite'),
- 'self_link': absoluteURL(person, self.api_request),
- 'is_team': person.is_team,
- 'can_edit': can_edit
- }
+ "name": person.name,
+ "display_name": person.displayname,
+ "web_link": canonical_url(person, rootsite="mainsite"),
+ "self_link": absoluteURL(person, self.api_request),
+ "is_team": person.is_team,
+ "can_edit": can_edit,
+ }
record = {
- 'subscriber': subscriber,
- 'subscription_level': 'Direct',
- }
+ "subscriber": subscriber,
+ "subscription_level": "Direct",
+ }
data.append(record)
return data
@@ -69,27 +66,27 @@ class QuestionPortletSubscribersWithDetails(LaunchpadView):
# Skip the current user viewing the page.
continue
subscriber = {
- 'name': person.name,
- 'display_name': person.displayname,
- 'web_link': canonical_url(person, rootsite='mainsite'),
- 'self_link': absoluteURL(person, self.api_request),
- 'is_team': person.is_team,
- 'can_edit': False,
- }
+ "name": person.name,
+ "display_name": person.displayname,
+ "web_link": canonical_url(person, rootsite="mainsite"),
+ "self_link": absoluteURL(person, self.api_request),
+ "is_team": person.is_team,
+ "can_edit": False,
+ }
record = {
- 'subscriber': subscriber,
- 'subscription_level': 'Indirect',
- }
+ "subscriber": subscriber,
+ "subscription_level": "Indirect",
+ }
data.append(record)
return dumps(data)
def render(self):
"""Override the default render() to return only JSON."""
- self.request.response.setHeader('content-type', 'application/json')
+ self.request.response.setHeader("content-type", "application/json")
return self.subscriber_data_js
-@delegate_to(IQuestionSubscription, context='subscription')
+@delegate_to(IQuestionSubscription, context="subscription")
class SubscriptionAttrDecorator:
"""A QuestionSubscription with added attributes for HTML/JS."""
@@ -98,4 +95,4 @@ class SubscriptionAttrDecorator:
@property
def css_name(self):
- return 'subscriber-%s' % self.subscription.person.id
+ return "subscriber-%s" % self.subscription.person.id
diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py
index 5eb0c08..488c860 100644
--- a/lib/lp/answers/browser/questiontarget.py
+++ b/lib/lp/answers/browser/questiontarget.py
@@ -4,46 +4,32 @@
"""IQuestionTarget browser views."""
__all__ = [
- 'AskAQuestionButtonPortlet',
- 'ManageAnswerContactView',
- 'SearchQuestionsView',
- 'QuestionCollectionByLanguageView',
- 'QuestionCollectionLatestQuestionsPortlet',
- 'QuestionCollectionMyQuestionsView',
- 'QuestionCollectionNeedAttentionView',
- 'QuestionCollectionAnswersMenu',
- 'QuestionTargetPortletAnswerContactsWithDetails',
- 'QuestionTargetTraversalMixin',
- 'QuestionTargetAnswersMenu',
- 'UserSupportLanguagesMixin',
- ]
+ "AskAQuestionButtonPortlet",
+ "ManageAnswerContactView",
+ "SearchQuestionsView",
+ "QuestionCollectionByLanguageView",
+ "QuestionCollectionLatestQuestionsPortlet",
+ "QuestionCollectionMyQuestionsView",
+ "QuestionCollectionNeedAttentionView",
+ "QuestionCollectionAnswersMenu",
+ "QuestionTargetPortletAnswerContactsWithDetails",
+ "QuestionTargetTraversalMixin",
+ "QuestionTargetAnswersMenu",
+ "UserSupportLanguagesMixin",
+]
from operator import attrgetter
from urllib.parse import urlencode
-from lazr.restful.interfaces import (
- IJSONRequestCache,
- IWebServiceClientRequest,
- )
+from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
from simplejson import dumps
from zope.browserpage import ViewPageTemplateFile
-from zope.component import (
- getMultiAdapter,
- getUtility,
- queryMultiAdapter,
- )
+from zope.component import getMultiAdapter, getUtility, queryMultiAdapter
from zope.formlib import form
from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import DropdownWidget
-from zope.schema import (
- Bool,
- Choice,
- List,
- )
-from zope.schema.vocabulary import (
- SimpleTerm,
- SimpleVocabulary,
- )
+from zope.schema import Bool, Choice, List
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
from zope.traversing.browser import absoluteURL
from lp import _
@@ -54,16 +40,12 @@ from lp.answers.interfaces.questioncollection import (
IQuestionCollection,
IQuestionSet,
ISearchableByQuestionOwner,
- )
+)
from lp.answers.interfaces.questiontarget import (
IQuestionTarget,
ISearchQuestionsForm,
- )
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- safe_action,
- )
+)
+from lp.app.browser.launchpadform import LaunchpadFormView, action, safe_action
from lp.app.enums import service_uses_launchpad
from lp.app.errors import NotFoundError
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -74,12 +56,12 @@ from lp.registry.interfaces.projectgroup import IProjectGroup
from lp.services.fields import PublicPersonChoice
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
- canonical_url,
Link,
+ canonical_url,
stepthrough,
stepto,
urlappend,
- )
+)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.batching import BatchNavigator
from lp.services.webapp.escaping import structured
@@ -88,7 +70,7 @@ from lp.services.worlddata.helpers import (
browser_languages,
is_english_variant,
preferred_or_request_languages,
- )
+)
from lp.services.worlddata.interfaces.language import ILanguageSet
@@ -98,7 +80,8 @@ class AskAQuestionButtonPortlet:
def __call__(self):
# Check if the context has an +addquestion view available...
if queryMultiAdapter(
- (self.context, self.request), name='+addquestion'):
+ (self.context, self.request), name="+addquestion"
+ ):
target = self.context
else:
# otherwise find an adapter to IQuestionTarget which will.
@@ -114,7 +97,8 @@ class AskAQuestionButtonPortlet:
</ul>
</div>
""" % canonical_url(
- target, view_name='+addquestion', rootsite='answers')
+ target, view_name="+addquestion", rootsite="answers"
+ )
class UserSupportLanguagesMixin:
@@ -153,7 +137,7 @@ class QuestionCollectionLatestQuestionsPortlet:
@property
def page_title(self):
- return 'Latest questions for %s' % (self.context.displayname)
+ return "Latest questions for %s" % (self.context.displayname)
@cachedproperty
def getLatestQuestions(self, quantity=5):
@@ -173,15 +157,17 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
schema = ISearchQuestionsForm
custom_widget_language = CustomWidgetFactory(
- LabeledMultiCheckBoxWidget, orientation='horizontal')
+ LabeledMultiCheckBoxWidget, orientation="horizontal"
+ )
custom_widget_sort = CustomWidgetFactory(
- DropdownWidget, cssClass='inlined-widget')
+ DropdownWidget, cssClass="inlined-widget"
+ )
custom_widget_status = CustomWidgetFactory(
- LabeledMultiCheckBoxWidget, orientation='horizontal')
+ LabeledMultiCheckBoxWidget, orientation="horizontal"
+ )
- default_template = ViewPageTemplateFile(
- '../templates/question-listing.pt')
- unknown_template = ViewPageTemplateFile('../templates/unknown-support.pt')
+ default_template = ViewPageTemplateFile("../templates/question-listing.pt")
+ unknown_template = ViewPageTemplateFile("../templates/unknown-support.pt")
@property
def template(self):
@@ -192,7 +178,8 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
if IQuestionSet.providedBy(self.context):
return self.default_template
involvement = getMultiAdapter(
- (self.context, self.request), name='+get-involved')
+ (self.context, self.request), name="+get-involved"
+ )
if service_uses_launchpad(involvement.answers_usage):
# Primary contexts that officially use answers have a
# search and listing presentation.
@@ -213,29 +200,36 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
if IQuestionSet.providedBy(self.context):
return _(
'Questions matching "${search_text}"',
- mapping=dict(search_text=self.search_text))
+ mapping=dict(search_text=self.search_text),
+ )
replacements = dict(
- context=self.context.displayname,
- search_text=self.search_text)
+ context=self.context.displayname, search_text=self.search_text
+ )
# Check if the set of selected status has a special title.
status_set_title = self.status_title_map.get(
- frozenset(self.status_filter))
+ frozenset(self.status_filter)
+ )
if status_set_title:
- replacements['status'] = status_set_title
+ replacements["status"] = status_set_title
if self.search_text:
- return _('${status} questions matching "${search_text}" '
- 'for ${context}', mapping=replacements)
+ return _(
+ '${status} questions matching "${search_text}" '
+ "for ${context}",
+ mapping=replacements,
+ )
else:
- return _('${status} questions for ${context}',
- mapping=replacements)
+ return _(
+ "${status} questions for ${context}", mapping=replacements
+ )
else:
if self.search_text:
- return _('Questions matching "${search_text}" for '
- '${context}', mapping=replacements)
+ return _(
+ 'Questions matching "${search_text}" for ' "${context}",
+ mapping=replacements,
+ )
else:
- return _('Questions for ${context}',
- mapping=replacements)
+ return _("Questions for ${context}", mapping=replacements)
label = page_title
@@ -273,26 +267,31 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
languages = set(self.user_support_languages)
languages.intersection_update(self.context_question_languages)
terms = []
- for lang in sorted(languages, key=attrgetter('code')):
+ for lang in sorted(languages, key=attrgetter("code")):
terms.append(SimpleTerm(lang, lang.code, lang.displayname))
return form.Fields(
- List(__name__='language',
- title=_('Languages filter'),
- value_type=Choice(vocabulary=SimpleVocabulary(terms)),
- required=False,
- default=self.user_support_languages,
- description=_(
- 'The languages to filter the search results by.')),
- render_context=self.render_context)
+ List(
+ __name__="language",
+ title=_("Languages filter"),
+ value_type=Choice(vocabulary=SimpleVocabulary(terms)),
+ required=False,
+ default=self.user_support_languages,
+ description=_(
+ "The languages to filter the search results by."
+ ),
+ ),
+ render_context=self.render_context,
+ )
def validate(self, data):
"""Validate hook.
This validation method checks that a valid status is submitted.
"""
- if not data.get('status', []):
+ if not data.get("status", []):
self.setFieldError(
- 'status', _('You must choose at least one status.'))
+ "status", _("You must choose at least one status.")
+ )
@cachedproperty
def status_title_map(self):
@@ -306,8 +305,9 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
for status in QuestionStatus.items:
mapping[frozenset([status])] = status.title
- mapping[frozenset(
- [QuestionStatus.ANSWERED, QuestionStatus.SOLVED])] = _('Answered')
+ mapping[
+ frozenset([QuestionStatus.ANSWERED, QuestionStatus.SOLVED])
+ ] = _("Answered")
return mapping
@@ -331,48 +331,63 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
provide their own implementation.
"""
if not IQuestionTarget.providedBy(self.context):
- return ''
+ return ""
language_counts = {}
questions = self.context.searchQuestions(
- unsupported=self.context, status=[QuestionStatus.OPEN])
+ unsupported=self.context, status=[QuestionStatus.OPEN]
+ )
for question in questions:
lang = question.language
language_counts[lang] = language_counts.get(lang, 0) + 1
if len(language_counts) == 0:
- return ''
- url = canonical_url(self.context, rootsite='answers')
- format = ('%s in <a href="' + url + '/+by-language'
- '?field.language=%s&field.status=Open">%s</a>')
- links = [format % (language_counts[key], key.code, key.englishname)
- for key in language_counts]
- return ', '.join(links)
+ return ""
+ url = canonical_url(self.context, rootsite="answers")
+ format = (
+ '%s in <a href="' + url + "/+by-language"
+ '?field.language=%s&field.status=Open">%s</a>'
+ )
+ links = [
+ format % (language_counts[key], key.code, key.englishname)
+ for key in language_counts
+ ]
+ return ", ".join(links)
@property
def empty_listing_message(self):
"""Message shown when there is no questions matching the filter."""
replacements = dict(
- context=self.context.displayname,
- search_text=self.search_text)
+ context=self.context.displayname, search_text=self.search_text
+ )
# Check if the set of selected status has a special title.
status_set_title = self.status_title_map.get(
- frozenset(self.status_filter))
+ frozenset(self.status_filter)
+ )
if status_set_title:
- replacements['status'] = status_set_title.lower()
+ replacements["status"] = status_set_title.lower()
if self.search_text:
- return _('There are no ${status} questions matching '
- '"${search_text}" for ${context}.',
- mapping=replacements)
+ return _(
+ "There are no ${status} questions matching "
+ '"${search_text}" for ${context}.',
+ mapping=replacements,
+ )
else:
- return _('There are no ${status} questions for '
- '${context}.', mapping=replacements)
+ return _(
+ "There are no ${status} questions for " "${context}.",
+ mapping=replacements,
+ )
else:
if self.search_text:
- return _('There are no questions matching "${search_text}" '
- 'for ${context} with the requested statuses.',
- mapping=replacements)
+ return _(
+ 'There are no questions matching "${search_text}" '
+ "for ${context} with the requested statuses.",
+ mapping=replacements,
+ )
else:
- return _('There are no questions for ${context} with '
- 'the requested statuses.', mapping=replacements)
+ return _(
+ "There are no questions for ${context} with "
+ "the requested statuses.",
+ mapping=replacements,
+ )
def getDefaultFilter(self):
"""Hook for subclass to provide a default search filter."""
@@ -382,17 +397,17 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
def search_text(self):
"""Search text used by the filter."""
if self.search_params:
- return self.search_params.get('search_text')
+ return self.search_params.get("search_text")
else:
- return self.getDefaultFilter().get('search_text')
+ return self.getDefaultFilter().get("search_text")
@property
def status_filter(self):
"""Set of statuses to filter the search with."""
if self.search_params:
- return set(self.search_params.get('status', []))
+ return set(self.search_params.get("status", []))
else:
- return set(self.getDefaultFilter().get('status', []))
+ return set(self.getDefaultFilter().get("status", []))
@cachedproperty
def context_question_languages(self):
@@ -408,13 +423,14 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
and that language is among the user's languages, we do not render
the language control because there are no choices to be made.
"""
- if not check_permission('launchpad.View', self.context):
+ if not check_permission("launchpad.View", self.context):
return False
languages = list(self.context_question_languages)
if len(languages) == 0:
return False
- elif (len(languages) == 1
- and languages[0] in self.user_support_languages):
+ elif (
+ len(languages) == 1 and languages[0] in self.user_support_languages
+ ):
return False
else:
return True
@@ -434,16 +450,23 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
@property
def matching_faqs_url(self):
"""Return the URL to use to display the list of matching FAQs."""
- assert self.matching_faqs_count > 0, (
- "can't call matching_faqs_url when matching_faqs_count == 0")
+ assert (
+ self.matching_faqs_count > 0
+ ), "can't call matching_faqs_url when matching_faqs_count == 0"
collection = IFAQCollection(self.context)
- return canonical_url(collection) + '/+faqs?' + urlencode({
- 'field.search_text': self.search_text.encode('utf-8'),
- 'field.actions.search': 'Search',
- })
+ return (
+ canonical_url(collection)
+ + "/+faqs?"
+ + urlencode(
+ {
+ "field.search_text": self.search_text.encode("utf-8"),
+ "field.actions.search": "Search",
+ }
+ )
+ )
@safe_action
- @action(_('Search'))
+ @action(_("Search"))
def search_action(self, action, data):
"""Action executed when the user clicked the search button.
@@ -452,9 +475,9 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
"""
self.search_params = dict(self.getDefaultFilter())
self.search_params.update(**data)
- search_text = self.search_params.get('search_text', None)
+ search_text = self.search_params.get("search_text", None)
if search_text is not None:
- self.search_params['search_text'] = search_text.strip()
+ self.search_params["search_text"] = search_text.strip()
def searchResults(self):
"""Return the questions corresponding to the search."""
@@ -468,8 +491,10 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
# ones defined in getDefaultFilter() which varies based on the
# concrete view class.
question_collection = IQuestionCollection(self.context)
- return BatchNavigator(question_collection.searchQuestions(
- **self.search_params), self.request)
+ return BatchNavigator(
+ question_collection.searchQuestions(**self.search_params),
+ self.request,
+ )
@property
def display_sourcepackage_column(self):
@@ -486,22 +511,25 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
# SQLObject can refetch the question, so we are comparing ids.
assert self.context.id == question.distribution.id, (
"The question.distribution (%s) must be equal to the context (%s)"
- % (question.distribution, self.context))
+ % (question.distribution, self.context)
+ )
if not question.sourcepackagename:
return "—"
else:
sourcepackage = self.context.getSourcePackage(
- question.sourcepackagename)
+ question.sourcepackagename
+ )
return '<a href="%s">%s</a>' % (
- canonical_url(sourcepackage, rootsite='answers'),
- question.sourcepackagename.name)
+ canonical_url(sourcepackage, rootsite="answers"),
+ question.sourcepackagename.name,
+ )
@property
def can_configure_answers(self):
"""Can the user configure answers for the `IQuestionTarget`."""
target = self.context
if IProduct.providedBy(target) or IDistribution.providedBy(target):
- return check_permission('launchpad.Edit', self.context)
+ return check_permission("launchpad.Edit", self.context)
else:
return False
@@ -520,13 +548,19 @@ class QuestionCollectionMyQuestionsView(SearchQuestionsView):
def page_title(self):
"""See `SearchQuestionsView`."""
if self.search_text:
- return _('Questions you asked matching "${search_text}" for '
- '${context}', mapping=dict(
- context=self.context.displayname,
- search_text=self.search_text))
+ return _(
+ 'Questions you asked matching "${search_text}" for '
+ "${context}",
+ mapping=dict(
+ context=self.context.displayname,
+ search_text=self.search_text,
+ ),
+ )
else:
- return _('Questions you asked about ${context}',
- mapping={'context': self.context.displayname})
+ return _(
+ "Questions you asked about ${context}",
+ mapping={"context": self.context.displayname},
+ )
label = page_title
@@ -534,13 +568,19 @@ class QuestionCollectionMyQuestionsView(SearchQuestionsView):
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
if self.search_text:
- return _("You didn't ask any questions matching "
- '"${search_text}" for ${context}.', mapping=dict(
- context=self.context.displayname,
- search_text=self.search_text))
+ return _(
+ "You didn't ask any questions matching "
+ '"${search_text}" for ${context}.',
+ mapping=dict(
+ context=self.context.displayname,
+ search_text=self.search_text,
+ ),
+ )
else:
- return _("You didn't ask any questions about ${context}.",
- mapping={'context': self.context.displayname})
+ return _(
+ "You didn't ask any questions about ${context}.",
+ mapping={"context": self.context.displayname},
+ )
def getDefaultFilter(self):
"""See `SearchQuestionsView`."""
@@ -561,13 +601,19 @@ class QuestionCollectionNeedAttentionView(SearchQuestionsView):
def page_title(self):
"""See `SearchQuestionsView`."""
if self.search_text:
- return _('Questions matching "${search_text}" needing your '
- 'attention for ${context}', mapping=dict(
- context=self.context.displayname,
- search_text=self.search_text))
+ return _(
+ 'Questions matching "${search_text}" needing your '
+ "attention for ${context}",
+ mapping=dict(
+ context=self.context.displayname,
+ search_text=self.search_text,
+ ),
+ )
else:
- return _('Questions needing your attention for ${context}',
- mapping={'context': self.context.displayname})
+ return _(
+ "Questions needing your attention for ${context}",
+ mapping={"context": self.context.displayname},
+ )
label = page_title
@@ -575,29 +621,38 @@ class QuestionCollectionNeedAttentionView(SearchQuestionsView):
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
if self.search_text:
- return _('No questions matching "${search_text}" need your '
- 'attention for ${context}.', mapping=dict(
- context=self.context.displayname,
- search_text=self.search_text))
+ return _(
+ 'No questions matching "${search_text}" need your '
+ "attention for ${context}.",
+ mapping=dict(
+ context=self.context.displayname,
+ search_text=self.search_text,
+ ),
+ )
else:
- return _("No questions need your attention for ${context}.",
- mapping={'context': self.context.displayname})
+ return _(
+ "No questions need your attention for ${context}.",
+ mapping={"context": self.context.displayname},
+ )
def getDefaultFilter(self):
"""See `SearchQuestionsView`."""
- return dict(needs_attention_from=self.user,
- language=self.user_support_languages)
+ return dict(
+ needs_attention_from=self.user,
+ language=self.user_support_languages,
+ )
class QuestionCollectionByLanguageView(SearchQuestionsView):
"""Search for questions in a specific language.
- This view displays questions that are asked in the specified language
- for the QuestionTarget context.
- """
+ This view displays questions that are asked in the specified language
+ for the QuestionTarget context.
+ """
custom_widget_language = CustomWidgetFactory(
- LabeledMultiCheckBoxWidget, visible=False)
+ LabeledMultiCheckBoxWidget, visible=False
+ )
# No point showing a matching FAQs link on this report.
matching_faqs_count = 0
@@ -611,43 +666,53 @@ class QuestionCollectionByLanguageView(SearchQuestionsView):
SearchQuestionsView.__init__(self, context, request)
# Language is intrinsic to this view; it manages the language
# field without the help of formlib.
- lang_code = request.get('field.language', '')
+ lang_code = request.get("field.language", "")
try:
self.language = getUtility(ILanguageSet)[lang_code]
except NotFoundError:
self.request.response.redirect(
- canonical_url(self.context, rootsite='answers'))
+ canonical_url(self.context, rootsite="answers")
+ )
@property
def page_title(self):
"""See `SearchQuestionsView`."""
- mapping = dict(context=self.context.displayname,
- search_text=self.search_text,
- language=self.language.englishname)
+ mapping = dict(
+ context=self.context.displayname,
+ search_text=self.search_text,
+ language=self.language.englishname,
+ )
if self.search_text:
- return _('${language} questions matching "${search_text}" '
- 'in ${context}',
- mapping=mapping)
+ return _(
+ '${language} questions matching "${search_text}" '
+ "in ${context}",
+ mapping=mapping,
+ )
else:
- return _('${language} questions in ${context}',
- mapping=mapping)
+ return _("${language} questions in ${context}", mapping=mapping)
label = page_title
@property
def empty_listing_message(self):
"""See `SearchQuestionsView`."""
- mapping = dict(context=self.context.displayname,
- search_text=self.search_text,
- language=self.language.englishname)
+ mapping = dict(
+ context=self.context.displayname,
+ search_text=self.search_text,
+ language=self.language.englishname,
+ )
if self.search_text:
- return _('No ${language} questions matching "${search_text}" '
- 'in ${context} for the selected status.',
- mapping=mapping)
+ return _(
+ 'No ${language} questions matching "${search_text}" '
+ "in ${context} for the selected status.",
+ mapping=mapping,
+ )
else:
- return _('No ${language} questions in ${context} for the '
- 'selected status.',
- mapping=mapping)
+ return _(
+ "No ${language} questions in ${context} for the "
+ "selected status.",
+ mapping=mapping,
+ )
@property
def show_language_control(self):
@@ -669,7 +734,7 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
@property
def page_title(self):
- return 'Answer contact for %s' % self.context.title
+ return "Answer contact for %s" % self.context.title
label = page_title
custom_widget_answer_contact_teams = LabeledMultiCheckBoxWidget
@@ -678,86 +743,114 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
"""See `LaunchpadFormView`."""
self.form_fields = form.Fields(
self._createUserAnswerContactField(),
- self._createTeamAnswerContactsField())
+ self._createTeamAnswerContactsField(),
+ )
def _createUserAnswerContactField(self):
"""Create the want_to_be_answer_contact field."""
return Bool(
- __name__='want_to_be_answer_contact',
- title=_("I want to be an answer contact for $context",
- mapping=dict(context=self.context.displayname)),
- required=False)
+ __name__="want_to_be_answer_contact",
+ title=_(
+ "I want to be an answer contact for $context",
+ mapping=dict(context=self.context.displayname),
+ ),
+ required=False,
+ )
def _createTeamAnswerContactsField(self):
"""Create a list of teams the user is an administrator of."""
- sort_key = attrgetter('displayname')
+ sort_key = attrgetter("displayname")
terms = []
for team in sorted(self.administrated_teams, key=sort_key):
terms.append(SimpleTerm(team, team.name, team.displayname))
public_person_choice = PublicPersonChoice(
- vocabulary=SimpleVocabulary(terms))
+ vocabulary=SimpleVocabulary(terms)
+ )
return form.FormField(
List(
- __name__='answer_contact_teams',
- title=_("Let the following teams be an answer contact for "
- "$context",
- mapping=dict(context=self.context.displayname)),
+ __name__="answer_contact_teams",
+ title=_(
+ "Let the following teams be an answer contact for "
+ "$context",
+ mapping=dict(context=self.context.displayname),
+ ),
value_type=public_person_choice,
- required=False))
+ required=False,
+ )
+ )
@property
def initial_values(self):
"""Return a dictionary of the default values for the form_fields."""
user = self.user
answer_contacts = self.context.direct_answer_contacts
- answer_contact_teams = set(
- answer_contacts).intersection(self.administrated_teams)
+ answer_contact_teams = set(answer_contacts).intersection(
+ self.administrated_teams
+ )
return {
- 'want_to_be_answer_contact': user in answer_contacts,
- 'answer_contact_teams': list(answer_contact_teams),
- }
+ "want_to_be_answer_contact": user in answer_contacts,
+ "answer_contact_teams": list(answer_contact_teams),
+ }
- @action(_('Continue'), name='update')
+ @action(_("Continue"), name="update")
def update_action(self, action, data):
"""Update the answer contact registration."""
- want_to_be_answer_contact = data['want_to_be_answer_contact']
- answer_contact_teams = data.get('answer_contact_teams', [])
+ want_to_be_answer_contact = data["want_to_be_answer_contact"]
+ answer_contact_teams = data.get("answer_contact_teams", [])
response = self.request.response
- replacements = {'context': self.context.displayname}
+ replacements = {"context": self.context.displayname}
if want_to_be_answer_contact:
self._updatePreferredLanguages(self.user)
if self.context.addAnswerContact(self.user, self.user):
response.addNotification(
- _('You have been added as an answer contact for '
- '$context.', mapping=replacements))
+ _(
+ "You have been added as an answer contact for "
+ "$context.",
+ mapping=replacements,
+ )
+ )
else:
if self.context.removeAnswerContact(self.user, self.user):
response.addNotification(
- _('You have been removed as an answer contact for '
- '$context.', mapping=replacements))
+ _(
+ "You have been removed as an answer contact for "
+ "$context.",
+ mapping=replacements,
+ )
+ )
for team in self.administrated_teams:
- replacements['teamname'] = team.displayname
+ replacements["teamname"] = team.displayname
if team in answer_contact_teams:
self._updatePreferredLanguages(team)
if self.context.addAnswerContact(team, self.user):
response.addNotification(
- _('$teamname has been added as an answer contact '
- 'for $context.', mapping=replacements))
+ _(
+ "$teamname has been added as an answer contact "
+ "for $context.",
+ mapping=replacements,
+ )
+ )
else:
if self.context.removeAnswerContact(team, self.user):
response.addNotification(
- _('$teamname has been removed as an answer contact '
- 'for $context.', mapping=replacements))
+ _(
+ "$teamname has been removed as an answer contact "
+ "for $context.",
+ mapping=replacements,
+ )
+ )
- self.next_url = canonical_url(self.context, rootsite='answers')
+ self.next_url = canonical_url(self.context, rootsite="answers")
@property
def administrated_teams(self):
from lp.registry.browser.person import RestrictedMembershipsPersonView
- restricted_view = RestrictedMembershipsPersonView(self.user,
- self.request)
+
+ restricted_view = RestrictedMembershipsPersonView(
+ self.user, self.request
+ )
return restricted_view.administrated_teams
def _updatePreferredLanguages(self, person_or_team):
@@ -776,12 +869,16 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
english = getUtility(ILaunchpadCelebrities).english
if person_or_team.is_team:
person_or_team.addLanguage(english)
- team_mapping = {'name': person_or_team.name,
- 'displayname': person_or_team.displayname}
- msgid = _("English was added to ${displayname}'s "
- '<a href="/~${name}/+editlanguages">preferred '
- 'languages</a>.',
- mapping=team_mapping)
+ team_mapping = {
+ "name": person_or_team.name,
+ "displayname": person_or_team.displayname,
+ }
+ msgid = _(
+ "English was added to ${displayname}'s "
+ '<a href="/~${name}/+editlanguages">preferred '
+ "languages</a>.",
+ mapping=team_mapping,
+ )
response.addNotification(structured(msgid))
else:
if len(browser_languages(self.request)) > 0:
@@ -790,11 +887,13 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
languages = [english]
for language in languages:
person_or_team.addLanguage(language)
- language_str = ', '.join([lang.displayname for lang in languages])
- msgid = _('<a href="/people/+me/+editlanguages">Your preferred '
- 'languages</a> were updated to include your browser '
- 'languages: $languages.',
- mapping={'languages': language_str})
+ language_str = ", ".join([lang.displayname for lang in languages])
+ msgid = _(
+ '<a href="/people/+me/+editlanguages">Your preferred '
+ "languages</a> were updated to include your browser "
+ "languages: $languages.",
+ mapping={"languages": language_str},
+ )
response.addNotification(structured(msgid))
@@ -808,11 +907,12 @@ class QuestionTargetPortletAnswerContacts(LaunchpadView):
def initialize(self):
cache = IJSONRequestCache(self.request).objects
context_url_data = {
- 'web_link': canonical_url(self.context, rootsite='mainsite'),
- 'self_link': absoluteURL(self.context, self.api_request),
- }
- cache[self.context.name + '_answer_portlet_url_data'] = (
- context_url_data)
+ "web_link": canonical_url(self.context, rootsite="mainsite"),
+ "self_link": absoluteURL(self.context, self.api_request),
+ }
+ cache[
+ self.context.name + "_answer_portlet_url_data"
+ ] = context_url_data
class QuestionTargetPortletAnswerContactsWithDetails(LaunchpadView):
@@ -832,22 +932,23 @@ class QuestionTargetPortletAnswerContactsWithDetails(LaunchpadView):
answer_contacts = list(questiontarget.direct_answer_contacts)
for person in answer_contacts:
can_edit = questiontarget.canUserAlterAnswerContact(
- person, self.user)
+ person, self.user
+ )
if person.private and not can_edit:
# Skip private teams user is not a member of.
continue
answer_contact = {
- 'name': person.name,
- 'display_name': person.displayname,
- 'web_link': canonical_url(person, rootsite='mainsite'),
- 'self_link': absoluteURL(person, self.api_request),
- 'is_team': person.is_team,
- 'can_edit': can_edit
- }
+ "name": person.name,
+ "display_name": person.displayname,
+ "web_link": canonical_url(person, rootsite="mainsite"),
+ "self_link": absoluteURL(person, self.api_request),
+ "is_team": person.is_team,
+ "can_edit": can_edit,
+ }
record = {
- 'subscriber': answer_contact,
- }
+ "subscriber": answer_contact,
+ }
data.append(record)
return data
@@ -860,14 +961,14 @@ class QuestionTargetPortletAnswerContactsWithDetails(LaunchpadView):
def render(self):
"""Override the default render() to return only JSON."""
- self.request.response.setHeader('content-type', 'application/json')
+ self.request.response.setHeader("content-type", "application/json")
return self.answercontact_data_js
class QuestionTargetTraversalMixin:
"""Navigation mixin for IQuestionTarget."""
- @stepthrough('+question')
+ @stepthrough("+question")
def traverse_question(self, name):
"""Return the question."""
# questions should be ints
@@ -885,9 +986,10 @@ class QuestionTargetTraversalMixin:
if question is None:
raise NotFoundError(name)
return self.redirectSubTree(
- canonical_url(question, request=self.request))
+ canonical_url(question, request=self.request)
+ )
- @stepto('+ticket')
+ @stepto("+ticket")
def redirect_ticket(self):
"""Use RedirectionNavigation to redirect to +question.
@@ -895,68 +997,79 @@ class QuestionTargetTraversalMixin:
"""
target = urlappend(
canonical_url(
- self.context, request=self.request, rootsite='answers'),
- '+question')
+ self.context, request=self.request, rootsite="answers"
+ ),
+ "+question",
+ )
return self.redirectSubTree(target)
class QuestionCollectionAnswersMenu(FAQCollectionMenu):
"""Base menu definition for QuestionCollection searchable by owner."""
+
# XXX flacoste 2007-07-08 bug=125851:
# This menu shouldn't "extend" FAQCollectionMenu.
# architecture. But this is needed because of limitations in the current
# menu Menu should be built by merging all menus applying to the context
# object (-based on the interfaces it provides).
usedfor = ISearchableByQuestionOwner
- facet = 'answers'
+ facet = "answers"
links = FAQCollectionMenu.links + [
- 'open', 'answered', 'myrequests', 'need_attention']
+ "open",
+ "answered",
+ "myrequests",
+ "need_attention",
+ ]
- def makeSearchLink(self, statuses, sort='by relevancy'):
+ def makeSearchLink(self, statuses, sort="by relevancy"):
"""Return the search parameters for a search link."""
return "+questions?" + urlencode(
- {'field.status': statuses,
- 'field.sort': sort,
- 'field.search_text': '',
- 'field.actions.search': 'Search',
- 'field.status': statuses}, doseq=True)
+ {
+ "field.status": statuses,
+ "field.sort": sort,
+ "field.search_text": "",
+ "field.actions.search": "Search",
+ "field.status": statuses,
+ },
+ doseq=True,
+ )
def open(self):
"""Return a Link that opens a question."""
- url = self.makeSearchLink('Open', sort='recently updated first')
- return Link(url, 'Open', icon='question')
+ url = self.makeSearchLink("Open", sort="recently updated first")
+ return Link(url, "Open", icon="question")
def answered(self):
"""Return a Link to display questions that are open."""
- text = 'Answered'
+ text = "Answered"
return Link(
- self.makeSearchLink(['Answered', 'Solved']),
- text, icon='question')
+ self.makeSearchLink(["Answered", "Solved"]), text, icon="question"
+ )
def myrequests(self):
"""Return a Link to display the user's questions."""
- text = 'My questions'
- return Link('+myquestions', text, icon='question')
+ text = "My questions"
+ return Link("+myquestions", text, icon="question")
def need_attention(self):
"""Return a Link to display questions that need attention."""
- text = 'Need attention'
- return Link('+need-attention', text, icon='question')
+ text = "Need attention"
+ return Link("+need-attention", text, icon="question")
class QuestionTargetAnswersMenu(QuestionCollectionAnswersMenu):
"""Base menu definition for QuestionTargets."""
usedfor = IQuestionTarget
- facet = 'answers'
- links = QuestionCollectionAnswersMenu.links + ['new', 'answer_contact']
+ facet = "answers"
+ links = QuestionCollectionAnswersMenu.links + ["new", "answer_contact"]
def new(self):
"""Return a link to ask a question."""
- text = 'Ask a question'
- return Link('+addquestion', text, icon='add')
+ text = "Ask a question"
+ return Link("+addquestion", text, icon="add")
def answer_contact(self):
"""Return a link to the manage answer contact view."""
- text = 'Set answer contact'
- return Link('+answer-contact', text, icon='edit')
+ text = "Set answer contact"
+ return Link("+answer-contact", text, icon="edit")
diff --git a/lib/lp/answers/browser/tests/test_breadcrumbs.py b/lib/lp/answers/browser/tests/test_breadcrumbs.py
index cca8dd6..23e6b60 100644
--- a/lib/lp/answers/browser/tests/test_breadcrumbs.py
+++ b/lib/lp/answers/browser/tests/test_breadcrumbs.py
@@ -7,7 +7,8 @@ from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersFacet(
- BaseBreadcrumbTestCase):
+ BaseBreadcrumbTestCase
+):
"""Test Breadcrumbs for IQuestionTarget, IProjectGroup and IPerson on the
answers vhost.
@@ -20,33 +21,34 @@ class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersFacet(
super().setUp()
self.person = self.factory.makePerson()
self.person_questions_url = canonical_url(
- self.person, rootsite='answers')
+ self.person, rootsite="answers"
+ )
self.product = self.factory.makeProduct()
self.product_questions_url = canonical_url(
- self.product, rootsite='answers')
+ self.product, rootsite="answers"
+ )
self.project = self.factory.makeProject()
self.project_questions_url = canonical_url(
- self.project, rootsite='answers')
+ self.project, rootsite="answers"
+ )
def test_product(self):
- crumbs = self.getBreadcrumbsForObject(
- self.product, rootsite='answers')
+ crumbs = self.getBreadcrumbsForObject(self.product, rootsite="answers")
last_crumb = crumbs[-1]
self.assertEqual(last_crumb.url, self.product_questions_url)
- self.assertEqual(last_crumb.text, 'Questions')
+ self.assertEqual(last_crumb.text, "Questions")
def test_project(self):
- crumbs = self.getBreadcrumbsForObject(
- self.project, rootsite='answers')
+ crumbs = self.getBreadcrumbsForObject(self.project, rootsite="answers")
last_crumb = crumbs[-1]
self.assertEqual(last_crumb.url, self.project_questions_url)
- self.assertEqual(last_crumb.text, 'Questions')
+ self.assertEqual(last_crumb.text, "Questions")
def test_person(self):
- crumbs = self.getBreadcrumbsForObject(self.person, rootsite='answers')
+ crumbs = self.getBreadcrumbsForObject(self.person, rootsite="answers")
last_crumb = crumbs[-1]
self.assertEqual(last_crumb.url, self.person_questions_url)
- self.assertEqual(last_crumb.text, 'Questions')
+ self.assertEqual(last_crumb.text, "Questions")
class TestAnswersBreadcrumb(BaseBreadcrumbTestCase):
@@ -59,15 +61,16 @@ class TestAnswersBreadcrumb(BaseBreadcrumbTestCase):
def test_question(self):
self.question = self.factory.makeQuestion(
- target=self.product, title='Seeds are hard to chew')
- self.question_url = canonical_url(self.question, rootsite='answers')
+ target=self.product, title="Seeds are hard to chew"
+ )
+ self.question_url = canonical_url(self.question, rootsite="answers")
crumbs = self.getBreadcrumbsForObject(self.question)
last_crumb = crumbs[-1]
- self.assertEqual(last_crumb.text, 'Question #%d' % self.question.id)
+ self.assertEqual(last_crumb.text, "Question #%d" % self.question.id)
def test_faq(self):
- self.faq = self.factory.makeFAQ(target=self.product, title='Seedless')
- self.faq_url = canonical_url(self.faq, rootsite='answers')
+ self.faq = self.factory.makeFAQ(target=self.product, title="Seedless")
+ self.faq_url = canonical_url(self.faq, rootsite="answers")
crumbs = self.getBreadcrumbsForObject(self.faq)
last_crumb = crumbs[-1]
- self.assertEqual(last_crumb.text, 'FAQ #%d' % self.faq.id)
+ self.assertEqual(last_crumb.text, "FAQ #%d" % self.faq.id)
diff --git a/lib/lp/answers/browser/tests/test_menus.py b/lib/lp/answers/browser/tests/test_menus.py
index a0fdf48..622faee 100644
--- a/lib/lp/answers/browser/tests/test_menus.py
+++ b/lib/lp/answers/browser/tests/test_menus.py
@@ -3,21 +3,16 @@
from zope.component import getUtility
-from lp.answers.browser.question import (
- QuestionEditMenu,
- QuestionExtrasMenu,
- )
+from lp.answers.browser.question import QuestionEditMenu, QuestionExtrasMenu
from lp.services.worlddata.interfaces.language import ILanguageSet
-from lp.testing import (
- login_person,
- TestCaseWithFactory,
- )
+from lp.testing import TestCaseWithFactory, login_person
from lp.testing.layers import DatabaseFunctionalLayer
from lp.testing.menu import check_menu_links
class TestQuestionMenus(TestCaseWithFactory):
"""Test specification menus links."""
+
layer = DatabaseFunctionalLayer
def setUp(self):
@@ -38,12 +33,12 @@ class TestQuestionMenus(TestCaseWithFactory):
# A question without a linked FAQ has an 'add' icon.
menu = QuestionExtrasMenu(self.question)
link = menu.linkfaq()
- self.assertEqual('add', link.icon)
+ self.assertEqual("add", link.icon)
# A question with a linked FAQ has an 'edit' icon.
- self.person.addLanguage(getUtility(ILanguageSet)['en'])
+ self.person.addLanguage(getUtility(ILanguageSet)["en"])
target = self.question.target
target.addAnswerContact(self.person, self.person)
faq = self.factory.makeFAQ(target=target)
- self.question.linkFAQ(self.person, faq, 'message')
+ self.question.linkFAQ(self.person, faq, "message")
link = menu.linkfaq()
- self.assertEqual('edit', link.icon)
+ self.assertEqual("edit", link.icon)
diff --git a/lib/lp/answers/browser/tests/test_question.py b/lib/lp/answers/browser/tests/test_question.py
index 9178619..0619fc3 100644
--- a/lib/lp/answers/browser/tests/test_question.py
+++ b/lib/lp/answers/browser/tests/test_question.py
@@ -11,11 +11,7 @@ from lp.answers.browser.question import QuestionTargetWidget
from lp.answers.interfaces.question import IQuestion
from lp.app.enums import ServiceUsage
from lp.services.webapp.servers import LaunchpadTestRequest
-from lp.testing import (
- login_person,
- person_logged_in,
- TestCaseWithFactory,
- )
+from lp.testing import TestCaseWithFactory, login_person, person_logged_in
from lp.testing.layers import DatabaseFunctionalLayer
from lp.testing.views import create_initialized_view
@@ -31,40 +27,48 @@ class TestQuestionAddView(TestCaseWithFactory):
self.user = self.factory.makePerson()
login_person(self.user)
- def getSearchForm(self, title, language='en'):
+ def getSearchForm(self, title, language="en"):
return {
- 'field.title': title,
- 'field.language': language,
- 'field.actions.continue': 'Continue',
- }
+ "field.title": title,
+ "field.language": language,
+ "field.actions.continue": "Continue",
+ }
def test_question_title_within_max_display_width(self):
# Titles (summary in the view) less than 250 characters are accepted.
- form = self.getSearchForm('123456789 ' * 10)
+ form = self.getSearchForm("123456789 " * 10)
view = create_initialized_view(
- self.question_target, name='+addquestion', form=form,
- principal=self.user)
+ self.question_target,
+ name="+addquestion",
+ form=form,
+ principal=self.user,
+ )
self.assertEqual([], view.errors)
def test_question_title_exceeds_max_display_width(self):
# Titles (summary in the view) cannot exceed 250 characters.
- form = self.getSearchForm('123456789 ' * 26)
+ form = self.getSearchForm("123456789 " * 26)
view = create_initialized_view(
- self.question_target, name='+addquestion', form=form,
- principal=self.user)
+ self.question_target,
+ name="+addquestion",
+ form=form,
+ principal=self.user,
+ )
self.assertEqual(1, len(view.errors))
self.assertEqual(
- 'The summary cannot exceed 250 characters.', view.errors[0])
+ "The summary cannot exceed 250 characters.", view.errors[0]
+ )
def test_context_uses_answers(self):
# If a target doesn't use answers, it doesn't provide the form.
- #logout()
+ # logout()
owner = removeSecurityProxy(self.question_target).owner
with person_logged_in(owner):
self.question_target.answers_usage = ServiceUsage.NOT_APPLICABLE
login_person(self.user)
view = create_initialized_view(
- self.question_target, name='+addquestion', principal=self.user)
+ self.question_target, name="+addquestion", principal=self.user
+ )
self.assertFalse(view.context_uses_answers)
contents = view.render()
msg = "<strong>does not use</strong> Launchpad as its answer forum"
@@ -78,21 +82,21 @@ class QuestionEditViewTestCase(TestCaseWithFactory):
def getForm(self, question):
if question.assignee is None:
- assignee = ''
+ assignee = ""
else:
assignee = question.assignee.name
return {
- 'field.title': question.title,
- 'field.description': question.description,
- 'field.language': question.language.code,
- 'field.assignee': assignee,
- 'field.target': 'product',
- 'field.target.distribution': '',
- 'field.target.package': '',
- 'field.target.product': question.target.name,
- 'field.whiteboard': question.whiteboard,
- 'field.actions.change': 'Change',
- }
+ "field.title": question.title,
+ "field.description": question.description,
+ "field.language": question.language.code,
+ "field.assignee": assignee,
+ "field.target": "product",
+ "field.target.distribution": "",
+ "field.target.package": "",
+ "field.target.product": question.target.name,
+ "field.whiteboard": question.whiteboard,
+ "field.actions.change": "Change",
+ }
def test_retarget_with_other_changed(self):
# Retargeting must be the last change made to the question
@@ -103,20 +107,21 @@ class QuestionEditViewTestCase(TestCaseWithFactory):
other_target = self.factory.makeProduct()
login_person(target.owner)
form = self.getForm(question)
- form['field.whiteboard'] = 'comment'
- form['field.target.product'] = other_target.name
- view = create_initialized_view(question, name='+edit', form=form)
+ form["field.whiteboard"] = "comment"
+ form["field.target.product"] = other_target.name
+ view = create_initialized_view(question, name="+edit", form=form)
self.assertEqual([], view.errors)
self.assertEqual(other_target, question.target)
- self.assertEqual('comment', question.whiteboard)
+ self.assertEqual("comment", question.whiteboard)
class QuestionTargetWidgetTestCase(TestCaseWithFactory):
"""Test that QuestionTargetWidgetTestCase behaves as expected."""
+
layer = DatabaseFunctionalLayer
def getWidget(self, question):
- field = IQuestion['target']
+ field = IQuestion["target"]
bound_field = field.bind(question)
request = LaunchpadTestRequest()
return QuestionTargetWidget(bound_field, request)
@@ -132,7 +137,8 @@ class QuestionTargetWidgetTestCase(TestCaseWithFactory):
self.assertEqual(None, vocabulary.distribution)
self.assertFalse(
distribution in vocabulary,
- "Vocabulary contains distros that do not use Launchpad Answers.")
+ "Vocabulary contains distros that do not use Launchpad Answers.",
+ )
def test_getDistributionVocabulary_with_distribution_question(self):
# The vocabulary does not contain distros that do not use
@@ -145,7 +151,9 @@ class QuestionTargetWidgetTestCase(TestCaseWithFactory):
self.assertEqual(distribution, vocabulary.distribution)
self.assertTrue(
distribution in vocabulary,
- "Vocabulary missing context distribution.")
+ "Vocabulary missing context distribution.",
+ )
self.assertFalse(
other_distribution in vocabulary,
- "Vocabulary contains distros that do not use Launchpad Answers.")
+ "Vocabulary contains distros that do not use Launchpad Answers.",
+ )
diff --git a/lib/lp/answers/browser/tests/test_questionmessages.py b/lib/lp/answers/browser/tests/test_questionmessages.py
index bd4f5d6..0e1bb02 100644
--- a/lib/lp/answers/browser/tests/test_questionmessages.py
+++ b/lib/lp/answers/browser/tests/test_questionmessages.py
@@ -10,17 +10,15 @@ from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.coop.answersbugs.visibility import (
TestHideMessageControlMixin,
TestMessageVisibilityMixin,
- )
-from lp.testing import (
- BrowserTestCase,
- person_logged_in,
- )
+)
+from lp.testing import BrowserTestCase, person_logged_in
from lp.testing.layers import DatabaseFunctionalLayer
from lp.testing.pages import find_tag_by_id
class TestQuestionMessageVisibility(
- BrowserTestCase, TestMessageVisibilityMixin):
+ BrowserTestCase, TestMessageVisibilityMixin
+):
layer = DatabaseFunctionalLayer
@@ -38,18 +36,18 @@ class TestQuestionMessageVisibility(
def getView(self, context, user=None, no_login=False):
"""Required by the mixin."""
view = self.getViewBrowser(
- context=context,
- user=user,
- no_login=no_login)
+ context=context, user=user, no_login=no_login
+ )
return view
class TestHideQuestionMessageControls(
- BrowserTestCase, TestHideMessageControlMixin):
+ BrowserTestCase, TestHideMessageControlMixin
+):
layer = DatabaseFunctionalLayer
- control_text = 'mark-spam-0'
+ control_text = "mark-spam-0"
def getContext(self, comment_owner=None):
"""Required by the mixin."""
@@ -64,9 +62,8 @@ class TestHideQuestionMessageControls(
def getView(self, context, user=None, no_login=False):
"""Required by the mixin."""
view = self.getViewBrowser(
- context=context,
- user=user,
- no_login=no_login)
+ context=context, user=user, no_login=no_login
+ )
return view
def test_comment_owner_sees_hide_control(self):
diff --git a/lib/lp/answers/browser/tests/test_questionsubscription_views.py b/lib/lp/answers/browser/tests/test_questionsubscription_views.py
index 8c7002e..fc0b918 100644
--- a/lib/lp/answers/browser/tests/test_questionsubscription_views.py
+++ b/lib/lp/answers/browser/tests/test_questionsubscription_views.py
@@ -14,10 +14,10 @@ from zope.traversing.browser import absoluteURL
from lp.registry.interfaces.person import IPersonSet
from lp.services.webapp import canonical_url
from lp.testing import (
- person_logged_in,
StormStatementRecorder,
TestCaseWithFactory,
- )
+ person_logged_in,
+)
from lp.testing.layers import LaunchpadFunctionalLayer
from lp.testing.matchers import HasQueryCount
from lp.testing.sampledata import ADMIN_EMAIL
@@ -26,18 +26,19 @@ from lp.testing.views import create_view
class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
"""Tests for IQuestion:+portlet-subscribers-details view."""
+
layer = LaunchpadFunctionalLayer
def test_content_type(self):
question = self.factory.makeQuestion()
# It works even for anonymous users, so no log-in is needed.
- view = create_view(question, '+portlet-subscribers-details')
+ view = create_view(question, "+portlet-subscribers-details")
view.render()
self.assertEqual(
- view.request.response.getHeader('content-type'),
- 'application/json')
+ view.request.response.getHeader("content-type"), "application/json"
+ )
def _makeQuestionWithNoSubscribers(self):
question = self.factory.makeQuestion()
@@ -48,7 +49,7 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
def test_data_no_subscriptions(self):
question = self._makeQuestionWithNoSubscribers()
- view = create_view(question, '+portlet-subscribers-details')
+ view = create_view(question, "+portlet-subscribers-details")
self.assertEqual([], json.loads(view.subscriber_data_js))
def test_data_person_subscription(self):
@@ -57,37 +58,40 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
# subscribers_list.js subscribers loading.
question = self._makeQuestionWithNoSubscribers()
subscriber = self.factory.makePerson(
- name='user', displayname='Subscriber Name')
+ name="user", displayname="Subscriber Name"
+ )
with person_logged_in(subscriber):
question.subscribe(subscriber, subscriber)
- view = create_view(question, '+portlet-subscribers-details')
+ view = create_view(question, "+portlet-subscribers-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'user',
- 'display_name': 'Subscriber Name',
- 'is_team': False,
- 'can_edit': False,
- 'web_link': canonical_url(subscriber),
- 'self_link': absoluteURL(subscriber, api_request)
- },
- 'subscription_level': "Direct",
- }
+ "subscriber": {
+ "name": "user",
+ "display_name": "Subscriber Name",
+ "is_team": False,
+ "can_edit": False,
+ "web_link": canonical_url(subscriber),
+ "self_link": absoluteURL(subscriber, api_request),
+ },
+ "subscription_level": "Direct",
+ }
self.assertEqual(
- [expected_result], json.loads(view.subscriber_data_js))
+ [expected_result], json.loads(view.subscriber_data_js)
+ )
def test_data_person_subscription_other_subscriber_query_count(self):
# All subscriber data should be retrieved with a single query.
question = self._makeQuestionWithNoSubscribers()
subscribed_by = self.factory.makePerson(
- name="someone", displayname='Someone')
+ name="someone", displayname="Someone"
+ )
subscriber = self.factory.makePerson(
- name='user', displayname='Subscriber Name')
+ name="user", displayname="Subscriber Name"
+ )
with person_logged_in(subscriber):
- question.subscribe(person=subscriber,
- subscribed_by=subscribed_by)
- view = create_view(question, '+portlet-subscribers-details')
+ question.subscribe(person=subscriber, subscribed_by=subscribed_by)
+ view = create_view(question, "+portlet-subscribers-details")
# Invoke the view method, ignoring the results.
Store.of(question).invalidate()
with StormStatementRecorder() as recorder:
@@ -99,55 +103,61 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
# to true.
question = self._makeQuestionWithNoSubscribers()
teamowner = self.factory.makePerson(
- name="team-owner", displayname="Team Owner")
+ name="team-owner", displayname="Team Owner"
+ )
subscriber = self.factory.makeTeam(
- name='team', displayname='Team Name', owner=teamowner)
+ name="team", displayname="Team Name", owner=teamowner
+ )
with person_logged_in(subscriber.teamowner):
question.subscribe(subscriber, subscriber.teamowner)
- view = create_view(question, '+portlet-subscribers-details')
+ view = create_view(question, "+portlet-subscribers-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'team',
- 'display_name': 'Team Name',
- 'is_team': True,
- 'can_edit': False,
- 'web_link': canonical_url(subscriber),
- 'self_link': absoluteURL(subscriber, api_request)
- },
- 'subscription_level': "Direct",
- }
+ "subscriber": {
+ "name": "team",
+ "display_name": "Team Name",
+ "is_team": True,
+ "can_edit": False,
+ "web_link": canonical_url(subscriber),
+ "self_link": absoluteURL(subscriber, api_request),
+ },
+ "subscription_level": "Direct",
+ }
self.assertEqual(
- [expected_result], json.loads(view.subscriber_data_js))
+ [expected_result], json.loads(view.subscriber_data_js)
+ )
def test_data_team_subscription_owner_looks(self):
# For a team subscription, subscriber_data_js has can_edit
# set to true for team owner.
question = self._makeQuestionWithNoSubscribers()
teamowner = self.factory.makePerson(
- name="team-owner", displayname="Team Owner")
+ name="team-owner", displayname="Team Owner"
+ )
subscriber = self.factory.makeTeam(
- name='team', displayname='Team Name', owner=teamowner)
+ name="team", displayname="Team Name", owner=teamowner
+ )
with person_logged_in(subscriber.teamowner):
question.subscribe(subscriber, subscriber.teamowner)
- view = create_view(question, '+portlet-subscribers-details')
+ view = create_view(question, "+portlet-subscribers-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'team',
- 'display_name': 'Team Name',
- 'is_team': True,
- 'can_edit': True,
- 'web_link': canonical_url(subscriber),
- 'self_link': absoluteURL(subscriber, api_request)
- },
- 'subscription_level': "Direct",
- }
+ "subscriber": {
+ "name": "team",
+ "display_name": "Team Name",
+ "is_team": True,
+ "can_edit": True,
+ "web_link": canonical_url(subscriber),
+ "self_link": absoluteURL(subscriber, api_request),
+ },
+ "subscription_level": "Direct",
+ }
with person_logged_in(subscriber.teamowner):
self.assertEqual(
- [expected_result], json.loads(view.subscriber_data_js))
+ [expected_result], json.loads(view.subscriber_data_js)
+ )
def test_data_team_subscription_member_looks(self):
# For a team subscription, subscriber_data_js has can_edit
@@ -155,29 +165,34 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
question = self._makeQuestionWithNoSubscribers()
member = self.factory.makePerson()
teamowner = self.factory.makePerson(
- name="team-owner", displayname="Team Owner")
+ name="team-owner", displayname="Team Owner"
+ )
subscriber = self.factory.makeTeam(
- name='team', displayname='Team Name', owner=teamowner,
- members=[member])
+ name="team",
+ displayname="Team Name",
+ owner=teamowner,
+ members=[member],
+ )
with person_logged_in(subscriber.teamowner):
question.subscribe(subscriber, subscriber.teamowner)
- view = create_view(question, '+portlet-subscribers-details')
+ view = create_view(question, "+portlet-subscribers-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'team',
- 'display_name': 'Team Name',
- 'is_team': True,
- 'can_edit': True,
- 'web_link': canonical_url(subscriber),
- 'self_link': absoluteURL(subscriber, api_request)
- },
- 'subscription_level': "Direct",
- }
+ "subscriber": {
+ "name": "team",
+ "display_name": "Team Name",
+ "is_team": True,
+ "can_edit": True,
+ "web_link": canonical_url(subscriber),
+ "self_link": absoluteURL(subscriber, api_request),
+ },
+ "subscription_level": "Direct",
+ }
with person_logged_in(subscriber.teamowner):
self.assertEqual(
- [expected_result], json.loads(view.subscriber_data_js))
+ [expected_result], json.loads(view.subscriber_data_js)
+ )
def test_data_subscription_lp_admin(self):
# For a subscription, subscriber_data_js has can_edit
@@ -185,26 +200,28 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
question = self._makeQuestionWithNoSubscribers()
member = self.factory.makePerson()
subscriber = self.factory.makePerson(
- name='user', displayname='Subscriber Name')
+ name="user", displayname="Subscriber Name"
+ )
with person_logged_in(member):
question.subscribe(subscriber, subscriber)
- view = create_view(question, '+portlet-subscribers-details')
+ view = create_view(question, "+portlet-subscribers-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'user',
- 'display_name': 'Subscriber Name',
- 'is_team': False,
- 'can_edit': True,
- 'web_link': canonical_url(subscriber),
- 'self_link': absoluteURL(subscriber, api_request)
- },
- 'subscription_level': "Direct",
- }
+ "subscriber": {
+ "name": "user",
+ "display_name": "Subscriber Name",
+ "is_team": False,
+ "can_edit": True,
+ "web_link": canonical_url(subscriber),
+ "self_link": absoluteURL(subscriber, api_request),
+ },
+ "subscription_level": "Direct",
+ }
# Login as admin
admin = getUtility(IPersonSet).find(ADMIN_EMAIL).any()
with person_logged_in(admin):
self.assertEqual(
- [expected_result], json.loads(view.subscriber_data_js))
+ [expected_result], json.loads(view.subscriber_data_js)
+ )
diff --git a/lib/lp/answers/browser/tests/test_questiontarget.py b/lib/lp/answers/browser/tests/test_questiontarget.py
index 386846f..5a289c0 100644
--- a/lib/lp/answers/browser/tests/test_questiontarget.py
+++ b/lib/lp/answers/browser/tests/test_questiontarget.py
@@ -7,10 +7,7 @@ import json
import os
from urllib.parse import quote
-from lazr.restful.interfaces import (
- IJSONRequestCache,
- IWebServiceClientRequest,
- )
+from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
@@ -22,22 +19,12 @@ from lp.registry.interfaces.person import IPersonSet
from lp.services.beautifulsoup import BeautifulSoup
from lp.services.webapp import canonical_url
from lp.services.worlddata.interfaces.language import ILanguageSet
-from lp.testing import (
- login_person,
- person_logged_in,
- TestCaseWithFactory,
- )
-from lp.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
+from lp.testing import TestCaseWithFactory, login_person, person_logged_in
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
from lp.testing.matchers import BrowsesWithQueryLimit
from lp.testing.pages import find_tag_by_id
from lp.testing.sampledata import ADMIN_EMAIL
-from lp.testing.views import (
- create_initialized_view,
- create_view,
- )
+from lp.testing.views import create_initialized_view, create_view
class TestSearchQuestionsView(TestCaseWithFactory):
@@ -48,18 +35,19 @@ class TestSearchQuestionsView(TestCaseWithFactory):
product = self.factory.makeProduct()
# Avoid non-ascii character in unicode literal to not upset
# pocket-lint. Bug #776389.
- non_ascii_string = 'portugu\xeas'
+ non_ascii_string = "portugu\xeas"
with person_logged_in(product.owner):
self.factory.makeFAQ(product, non_ascii_string)
form = {
- 'field.search_text': non_ascii_string,
- 'field.status': 'OPEN',
- 'field.actions.search': 'Search',
- }
+ "field.search_text": non_ascii_string,
+ "field.status": "OPEN",
+ "field.actions.search": "Search",
+ }
view = create_initialized_view(
- product, '+questions', form=form, method='GET')
+ product, "+questions", form=form, method="GET"
+ )
- encoded_string = quote(non_ascii_string.encode('utf-8'))
+ encoded_string = quote(non_ascii_string.encode("utf-8"))
# This must not raise UnicodeEncodeError.
self.assertIn(encoded_string, view.matching_faqs_url)
@@ -68,11 +56,11 @@ class TestSearchQuestionsView(TestCaseWithFactory):
owner = self.factory.makePerson()
distro = self.factory.makeDistribution()
removeSecurityProxy(distro).official_answers = True
- dsp = self.factory.makeDistributionSourcePackage(
- distribution=distro)
+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
[self.factory.makeQuestion(target=dsp, owner=owner) for i in range(5)]
browses_under_limit = BrowsesWithQueryLimit(
- 31, owner, view_name="+questions")
+ 31, owner, view_name="+questions"
+ )
self.assertThat(dsp, browses_under_limit)
@@ -82,38 +70,38 @@ class TestSearchQuestionsViewCanConfigureAnswers(TestCaseWithFactory):
def test_cannot_configure_answers_product_no_edit_permission(self):
product = self.factory.makeProduct()
- view = create_initialized_view(product, '+questions')
+ view = create_initialized_view(product, "+questions")
self.assertEqual(False, view.can_configure_answers)
def test_can_configure_answers_product_with_edit_permission(self):
product = self.factory.makeProduct()
login_person(product.owner)
- view = create_initialized_view(product, '+questions')
+ view = create_initialized_view(product, "+questions")
self.assertEqual(True, view.can_configure_answers)
def test_cannot_configure_answers_distribution_no_edit_permission(self):
distribution = self.factory.makeDistribution()
- view = create_initialized_view(distribution, '+questions')
+ view = create_initialized_view(distribution, "+questions")
self.assertEqual(False, view.can_configure_answers)
def test_can_configure_answers_distribution_with_edit_permission(self):
distribution = self.factory.makeDistribution()
login_person(distribution.owner)
- view = create_initialized_view(distribution, '+questions')
+ view = create_initialized_view(distribution, "+questions")
self.assertEqual(True, view.can_configure_answers)
def test_cannot_configure_answers_projectgroup_with_edit_permission(self):
# Project groups inherit Launchpad usage from their projects.
project_group = self.factory.makeProject()
login_person(project_group.owner)
- view = create_initialized_view(project_group, '+questions')
+ view = create_initialized_view(project_group, "+questions")
self.assertEqual(False, view.can_configure_answers)
def test_cannot_configure_answers_dsp_with_edit_permission(self):
# DSPs inherit Launchpad usage from their distribution.
dsp = self.factory.makeDistributionSourcePackage()
login_person(dsp.distribution.owner)
- view = create_initialized_view(dsp, '+questions')
+ view = create_initialized_view(dsp, "+questions")
self.assertEqual(False, view.can_configure_answers)
@@ -123,26 +111,25 @@ class TestSearchQuestionsViewTemplate(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
def assertViewTemplate(self, context, file_name):
- view = create_initialized_view(context, '+questions')
- self.assertEqual(
- file_name, os.path.basename(view.template.filename))
+ view = create_initialized_view(context, "+questions")
+ self.assertEqual(file_name, os.path.basename(view.template.filename))
def test_template_product_answers_usage_unknown(self):
product = self.factory.makeProduct()
- self.assertViewTemplate(product, 'unknown-support.pt')
+ self.assertViewTemplate(product, "unknown-support.pt")
def test_template_product_answers_usage_launchpad(self):
product = self.factory.makeProduct()
with person_logged_in(product.owner):
product.answers_usage = ServiceUsage.LAUNCHPAD
- self.assertViewTemplate(product, 'question-listing.pt')
+ self.assertViewTemplate(product, "question-listing.pt")
def test_template_projectgroup_answers_usage_unknown(self):
product = self.factory.makeProduct()
project_group = self.factory.makeProject(owner=product.owner)
with person_logged_in(product.owner):
product.projectgroup = project_group
- self.assertViewTemplate(project_group, 'unknown-support.pt')
+ self.assertViewTemplate(project_group, "unknown-support.pt")
def test_template_projectgroup_answers_usage_launchpad(self):
product = self.factory.makeProduct()
@@ -150,31 +137,31 @@ class TestSearchQuestionsViewTemplate(TestCaseWithFactory):
with person_logged_in(product.owner):
product.projectgroup = project_group
product.answers_usage = ServiceUsage.LAUNCHPAD
- self.assertViewTemplate(project_group, 'question-listing.pt')
+ self.assertViewTemplate(project_group, "question-listing.pt")
def test_template_distribution_answers_usage_unknown(self):
distribution = self.factory.makeDistribution()
- self.assertViewTemplate(distribution, 'unknown-support.pt')
+ self.assertViewTemplate(distribution, "unknown-support.pt")
def test_template_distribution_answers_usage_launchpad(self):
distribution = self.factory.makeDistribution()
with person_logged_in(distribution.owner):
distribution.answers_usage = ServiceUsage.LAUNCHPAD
- self.assertViewTemplate(distribution, 'question-listing.pt')
+ self.assertViewTemplate(distribution, "question-listing.pt")
def test_template_DSP_answers_usage_unknown(self):
dsp = self.factory.makeDistributionSourcePackage()
- self.assertViewTemplate(dsp, 'unknown-support.pt')
+ self.assertViewTemplate(dsp, "unknown-support.pt")
def test_template_DSP_answers_usage_launchpad(self):
dsp = self.factory.makeDistributionSourcePackage()
with person_logged_in(dsp.distribution.owner):
dsp.distribution.answers_usage = ServiceUsage.LAUNCHPAD
- self.assertViewTemplate(dsp, 'question-listing.pt')
+ self.assertViewTemplate(dsp, "question-listing.pt")
def test_template_question_set(self):
question_set = getUtility(IQuestionSet)
- self.assertViewTemplate(question_set, 'question-listing.pt')
+ self.assertViewTemplate(question_set, "question-listing.pt")
class TestSearchQuestionsViewUnknown(TestCaseWithFactory):
@@ -185,43 +172,46 @@ class TestSearchQuestionsViewUnknown(TestCaseWithFactory):
def linkPackage(self, product, name):
# A helper to setup a legitimate Packaging link between a product
# and an Ubuntu source package.
- hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
+ hoary = getUtility(ILaunchpadCelebrities).ubuntu["hoary"]
sourcepackagename = self.factory.makeSourcePackageName(name)
self.factory.makeSourcePackage(
- sourcepackagename=sourcepackagename, distroseries=hoary)
+ sourcepackagename=sourcepackagename, distroseries=hoary
+ )
self.factory.makeSourcePackagePublishingHistory(
- sourcepackagename=sourcepackagename, distroseries=hoary)
+ sourcepackagename=sourcepackagename, distroseries=hoary
+ )
product.development_focus.setPackaging(
- hoary, sourcepackagename, product.owner)
+ hoary, sourcepackagename, product.owner
+ )
def setUp(self):
super().setUp()
self.product = self.factory.makeProduct()
- self.view = create_initialized_view(self.product, '+questions')
+ self.view = create_initialized_view(self.product, "+questions")
def assertCommonPageElements(self, content):
- robots = content.find('meta', attrs={'name': 'robots'})
- self.assertEqual('noindex,nofollow', robots['content'])
- self.assertTrue(content.find(True, id='support-unknown') is not None)
+ robots = content.find("meta", attrs={"name": "robots"})
+ self.assertEqual("noindex,nofollow", robots["content"])
+ self.assertTrue(content.find(True, id="support-unknown") is not None)
def test_any_question_target_any_user(self):
content = BeautifulSoup(self.view())
self.assertCommonPageElements(content)
def test_product_with_packaging_elements(self):
- self.linkPackage(self.product, 'cow')
+ self.linkPackage(self.product, "cow")
content = BeautifulSoup(self.view())
self.assertCommonPageElements(content)
- self.assertTrue(content.find(True, id='ubuntu-support') is not None)
+ self.assertTrue(content.find(True, id="ubuntu-support") is not None)
def test_product_with_edit_permission(self):
login_person(self.product.owner)
self.view = create_initialized_view(
- self.product, '+questions', principal=self.product.owner)
+ self.product, "+questions", principal=self.product.owner
+ )
content = BeautifulSoup(self.view())
self.assertCommonPageElements(content)
- self.assertTrue(
- content.find(True, id='configure-support') is not None)
+ self.assertTrue(content.find(True, id="configure-support") is not None)
class QuestionSetViewTestCase(TestCaseWithFactory):
@@ -232,19 +222,19 @@ class QuestionSetViewTestCase(TestCaseWithFactory):
def test_search_questions_form_rendering(self):
# The view's template directly renders the form widgets.
question_set = getUtility(IQuestionSet)
- view = create_initialized_view(question_set, '+index')
- content = find_tag_by_id(view.render(), 'search-all-questions')
- self.assertEqual('form', content.name)
- self.assertIsNot(None, content.find(True, id='text'))
- self.assertIsNot(
- None, content.find(True, id='field.actions.search'))
- self.assertIsNot(
- None, content.find(True, id='field.scope.option.all'))
+ view = create_initialized_view(question_set, "+index")
+ content = find_tag_by_id(view.render(), "search-all-questions")
+ self.assertEqual("form", content.name)
+ self.assertIsNot(None, content.find(True, id="text"))
+ self.assertIsNot(None, content.find(True, id="field.actions.search"))
+ self.assertIsNot(None, content.find(True, id="field.scope.option.all"))
self.assertIsNot(
- None, content.find(True, id='field.scope.option.project'))
- target_widget = view.widgets['scope'].target_widget
+ None, content.find(True, id="field.scope.option.project")
+ )
+ target_widget = view.widgets["scope"].target_widget
self.assertIsNot(
- None, content.find(True, id=target_widget.show_widget_id))
+ None, content.find(True, id=target_widget.show_widget_id)
+ )
text = str(content)
picker_vocab = "DistributionOrProductOrProjectGroup"
self.assertIn(picker_vocab, text)
@@ -252,25 +242,25 @@ class QuestionSetViewTestCase(TestCaseWithFactory):
self.assertIn(focus_script, text)
-class QuestionTargetPortletAnswerContactsWithDetailsTests(
- TestCaseWithFactory):
+class QuestionTargetPortletAnswerContactsWithDetailsTests(TestCaseWithFactory):
"""Tests for IQuestionTarget:+portlet-answercontacts-details view."""
+
layer = LaunchpadFunctionalLayer
def test_content_type(self):
question = self.factory.makeQuestion()
# It works even for anonymous users, so no log-in is needed.
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
view.render()
self.assertEqual(
- view.request.response.getHeader('content-type'),
- 'application/json')
+ view.request.response.getHeader("content-type"), "application/json"
+ )
def test_data_no_answer_contacts(self):
question = self.factory.makeQuestion()
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
self.assertEqual([], json.loads(view.answercontact_data_js))
def test_data_person_answercontact(self):
@@ -279,80 +269,88 @@ class QuestionTargetPortletAnswerContactsWithDetailsTests(
# subscribers_list.js loading.
question = self.factory.makeQuestion()
contact = self.factory.makePerson(
- name='user', displayname='Contact Name')
+ name="user", displayname="Contact Name"
+ )
with person_logged_in(contact):
- contact.addLanguage(getUtility(ILanguageSet)['en'])
+ contact.addLanguage(getUtility(ILanguageSet)["en"])
question.target.addAnswerContact(contact, contact)
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'user',
- 'display_name': 'Contact Name',
- 'is_team': False,
- 'can_edit': False,
- 'web_link': canonical_url(contact),
- 'self_link': absoluteURL(contact, api_request)
- }
+ "subscriber": {
+ "name": "user",
+ "display_name": "Contact Name",
+ "is_team": False,
+ "can_edit": False,
+ "web_link": canonical_url(contact),
+ "self_link": absoluteURL(contact, api_request),
}
+ }
self.assertEqual(
- [expected_result], json.loads(view.answercontact_data_js))
+ [expected_result], json.loads(view.answercontact_data_js)
+ )
def test_data_team_answer_contact(self):
# For a team answer contacts, answercontact_data_js has is_team set
# to true.
question = self.factory.makeQuestion()
teamowner = self.factory.makePerson(
- name="team-owner", displayname="Team Owner")
+ name="team-owner", displayname="Team Owner"
+ )
contact = self.factory.makeTeam(
- name='team', displayname='Team Name', owner=teamowner)
+ name="team", displayname="Team Name", owner=teamowner
+ )
with person_logged_in(contact.teamowner):
- contact.addLanguage(getUtility(ILanguageSet)['en'])
+ contact.addLanguage(getUtility(ILanguageSet)["en"])
question.target.addAnswerContact(contact, contact)
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'team',
- 'display_name': 'Team Name',
- 'is_team': True,
- 'can_edit': False,
- 'web_link': canonical_url(contact),
- 'self_link': absoluteURL(contact, api_request)
- }
+ "subscriber": {
+ "name": "team",
+ "display_name": "Team Name",
+ "is_team": True,
+ "can_edit": False,
+ "web_link": canonical_url(contact),
+ "self_link": absoluteURL(contact, api_request),
}
+ }
self.assertEqual(
- [expected_result], json.loads(view.answercontact_data_js))
+ [expected_result], json.loads(view.answercontact_data_js)
+ )
def test_data_team_answercontact_owner_looks(self):
# For a team subscription, answercontact_data_js has can_edit
# set to true for team owner.
question = self.factory.makeQuestion()
teamowner = self.factory.makePerson(
- name="team-owner", displayname="Team Owner")
+ name="team-owner", displayname="Team Owner"
+ )
contact = self.factory.makeTeam(
- name='team', displayname='Team Name', owner=teamowner)
+ name="team", displayname="Team Name", owner=teamowner
+ )
with person_logged_in(contact.teamowner):
- contact.addLanguage(getUtility(ILanguageSet)['en'])
+ contact.addLanguage(getUtility(ILanguageSet)["en"])
question.target.addAnswerContact(contact, contact.teamowner)
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'team',
- 'display_name': 'Team Name',
- 'is_team': True,
- 'can_edit': True,
- 'web_link': canonical_url(contact),
- 'self_link': absoluteURL(contact, api_request)
- }
+ "subscriber": {
+ "name": "team",
+ "display_name": "Team Name",
+ "is_team": True,
+ "can_edit": True,
+ "web_link": canonical_url(contact),
+ "self_link": absoluteURL(contact, api_request),
}
+ }
with person_logged_in(contact.teamowner):
self.assertEqual(
- [expected_result], json.loads(view.answercontact_data_js))
+ [expected_result], json.loads(view.answercontact_data_js)
+ )
def test_data_team_subscription_member_looks(self):
# For a team subscription, answercontact_data_js has can_edit
@@ -360,55 +358,62 @@ class QuestionTargetPortletAnswerContactsWithDetailsTests(
question = self.factory.makeQuestion()
member = self.factory.makePerson()
teamowner = self.factory.makePerson(
- name="team-owner", displayname="Team Owner")
+ name="team-owner", displayname="Team Owner"
+ )
contact = self.factory.makeTeam(
- name='team', displayname='Team Name', owner=teamowner,
- members=[member])
+ name="team",
+ displayname="Team Name",
+ owner=teamowner,
+ members=[member],
+ )
with person_logged_in(contact.teamowner):
- contact.addLanguage(getUtility(ILanguageSet)['en'])
+ contact.addLanguage(getUtility(ILanguageSet)["en"])
question.target.addAnswerContact(contact, contact.teamowner)
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'team',
- 'display_name': 'Team Name',
- 'is_team': True,
- 'can_edit': True,
- 'web_link': canonical_url(contact),
- 'self_link': absoluteURL(contact, api_request)
- }
+ "subscriber": {
+ "name": "team",
+ "display_name": "Team Name",
+ "is_team": True,
+ "can_edit": True,
+ "web_link": canonical_url(contact),
+ "self_link": absoluteURL(contact, api_request),
}
+ }
with person_logged_in(contact.teamowner):
self.assertEqual(
- [expected_result], json.loads(view.answercontact_data_js))
+ [expected_result], json.loads(view.answercontact_data_js)
+ )
def test_data_target_owner_answercontact_looks(self):
# Answercontact_data_js has can_edit set to true for target owner.
distro = self.factory.makeDistribution()
question = self.factory.makeQuestion(target=distro)
contact = self.factory.makePerson(
- name='user', displayname='Contact Name')
+ name="user", displayname="Contact Name"
+ )
with person_logged_in(contact):
- contact.addLanguage(getUtility(ILanguageSet)['en'])
+ contact.addLanguage(getUtility(ILanguageSet)["en"])
question.target.addAnswerContact(contact, contact)
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'user',
- 'display_name': 'Contact Name',
- 'is_team': False,
- 'can_edit': True,
- 'web_link': canonical_url(contact),
- 'self_link': absoluteURL(contact, api_request)
- }
+ "subscriber": {
+ "name": "user",
+ "display_name": "Contact Name",
+ "is_team": False,
+ "can_edit": True,
+ "web_link": canonical_url(contact),
+ "self_link": absoluteURL(contact, api_request),
}
+ }
with person_logged_in(distro.owner):
self.assertEqual(
- [expected_result], json.loads(view.answercontact_data_js))
+ [expected_result], json.loads(view.answercontact_data_js)
+ )
def test_data_subscription_lp_admin(self):
# For a subscription, answercontact_data_js has can_edit
@@ -416,34 +421,37 @@ class QuestionTargetPortletAnswerContactsWithDetailsTests(
question = self.factory.makeQuestion()
member = self.factory.makePerson()
contact = self.factory.makePerson(
- name='user', displayname='Contact Name')
+ name="user", displayname="Contact Name"
+ )
with person_logged_in(contact):
- contact.addLanguage(getUtility(ILanguageSet)['en'])
+ contact.addLanguage(getUtility(ILanguageSet)["en"])
with person_logged_in(member):
question.target.addAnswerContact(contact, contact)
- view = create_view(question.target, '+portlet-answercontacts-details')
+ view = create_view(question.target, "+portlet-answercontacts-details")
api_request = IWebServiceClientRequest(view.request)
expected_result = {
- 'subscriber': {
- 'name': 'user',
- 'display_name': 'Contact Name',
- 'is_team': False,
- 'can_edit': True,
- 'web_link': canonical_url(contact),
- 'self_link': absoluteURL(contact, api_request)
- }
+ "subscriber": {
+ "name": "user",
+ "display_name": "Contact Name",
+ "is_team": False,
+ "can_edit": True,
+ "web_link": canonical_url(contact),
+ "self_link": absoluteURL(contact, api_request),
}
+ }
# Login as admin
admin = getUtility(IPersonSet).find(ADMIN_EMAIL).any()
with person_logged_in(admin):
self.assertEqual(
- [expected_result], json.loads(view.answercontact_data_js))
+ [expected_result], json.loads(view.answercontact_data_js)
+ )
class TestQuestionTargetPortletAnswerContacts(TestCaseWithFactory):
"""Tests for IQuestionTarget:+portlet-answercontacts."""
+
layer = LaunchpadFunctionalLayer
def test_jsoncache_contents(self):
@@ -453,13 +461,16 @@ class TestQuestionTargetPortletAnswerContacts(TestCaseWithFactory):
# It works even for anonymous users, so no log-in is needed.
view = create_initialized_view(
- question.target, '+portlet-answercontacts', rootsite='answers')
+ question.target, "+portlet-answercontacts", rootsite="answers"
+ )
cache = IJSONRequestCache(view.request).objects
context_url_data = {
- 'web_link': canonical_url(product, rootsite='mainsite'),
- 'self_link': absoluteURL(product,
- IWebServiceClientRequest(view.request)),
- }
- self.assertEqual(cache[product.name + '_answer_portlet_url_data'],
- context_url_data)
+ "web_link": canonical_url(product, rootsite="mainsite"),
+ "self_link": absoluteURL(
+ product, IWebServiceClientRequest(view.request)
+ ),
+ }
+ self.assertEqual(
+ cache[product.name + "_answer_portlet_url_data"], context_url_data
+ )
diff --git a/lib/lp/answers/browser/tests/test_views.py b/lib/lp/answers/browser/tests/test_views.py
index eeee41a..91ab009 100644
--- a/lib/lp/answers/browser/tests/test_views.py
+++ b/lib/lp/answers/browser/tests/test_views.py
@@ -9,11 +9,7 @@ import unittest
from lp.testing import BrowserTestCase
from lp.testing.layers import DatabaseFunctionalLayer
-from lp.testing.systemdocs import (
- LayeredDocFileSuite,
- setUp,
- tearDown,
- )
+from lp.testing.systemdocs import LayeredDocFileSuite, setUp, tearDown
class TestEmailObfuscated(BrowserTestCase):
@@ -24,22 +20,26 @@ class TestEmailObfuscated(BrowserTestCase):
def getBrowserForQuestionWithEmail(self, email_address, no_login):
question = self.factory.makeQuestion(
title="Title with %s contained" % email_address,
- description="Description with %s contained." % email_address)
+ description="Description with %s contained." % email_address,
+ )
return self.getViewBrowser(
- question, rootsite="answers", no_login=no_login)
+ question, rootsite="answers", no_login=no_login
+ )
def test_user_sees_email_address(self):
"""A logged-in user can see the email address on the page."""
email_address = "mark@xxxxxxxxxxx"
browser = self.getBrowserForQuestionWithEmail(
- email_address, no_login=False)
+ email_address, no_login=False
+ )
self.assertEqual(4, browser.contents.count(email_address))
def test_anonymous_sees_not_email_address(self):
"""The anonymous user cannot see the email address on the page."""
email_address = "mark@xxxxxxxxxxx"
browser = self.getBrowserForQuestionWithEmail(
- email_address, no_login=True)
+ email_address, no_login=True
+ )
self.assertEqual(0, browser.contents.count(email_address))
@@ -47,13 +47,28 @@ def test_suite():
suite = unittest.TestSuite()
loader = unittest.TestLoader()
suite.addTest(loader.loadTestsFromTestCase(TestEmailObfuscated))
- suite.addTest(LayeredDocFileSuite(
- 'question-subscribe_me.txt', setUp=setUp, tearDown=tearDown,
- layer=DatabaseFunctionalLayer))
- suite.addTest(LayeredDocFileSuite(
- 'views.txt', setUp=setUp, tearDown=tearDown,
- layer=DatabaseFunctionalLayer))
- suite.addTest(LayeredDocFileSuite(
- 'faq-views.txt', setUp=setUp, tearDown=tearDown,
- layer=DatabaseFunctionalLayer))
+ suite.addTest(
+ LayeredDocFileSuite(
+ "question-subscribe_me.txt",
+ setUp=setUp,
+ tearDown=tearDown,
+ layer=DatabaseFunctionalLayer,
+ )
+ )
+ suite.addTest(
+ LayeredDocFileSuite(
+ "views.txt",
+ setUp=setUp,
+ tearDown=tearDown,
+ layer=DatabaseFunctionalLayer,
+ )
+ )
+ suite.addTest(
+ LayeredDocFileSuite(
+ "faq-views.txt",
+ setUp=setUp,
+ tearDown=tearDown,
+ layer=DatabaseFunctionalLayer,
+ )
+ )
return suite
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..1f331da
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[tool.black]
+line-length = 79
+target-version = ['py35']
diff --git a/setup.cfg b/setup.cfg
index db4af9b..fe3f8eb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -197,7 +197,6 @@ exclude =
# Code here is imported from elsewhere and may not necessarily conform
# to Launchpad's style.
lib/contrib
-hang-closing = true
ignore =
# Skip all the pure whitespace issues for now. There are too many of
# them to be worth fixing manually, and most of them will get sorted out
@@ -209,6 +208,7 @@ ignore =
E117,
E121,
E122,
+ E123,
E124,
E125,
E126,
@@ -249,15 +249,8 @@ ignore =
W504
[isort]
-combine_as_imports = true
-force_grid_wrap = 2
-force_sort_within_sections = true
-include_trailing_comma = true
# database/* have some implicit relative imports.
known_first_party = canonical,lp,launchpad_loggerhead,devscripts,fti,replication,preflight,security,upgrade,dbcontroller
known_pythonpath = _pythonpath
-line_length = 78
-lines_after_imports = 2
-multi_line_output = 8
-order_by_type = false
+line_length = 79
sections = FUTURE,PYTHONPATH,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
Follow ups