Merge ~cjwatson/launchpad:black-answers-browser into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: d74bdbf50afe5371c13e0bdd930b46b229463d16
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:black-answers-browser
Merge into: launchpad:master
Diff against target: 4956 lines (+1498/-1177)
18 files modified
.git-blame-ignore-revs (+2/-0)
.pre-commit-config.yaml (+35/-5)
lib/lp/answers/browser/faq.py (+20/-23)
lib/lp/answers/browser/faqcollection.py (+58/-49)
lib/lp/answers/browser/faqtarget.py (+15/-18)
lib/lp/answers/browser/person.py (+117/-79)
lib/lp/answers/browser/question.py (+461/-365)
lib/lp/answers/browser/questionsubscription.py (+26/-29)
lib/lp/answers/browser/questiontarget.py (+374/-261)
lib/lp/answers/browser/tests/test_breadcrumbs.py (+21/-18)
lib/lp/answers/browser/tests/test_menus.py (+7/-12)
lib/lp/answers/browser/tests/test_question.py (+47/-39)
lib/lp/answers/browser/tests/test_questionmessages.py (+11/-14)
lib/lp/answers/browser/tests/test_questionsubscription_views.py (+97/-80)
lib/lp/answers/browser/tests/test_questiontarget.py (+169/-158)
lib/lp/answers/browser/tests/test_views.py (+33/-18)
pyproject.toml (+3/-0)
setup.cfg (+2/-9)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Andrey Fedoseev (community) Approve
Review via email: mp+423202@code.launchpad.net

Commit message

lp.answers.browser: Apply black

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

As this was clearly marked as RFC, I would really prefer single line import statements.

So instead of ....

```
- )
-from lp.app.browser.launchpadform import (
- action,
- LaunchpadFormView,
- safe_action,
- )
+)
+from lp.app.browser.launchpadform import LaunchpadFormView, action, safe_action
```

I favor

```
from lp.app.browser.launchpadform import LaunchpadFormView
from lp.app.browser.launchpadform import action
from lp.app.browser.launchpadform import safe_action
```

- still two lines shorter than the previous style
- very easy and quickly to scan with my eyes
- minimal and clear diff on changes

Revision history for this message
Jürgen Gmach (jugmac00) wrote :
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

This looks good. I don't have any preference regarding the import statement formatting.

review: Approve
Revision history for this message
Colin Watson (cjwatson) wrote :

I *really* dislike the repetition of Jürgen's proposed import style, I'm afraid. It's like fingernails down a blackboard for me

Revision history for this message
Guruprasad (lgp171188) wrote (last edit ):

> I *really* dislike the repetition of Jürgen's proposed import style, I'm
> afraid. It's like fingernails down a blackboard for me

+1

I am okay with using a single line for all the imports from the same package/module as long as it fits within the line maximum length and breaking it down into multiple lines when it exceeds that.

Revision history for this message
Guruprasad (lgp171188) wrote :

I also dislike the forced conversion of all single quotes to double quotes that black does - my preference is to use single quotes everywhere and double quotes where necessary. But I can bring myself to accept it for standardizing this across the codebase.

Revision history for this message
Colin Watson (cjwatson) wrote :

I can use black's `skip-string-normalization` option if people prefer.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

I would suggest not to use black's `skip-string-normalization` option as then we still would have discussions when to use single or double quotes.

Let's swallow the (black) pill :-)

Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

I'd like to stick to either double or single quotes, otherwise it defeats the purpose of using `black`. Historically, I use double quotes by default, mostly because I use them in other languages such as XML/HMTL.

Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

In general, I'd prefer to keep black customization to a minimum (including the line length). With `black` becoming a sort of industry standard it would be nice to use the code style that matches the code of other Python projects.

Revision history for this message
Colin Watson (cjwatson) wrote :

I stuck to LP's existing line length mainly for selfish reasons. With <80-character lines, I can have three columns of code side-by-side on my laptop screen at a comfortable font size; with longer lines, I can't. That's a big quality-of-life difference for me, significantly outweighing any questions of similarity with other projects.

Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Guruprasad (lgp171188) wrote :

Let's do this! 👍

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
2index 6c1e034..c258618 100644
3--- a/.git-blame-ignore-revs
4+++ b/.git-blame-ignore-revs
5@@ -52,3 +52,5 @@ d61c2ad002c2997a132a1580ce6ee82bb03de11d
6 afcfc15adcf3267d3fd07d7679df00231853c908
7 # apply pyupgrade --py3-plus to lp.translations
8 3f3ea0f7799093f93740d3a1db3a11965d0b25cb
9+# apply black to lp.answers.browser
10+c606443bdb2f342593c9a7c9437cb70c01f85f29
11diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
12index 80c13da..df17562 100644
13--- a/.pre-commit-config.yaml
14+++ b/.pre-commit-config.yaml
15@@ -23,11 +23,6 @@ repos:
16 exclude: systemdocs\.py
17 - id: no-commit-to-branch
18 args: [--branch, master, --branch, db-devel]
19-- repo: https://github.com/PyCQA/flake8
20- rev: 3.9.2
21- hooks:
22- - id: flake8
23- exclude: ^lib/contrib/
24 - repo: https://github.com/asottile/pyupgrade
25 rev: v2.31.0
26 hooks:
27@@ -40,10 +35,45 @@ repos:
28 |utilities/community-contributions\.py
29 |utilities/update-sourcecode
30 )$
31+- repo: https://github.com/psf/black
32+ rev: 22.3.0
33+ hooks:
34+ - id: black
35+ files: |
36+ (?x)^lib/lp/(
37+ answers/browser
38+ )/
39 - repo: https://github.com/PyCQA/isort
40 rev: 5.9.2
41 hooks:
42 - id: isort
43+ name: isort (old-style)
44+ args:
45+ - --combine-as
46+ - --force-grid-wrap=2
47+ - --force-sort-within-sections
48+ - --trailing-comma
49+ - --line-length=78
50+ - --lines-after-imports=2
51+ - --multi-line=8
52+ - --dont-order-by-type
53+ exclude: |
54+ (?x)^lib/lp/(
55+ answers/browser
56+ )/
57+ - id: isort
58+ alias: isort-black
59+ name: isort (black)
60+ args: [--profile, black]
61+ files: |
62+ (?x)^lib/lp/(
63+ answers/browser
64+ )/
65+- repo: https://github.com/PyCQA/flake8
66+ rev: 3.9.2
67+ hooks:
68+ - id: flake8
69+ exclude: ^lib/contrib/
70 - repo: https://github.com/pre-commit/mirrors-eslint
71 rev: v4.2.0
72 hooks:
73diff --git a/lib/lp/answers/browser/faq.py b/lib/lp/answers/browser/faq.py
74index dbca64d..732bd2d 100644
75--- a/lib/lp/answers/browser/faq.py
76+++ b/lib/lp/answers/browser/faq.py
77@@ -4,25 +4,22 @@
78 """`IFAQ` browser views."""
79
80 __all__ = [
81- 'FAQBreadcrumb',
82- 'FAQNavigationMenu',
83- 'FAQEditView',
84- 'FAQView',
85- ]
86+ "FAQBreadcrumb",
87+ "FAQNavigationMenu",
88+ "FAQEditView",
89+ "FAQView",
90+]
91
92 from lp import _
93 from lp.answers.interfaces.faq import IFAQ
94 from lp.answers.interfaces.faqcollection import IFAQCollection
95-from lp.app.browser.launchpadform import (
96- action,
97- LaunchpadEditFormView,
98- )
99+from lp.app.browser.launchpadform import LaunchpadEditFormView, action
100 from lp.services.webapp import (
101- canonical_url,
102- enabled_with_permission,
103 Link,
104 NavigationMenu,
105- )
106+ canonical_url,
107+ enabled_with_permission,
108+)
109 from lp.services.webapp.breadcrumb import Breadcrumb
110 from lp.services.webapp.publisher import LaunchpadView
111
112@@ -31,14 +28,14 @@ class FAQNavigationMenu(NavigationMenu):
113 """Context menu of actions that can be performed upon a FAQ."""
114
115 usedfor = IFAQ
116- title = 'Edit FAQ'
117- facet = 'answers'
118- links = ['edit', 'list_all']
119+ title = "Edit FAQ"
120+ facet = "answers"
121+ links = ["edit", "list_all"]
122
123- @enabled_with_permission('launchpad.Edit')
124+ @enabled_with_permission("launchpad.Edit")
125 def edit(self):
126 """Return a Link to the edit view."""
127- return Link('+edit', _('Edit FAQ'), icon='edit')
128+ return Link("+edit", _("Edit FAQ"), icon="edit")
129
130 def list_all(self):
131 """Return a Link to list all FAQs."""
132@@ -46,8 +43,8 @@ class FAQNavigationMenu(NavigationMenu):
133 # on objects which don't provide `IFAQCollection` directly, but for
134 # which an adapter exists that gives the proper context.
135 collection = IFAQCollection(self.context)
136- url = canonical_url(collection, rootsite='answers') + '/+faqs'
137- return Link(url, 'List all FAQs', icon='info')
138+ url = canonical_url(collection, rootsite="answers") + "/+faqs"
139+ return Link(url, "List all FAQs", icon="info")
140
141
142 class FAQBreadcrumb(Breadcrumb):
143@@ -55,7 +52,7 @@ class FAQBreadcrumb(Breadcrumb):
144
145 @property
146 def text(self):
147- return 'FAQ #%d' % self.context.id
148+ return "FAQ #%d" % self.context.id
149
150
151 class FAQView(LaunchpadView):
152@@ -70,14 +67,14 @@ class FAQEditView(LaunchpadEditFormView):
153 """View to change the FAQ details."""
154
155 schema = IFAQ
156- label = _('Edit FAQ')
157+ label = _("Edit FAQ")
158 field_names = ["title", "keywords", "content"]
159
160 @property
161 def page_title(self):
162- return 'Edit FAQ #%s details' % self.context.id
163+ return "Edit FAQ #%s details" % self.context.id
164
165- @action(_('Save'), name="save")
166+ @action(_("Save"), name="save")
167 def save_action(self, action, data):
168 """Update the FAQ details."""
169 self.updateContextFromData(data)
170diff --git a/lib/lp/answers/browser/faqcollection.py b/lib/lp/answers/browser/faqcollection.py
171index a2fb420..f00b3b2 100644
172--- a/lib/lp/answers/browser/faqcollection.py
173+++ b/lib/lp/answers/browser/faqcollection.py
174@@ -4,34 +4,23 @@
175 """IFAQCollection browser views."""
176
177 __all__ = [
178- 'FAQCollectionMenu',
179- 'SearchFAQsView',
180- ]
181+ "FAQCollectionMenu",
182+ "SearchFAQsView",
183+]
184
185 from urllib.parse import urlencode
186
187 from lp import _
188-from lp.answers.enums import (
189- QUESTION_STATUS_DEFAULT_SEARCH,
190- QuestionSort,
191- )
192+from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH, QuestionSort
193 from lp.answers.interfaces.faqcollection import (
194 FAQSort,
195 IFAQCollection,
196 ISearchFAQsForm,
197- )
198-from lp.app.browser.launchpadform import (
199- action,
200- LaunchpadFormView,
201- safe_action,
202- )
203+)
204+from lp.app.browser.launchpadform import LaunchpadFormView, action, safe_action
205 from lp.registry.interfaces.projectgroup import IProjectGroup
206 from lp.services.propertycache import cachedproperty
207-from lp.services.webapp import (
208- canonical_url,
209- Link,
210- NavigationMenu,
211- )
212+from lp.services.webapp import Link, NavigationMenu, canonical_url
213 from lp.services.webapp.batching import BatchNavigator
214 from lp.services.webapp.menu import enabled_with_permission
215
216@@ -40,8 +29,8 @@ class FAQCollectionMenu(NavigationMenu):
217 """Base menu definition for `IFAQCollection`."""
218
219 usedfor = IFAQCollection
220- facet = 'answers'
221- links = ['list_all', 'create_faq']
222+ facet = "answers"
223+ links = ["list_all", "create_faq"]
224
225 def list_all(self):
226 """Return a Link to list all FAQs."""
227@@ -49,21 +38,22 @@ class FAQCollectionMenu(NavigationMenu):
228 # on objects which don't provide `IFAQCollection` directly, but for
229 # which an adapter exists that gives the proper context.
230 collection = IFAQCollection(self.context)
231- url = canonical_url(collection, rootsite='answers') + '/+faqs'
232- return Link(url, 'All FAQs', icon='info')
233+ url = canonical_url(collection, rootsite="answers") + "/+faqs"
234+ return Link(url, "All FAQs", icon="info")
235
236- @enabled_with_permission('launchpad.Append')
237+ @enabled_with_permission("launchpad.Append")
238 def create_faq(self):
239 """Return a Link to create a new FAQ."""
240 collection = IFAQCollection(self.context)
241 if IProjectGroup.providedBy(self.context):
242- url = ''
243+ url = ""
244 enabled = False
245 else:
246 url = canonical_url(
247- collection, view_name='+createfaq', rootsite='answers')
248+ collection, view_name="+createfaq", rootsite="answers"
249+ )
250 enabled = True
251- return Link(url, 'Create a new FAQ', icon='add', enabled=enabled)
252+ return Link(url, "Create a new FAQ", icon="add", enabled=enabled)
253
254
255 class SearchFAQsView(LaunchpadFormView):
256@@ -82,13 +72,15 @@ class SearchFAQsView(LaunchpadFormView):
257 def page_title(self):
258 """Return the page_title that should be used for the listing."""
259 replacements = dict(
260- displayname=self.context.displayname,
261- search_text=self.search_text)
262+ displayname=self.context.displayname, search_text=self.search_text
263+ )
264 if self.search_text:
265- return _('FAQs matching \u201c${search_text}\u201d for '
266- '$displayname', mapping=replacements)
267+ return _(
268+ "FAQs matching \u201c${search_text}\u201d for " "$displayname",
269+ mapping=replacements,
270+ )
271 else:
272- return _('FAQs for $displayname', mapping=replacements)
273+ return _("FAQs for $displayname", mapping=replacements)
274
275 label = page_title
276
277@@ -96,14 +88,18 @@ class SearchFAQsView(LaunchpadFormView):
278 def empty_listing_message(self):
279 """Return the message to render when there are no FAQs to display."""
280 replacements = dict(
281- displayname=self.context.displayname,
282- search_text=self.search_text)
283+ displayname=self.context.displayname, search_text=self.search_text
284+ )
285 if self.search_text:
286- return _('There are no FAQs for $displayname matching '
287- '\u201c${search_text}\u201d.', mapping=replacements)
288+ return _(
289+ "There are no FAQs for $displayname matching "
290+ "\u201c${search_text}\u201d.",
291+ mapping=replacements,
292+ )
293 else:
294- return _('There are no FAQs for $displayname.',
295- mapping=replacements)
296+ return _(
297+ "There are no FAQs for $displayname.", mapping=replacements
298+ )
299
300 def getMatchingFAQs(self):
301 """Return a BatchNavigator of the matching FAQs."""
302@@ -114,7 +110,8 @@ class SearchFAQsView(LaunchpadFormView):
303 def portlet_action(self):
304 """The action URL of the portlet form."""
305 return canonical_url(
306- self.context, view_name='+faqs', rootsite='answers')
307+ self.context, view_name="+faqs", rootsite="answers"
308+ )
309
310 @cachedproperty
311 def latest_faqs(self):
312@@ -124,26 +121,38 @@ class SearchFAQsView(LaunchpadFormView):
313 """
314 quantity = 5
315 faqs = self.context.searchFAQs(
316- search_text=self.search_text, sort=FAQSort.NEWEST_FIRST)
317+ search_text=self.search_text, sort=FAQSort.NEWEST_FIRST
318+ )
319 return list(faqs[:quantity])
320
321 @safe_action
322- @action(_('Search'), name='search')
323+ @action(_("Search"), name="search")
324 def search_action(self, action, data):
325 """Filter the search results by keywords."""
326- self.search_text = data.get('search_text', None)
327+ self.search_text = data.get("search_text", None)
328 if self.search_text:
329 matching_questions = self.context.searchQuestions(
330- search_text=self.search_text)
331+ search_text=self.search_text
332+ )
333 self.matching_questions_count = matching_questions.count()
334
335 @property
336 def matching_questions_url(self):
337 """Return the URL to the questions matching the same keywords."""
338- return canonical_url(self.context) + '/+questions?' + urlencode(
339- {'field.status': [
340- status.title for status in QUESTION_STATUS_DEFAULT_SEARCH],
341- 'field.search_text': self.search_text,
342- 'field.actions.search': 'Search',
343- 'field.sort': QuestionSort.RELEVANCY.title,
344- 'field.language-empty-marker': 1}, doseq=True)
345+ return (
346+ canonical_url(self.context)
347+ + "/+questions?"
348+ + urlencode(
349+ {
350+ "field.status": [
351+ status.title
352+ for status in QUESTION_STATUS_DEFAULT_SEARCH
353+ ],
354+ "field.search_text": self.search_text,
355+ "field.actions.search": "Search",
356+ "field.sort": QuestionSort.RELEVANCY.title,
357+ "field.language-empty-marker": 1,
358+ },
359+ doseq=True,
360+ )
361+ )
362diff --git a/lib/lp/answers/browser/faqtarget.py b/lib/lp/answers/browser/faqtarget.py
363index c433007..7f37282 100644
364--- a/lib/lp/answers/browser/faqtarget.py
365+++ b/lib/lp/answers/browser/faqtarget.py
366@@ -4,28 +4,22 @@
367 """`IFAQTarget` browser views."""
368
369 __all__ = [
370- 'FAQTargetNavigationMixin',
371- 'FAQCreateView',
372- ]
373+ "FAQTargetNavigationMixin",
374+ "FAQCreateView",
375+]
376
377 from lp import _
378 from lp.answers.interfaces.faq import IFAQ
379-from lp.app.browser.launchpadform import (
380- action,
381- LaunchpadFormView,
382- )
383+from lp.app.browser.launchpadform import LaunchpadFormView, action
384 from lp.app.errors import NotFoundError
385 from lp.app.widgets.textwidgets import TokensTextWidget
386-from lp.services.webapp import (
387- canonical_url,
388- stepthrough,
389- )
390+from lp.services.webapp import canonical_url, stepthrough
391
392
393 class FAQTargetNavigationMixin:
394 """Navigation mixin for `IFAQTarget`."""
395
396- @stepthrough('+faq')
397+ @stepthrough("+faq")
398 def traverse_faq(self, name):
399 """Return the FAQ by ID."""
400 try:
401@@ -39,19 +33,22 @@ class FAQCreateView(LaunchpadFormView):
402 """A view to create a new FAQ."""
403
404 schema = IFAQ
405- label = _('Create a new FAQ')
406- field_names = ['title', 'keywords', 'content']
407+ label = _("Create a new FAQ")
408+ field_names = ["title", "keywords", "content"]
409
410 custom_widget_keywords = TokensTextWidget
411
412 @property
413 def page_title(self):
414- return 'Create a FAQ for %s' % self.context.displayname
415+ return "Create a FAQ for %s" % self.context.displayname
416
417- @action(_('Create'), name='create')
418+ @action(_("Create"), name="create")
419 def create__action(self, action, data):
420 """Creates the FAQ."""
421 faq = self.context.newFAQ(
422- self.user, data['title'], data['content'],
423- keywords=data['keywords'])
424+ self.user,
425+ data["title"],
426+ data["content"],
427+ keywords=data["keywords"],
428+ )
429 self.next_url = canonical_url(faq)
430diff --git a/lib/lp/answers/browser/person.py b/lib/lp/answers/browser/person.py
431index 04d0b4d..6d06aa1 100644
432--- a/lib/lp/answers/browser/person.py
433+++ b/lib/lp/answers/browser/person.py
434@@ -4,17 +4,17 @@
435 """Person-related answer listing classes."""
436
437 __all__ = [
438- 'PersonAnswerContactForView',
439- 'PersonAnswersMenu',
440- 'PersonLatestQuestionsView',
441- 'PersonSearchQuestionsView',
442- 'SearchAnsweredQuestionsView',
443- 'SearchAssignedQuestionsView',
444- 'SearchCommentedQuestionsView',
445- 'SearchCreatedQuestionsView',
446- 'SearchNeedAttentionQuestionsView',
447- 'SearchSubscribedQuestionsView',
448- ]
449+ "PersonAnswerContactForView",
450+ "PersonAnswersMenu",
451+ "PersonLatestQuestionsView",
452+ "PersonSearchQuestionsView",
453+ "SearchAnsweredQuestionsView",
454+ "SearchAssignedQuestionsView",
455+ "SearchCommentedQuestionsView",
456+ "SearchCreatedQuestionsView",
457+ "SearchNeedAttentionQuestionsView",
458+ "SearchSubscribedQuestionsView",
459+]
460
461
462 from operator import attrgetter
463@@ -26,10 +26,7 @@ from lp.answers.interfaces.questionsperson import IQuestionsPerson
464 from lp.app.browser.launchpadform import LaunchpadFormView
465 from lp.registry.interfaces.person import IPerson
466 from lp.services.propertycache import cachedproperty
467-from lp.services.webapp import (
468- Link,
469- NavigationMenu,
470- )
471+from lp.services.webapp import Link, NavigationMenu
472 from lp.services.webapp.publisher import LaunchpadView
473
474
475@@ -40,9 +37,10 @@ class PersonLatestQuestionsView(LaunchpadFormView):
476
477 @cachedproperty
478 def getLatestQuestions(self, quantity=5):
479- """Return <quantity> latest questions created for this target. """
480+ """Return <quantity> latest questions created for this target."""
481 return IQuestionsPerson(self.context).searchQuestions(
482- participation=QuestionParticipation.OWNER)[:quantity]
483+ participation=QuestionParticipation.OWNER
484+ )[:quantity]
485
486
487 class PersonSearchQuestionsView(SearchQuestionsView):
488@@ -58,15 +56,19 @@ class PersonSearchQuestionsView(SearchQuestionsView):
489 @property
490 def pageheading(self):
491 """See `SearchQuestionsView`."""
492- return _('Questions involving $name',
493- mapping=dict(name=self.context.displayname))
494+ return _(
495+ "Questions involving $name",
496+ mapping=dict(name=self.context.displayname),
497+ )
498
499 @property
500 def empty_listing_message(self):
501 """See `SearchQuestionsView`."""
502- return _('No questions involving $name found with the '
503- 'requested statuses.',
504- mapping=dict(name=self.context.displayname))
505+ return _(
506+ "No questions involving $name found with the "
507+ "requested statuses.",
508+ mapping=dict(name=self.context.displayname),
509+ )
510
511
512 class SearchAnsweredQuestionsView(PersonSearchQuestionsView):
513@@ -79,15 +81,19 @@ class SearchAnsweredQuestionsView(PersonSearchQuestionsView):
514 @property
515 def pageheading(self):
516 """See `SearchQuestionsView`."""
517- return _('Questions answered by $name',
518- mapping=dict(name=self.context.displayname))
519+ return _(
520+ "Questions answered by $name",
521+ mapping=dict(name=self.context.displayname),
522+ )
523
524 @property
525 def empty_listing_message(self):
526 """See `SearchQuestionsView`."""
527- return _('No questions answered by $name found with the '
528- 'requested statuses.',
529- mapping=dict(name=self.context.displayname))
530+ return _(
531+ "No questions answered by $name found with the "
532+ "requested statuses.",
533+ mapping=dict(name=self.context.displayname),
534+ )
535
536
537 class SearchAssignedQuestionsView(PersonSearchQuestionsView):
538@@ -100,15 +106,19 @@ class SearchAssignedQuestionsView(PersonSearchQuestionsView):
539 @property
540 def pageheading(self):
541 """See `SearchQuestionsView`."""
542- return _('Questions assigned to $name',
543- mapping=dict(name=self.context.displayname))
544+ return _(
545+ "Questions assigned to $name",
546+ mapping=dict(name=self.context.displayname),
547+ )
548
549 @property
550 def empty_listing_message(self):
551 """See `SearchQuestionsView`."""
552- return _('No questions assigned to $name found with the '
553- 'requested statuses.',
554- mapping=dict(name=self.context.displayname))
555+ return _(
556+ "No questions assigned to $name found with the "
557+ "requested statuses.",
558+ mapping=dict(name=self.context.displayname),
559+ )
560
561
562 class SearchCommentedQuestionsView(PersonSearchQuestionsView):
563@@ -121,15 +131,19 @@ class SearchCommentedQuestionsView(PersonSearchQuestionsView):
564 @property
565 def pageheading(self):
566 """See `SearchQuestionsView`."""
567- return _('Questions commented on by $name ',
568- mapping=dict(name=self.context.displayname))
569+ return _(
570+ "Questions commented on by $name ",
571+ mapping=dict(name=self.context.displayname),
572+ )
573
574 @property
575 def empty_listing_message(self):
576 """See `SearchQuestionsView`."""
577- return _('No questions commented on by $name found with the '
578- 'requested statuses.',
579- mapping=dict(name=self.context.displayname))
580+ return _(
581+ "No questions commented on by $name found with the "
582+ "requested statuses.",
583+ mapping=dict(name=self.context.displayname),
584+ )
585
586
587 class SearchCreatedQuestionsView(PersonSearchQuestionsView):
588@@ -142,15 +156,19 @@ class SearchCreatedQuestionsView(PersonSearchQuestionsView):
589 @property
590 def pageheading(self):
591 """See `SearchQuestionsView`."""
592- return _('Questions asked by $name',
593- mapping=dict(name=self.context.displayname))
594+ return _(
595+ "Questions asked by $name",
596+ mapping=dict(name=self.context.displayname),
597+ )
598
599 @property
600 def empty_listing_message(self):
601 """See `SearchQuestionsView`."""
602- return _('No questions asked by $name found with the '
603- 'requested statuses.',
604- mapping=dict(name=self.context.displayname))
605+ return _(
606+ "No questions asked by $name found with the "
607+ "requested statuses.",
608+ mapping=dict(name=self.context.displayname),
609+ )
610
611
612 class SearchNeedAttentionQuestionsView(PersonSearchQuestionsView):
613@@ -163,14 +181,18 @@ class SearchNeedAttentionQuestionsView(PersonSearchQuestionsView):
614 @property
615 def pageheading(self):
616 """See `SearchQuestionsView`."""
617- return _("Questions needing $name's attention",
618- mapping=dict(name=self.context.displayname))
619+ return _(
620+ "Questions needing $name's attention",
621+ mapping=dict(name=self.context.displayname),
622+ )
623
624 @property
625 def empty_listing_message(self):
626 """See `SearchQuestionsView`."""
627- return _("No questions need $name's attention.",
628- mapping=dict(name=self.context.displayname))
629+ return _(
630+ "No questions need $name's attention.",
631+ mapping=dict(name=self.context.displayname),
632+ )
633
634
635 class SearchSubscribedQuestionsView(PersonSearchQuestionsView):
636@@ -183,15 +205,19 @@ class SearchSubscribedQuestionsView(PersonSearchQuestionsView):
637 @property
638 def pageheading(self):
639 """See `SearchQuestionsView`."""
640- return _('Questions $name is subscribed to',
641- mapping=dict(name=self.context.displayname))
642+ return _(
643+ "Questions $name is subscribed to",
644+ mapping=dict(name=self.context.displayname),
645+ )
646
647 @property
648 def empty_listing_message(self):
649 """See `SearchQuestionsView`."""
650- return _('No questions subscribed to by $name found with the '
651- 'requested statuses.',
652- mapping=dict(name=self.context.displayname))
653+ return _(
654+ "No questions subscribed to by $name found with the "
655+ "requested statuses.",
656+ mapping=dict(name=self.context.displayname),
657+ )
658
659
660 class PersonAnswerContactForView(LaunchpadView):
661@@ -201,8 +227,9 @@ class PersonAnswerContactForView(LaunchpadView):
662
663 @property
664 def label(self):
665- return 'Projects for which %s is an answer contact' % (
666- self.context.displayname)
667+ return "Projects for which %s is an answer contact" % (
668+ self.context.displayname
669+ )
670
671 page_title = label
672
673@@ -214,7 +241,8 @@ class PersonAnswerContactForView(LaunchpadView):
674 """
675 return sorted(
676 IQuestionsPerson(self.context).getDirectAnswerQuestionTargets(),
677- key=attrgetter('title'))
678+ key=attrgetter("title"),
679+ )
680
681 @cachedproperty
682 def team_question_targets(self):
683@@ -224,7 +252,8 @@ class PersonAnswerContactForView(LaunchpadView):
684 """
685 return sorted(
686 IQuestionsPerson(self.context).getTeamAnswerQuestionTargets(),
687- key=attrgetter('title'))
688+ key=attrgetter("title"),
689+ )
690
691 def showRemoveYourselfLink(self):
692 """The link is shown when the page is in the user's own profile."""
693@@ -234,44 +263,53 @@ class PersonAnswerContactForView(LaunchpadView):
694 class PersonAnswersMenu(NavigationMenu):
695
696 usedfor = IPerson
697- facet = 'answers'
698- links = ['answered', 'assigned', 'created', 'commented', 'need_attention',
699- 'subscribed', 'answer_contact_for']
700+ facet = "answers"
701+ links = [
702+ "answered",
703+ "assigned",
704+ "created",
705+ "commented",
706+ "need_attention",
707+ "subscribed",
708+ "answer_contact_for",
709+ ]
710
711 def answer_contact_for(self):
712 summary = "Projects for which %s is an answer contact" % (
713- self.context.displayname)
714+ self.context.displayname
715+ )
716 return Link(
717- '+answer-contact-for', 'Answer contact for', summary, icon='edit')
718+ "+answer-contact-for", "Answer contact for", summary, icon="edit"
719+ )
720
721 def answered(self):
722- summary = 'Questions answered by %s' % self.context.displayname
723- return Link(
724- '+answeredquestions', 'Answered', summary, icon='question')
725+ summary = "Questions answered by %s" % self.context.displayname
726+ return Link("+answeredquestions", "Answered", summary, icon="question")
727
728 def assigned(self):
729- summary = 'Questions assigned to %s' % self.context.displayname
730- return Link(
731- '+assignedquestions', 'Assigned', summary, icon='question')
732+ summary = "Questions assigned to %s" % self.context.displayname
733+ return Link("+assignedquestions", "Assigned", summary, icon="question")
734
735 def created(self):
736- summary = 'Questions asked by %s' % self.context.displayname
737- return Link('+createdquestions', 'Asked', summary, icon='question')
738+ summary = "Questions asked by %s" % self.context.displayname
739+ return Link("+createdquestions", "Asked", summary, icon="question")
740
741 def commented(self):
742- summary = 'Questions commented on by %s' % (
743- self.context.displayname)
744+ summary = "Questions commented on by %s" % (self.context.displayname)
745 return Link(
746- '+commentedquestions', 'Commented', summary, icon='question')
747+ "+commentedquestions", "Commented", summary, icon="question"
748+ )
749
750 def need_attention(self):
751- summary = 'Questions needing %s attention' % (
752- self.context.displayname)
753- return Link('+needattentionquestions', 'Need attention', summary,
754- icon='question')
755+ summary = "Questions needing %s attention" % (self.context.displayname)
756+ return Link(
757+ "+needattentionquestions",
758+ "Need attention",
759+ summary,
760+ icon="question",
761+ )
762
763 def subscribed(self):
764- text = 'Subscribed'
765- summary = 'Questions subscribed to by %s' % (
766- self.context.displayname)
767- return Link('+subscribedquestions', text, summary, icon='question')
768+ text = "Subscribed"
769+ summary = "Questions subscribed to by %s" % (self.context.displayname)
770+ return Link("+subscribedquestions", text, summary, icon="question")
771diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
772index a72083e..32d63dc 100644
773--- a/lib/lp/answers/browser/question.py
774+++ b/lib/lp/answers/browser/question.py
775@@ -4,62 +4,46 @@
776 """Question views."""
777
778 __all__ = [
779- 'SearchAllQuestionsView',
780- 'QuestionAddView',
781- 'QuestionBreadcrumb',
782- 'QuestionChangeStatusView',
783- 'QuestionConfirmAnswerView',
784- 'QuestionCreateFAQView',
785- 'QuestionEditMenu',
786- 'QuestionEditView',
787- 'QuestionExtrasMenu',
788- 'QuestionHistoryView',
789- 'QuestionLinkFAQView',
790- 'QuestionMessageDisplayView',
791- 'QuestionSetContextMenu',
792- 'QuestionSetNavigation',
793- 'QuestionRejectView',
794- 'QuestionSetView',
795- 'QuestionSubscriptionView',
796- 'QuestionWorkflowView',
797- ]
798+ "SearchAllQuestionsView",
799+ "QuestionAddView",
800+ "QuestionBreadcrumb",
801+ "QuestionChangeStatusView",
802+ "QuestionConfirmAnswerView",
803+ "QuestionCreateFAQView",
804+ "QuestionEditMenu",
805+ "QuestionEditView",
806+ "QuestionExtrasMenu",
807+ "QuestionHistoryView",
808+ "QuestionLinkFAQView",
809+ "QuestionMessageDisplayView",
810+ "QuestionSetContextMenu",
811+ "QuestionSetNavigation",
812+ "QuestionRejectView",
813+ "QuestionSetView",
814+ "QuestionSubscriptionView",
815+ "QuestionWorkflowView",
816+]
817
818-from operator import attrgetter
819 import re
820+from operator import attrgetter
821
822+import zope.security
823 from lazr.restful.interface import copy_field
824 from lazr.restful.utils import smartquote
825 from zope.browserpage import ViewPageTemplateFile
826 from zope.component import getUtility
827 from zope.formlib import form
828 from zope.formlib.interfaces import IWidgetFactory
829-from zope.formlib.widget import (
830- CustomWidgetFactory,
831- renderElement,
832- )
833-from zope.formlib.widgets import (
834- TextAreaWidget,
835- TextWidget,
836- )
837-from zope.interface import (
838- alsoProvides,
839- implementer,
840- )
841+from zope.formlib.widget import CustomWidgetFactory, renderElement
842+from zope.formlib.widgets import TextAreaWidget, TextWidget
843+from zope.interface import alsoProvides, implementer
844 from zope.schema import Choice
845 from zope.schema.interfaces import IContextSourceBinder
846-from zope.schema.vocabulary import (
847- SimpleTerm,
848- SimpleVocabulary,
849- )
850-import zope.security
851+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
852
853 from lp import _
854 from lp.answers.browser.questiontarget import SearchQuestionsView
855-from lp.answers.enums import (
856- QuestionAction,
857- QuestionSort,
858- QuestionStatus,
859- )
860+from lp.answers.enums import QuestionAction, QuestionSort, QuestionStatus
861 from lp.answers.interfaces.faq import IFAQ
862 from lp.answers.interfaces.faqtarget import IFAQTarget
863 from lp.answers.interfaces.question import (
864@@ -67,30 +51,24 @@ from lp.answers.interfaces.question import (
865 IQuestionAddMessageForm,
866 IQuestionChangeStatusForm,
867 IQuestionLinkFAQForm,
868- )
869+)
870 from lp.answers.interfaces.questioncollection import IQuestionSet
871 from lp.answers.interfaces.questionmessage import IQuestionMessage
872 from lp.answers.interfaces.questiontarget import (
873 IAnswersFrontPageSearchForm,
874 IQuestionTarget,
875- )
876+)
877 from lp.answers.vocabulary import UsesAnswersDistributionVocabulary
878 from lp.app.browser.launchpadform import (
879- action,
880 LaunchpadEditFormView,
881 LaunchpadFormView,
882+ action,
883 safe_action,
884- )
885+)
886 from lp.app.browser.stringformatter import FormattersAPI
887 from lp.app.enums import ServiceUsage
888-from lp.app.errors import (
889- NotFoundError,
890- UnexpectedFormData,
891- )
892-from lp.app.interfaces.launchpad import (
893- ILaunchpadCelebrities,
894- IServiceUsage,
895- )
896+from lp.app.errors import NotFoundError, UnexpectedFormData
897+from lp.app.interfaces.launchpad import ILaunchpadCelebrities, IServiceUsage
898 from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
899 from lp.app.widgets.launchpadtarget import LaunchpadTargetWidget
900 from lp.app.widgets.project import ProjectScopeWidget
901@@ -101,15 +79,15 @@ from lp.services.propertycache import cachedproperty
902 from lp.services.statistics.interfaces.statistic import ILaunchpadStatisticSet
903 from lp.services.webapp import (
904 ApplicationMenu,
905- canonical_url,
906 ContextMenu,
907- enabled_with_permission,
908 LaunchpadView,
909 Link,
910 Navigation,
911 NavigationMenu,
912+ canonical_url,
913+ enabled_with_permission,
914 stepthrough,
915- )
916+)
917 from lp.services.webapp.authorization import check_permission
918 from lp.services.webapp.breadcrumb import Breadcrumb
919 from lp.services.webapp.escaping import structured
920@@ -118,7 +96,7 @@ from lp.services.webapp.snapshot import notify_modified
921 from lp.services.worlddata.helpers import (
922 is_english_variant,
923 preferred_or_request_languages,
924- )
925+)
926
927
928 class QuestionLinksMixin:
929@@ -127,117 +105,139 @@ class QuestionLinksMixin:
930 def subscription(self):
931 """Return a Link to the subscription view."""
932 if self.user is not None and self.context.isSubscribed(self.user):
933- text = 'Unsubscribe'
934- icon = 'remove'
935- summary = ('You will stop receiving email notifications about '
936- 'updates to this question')
937+ text = "Unsubscribe"
938+ icon = "remove"
939+ summary = (
940+ "You will stop receiving email notifications about "
941+ "updates to this question"
942+ )
943 else:
944- text = 'Subscribe'
945- icon = 'add'
946- summary = ('You will receive email notifications about updates '
947- 'to this question')
948- return Link('+subscribe', text, icon=icon, summary=summary)
949+ text = "Subscribe"
950+ icon = "add"
951+ summary = (
952+ "You will receive email notifications about updates "
953+ "to this question"
954+ )
955+ return Link("+subscribe", text, icon=icon, summary=summary)
956
957 def addsubscriber(self):
958 """Return the 'Subscribe someone else' Link."""
959- text = 'Subscribe someone else'
960+ text = "Subscribe someone else"
961 return Link(
962- '+addsubscriber', text, icon='add', summary=(
963- 'Launchpad will email that person whenever this question '
964- 'changes'))
965+ "+addsubscriber",
966+ text,
967+ icon="add",
968+ summary=(
969+ "Launchpad will email that person whenever this question "
970+ "changes"
971+ ),
972+ )
973
974 def edit(self):
975 """Return a Link to the edit view."""
976- text = 'Edit question'
977- return Link('+edit', text, icon='edit')
978+ text = "Edit question"
979+ return Link("+edit", text, icon="edit")
980
981
982 class QuestionEditMenu(NavigationMenu, QuestionLinksMixin):
983 """A menu for different aspects of editing a object."""
984
985 usedfor = IQuestion
986- facet = 'answers'
987- title = 'Edit question'
988- links = ['edit', 'reject']
989+ facet = "answers"
990+ title = "Edit question"
991+ links = ["edit", "reject"]
992
993 def reject(self):
994 """Return a Link to the reject view."""
995 enabled = self.user is not None and self.context.canReject(self.user)
996- text = 'Reject question'
997- return Link('+reject', text, icon='edit', enabled=enabled)
998+ text = "Reject question"
999+ return Link("+reject", text, icon="edit", enabled=enabled)
1000
1001
1002 class QuestionExtrasMenu(ApplicationMenu, QuestionLinksMixin):
1003 """Context menu of actions that can be performed upon a Question."""
1004+
1005 usedfor = IQuestion
1006- facet = 'answers'
1007+ facet = "answers"
1008 links = [
1009- 'history', 'linkbug', 'unlinkbug', 'makebug', 'linkfaq',
1010- 'createfaq', 'edit', 'changestatus', 'subscription', 'addsubscriber']
1011+ "history",
1012+ "linkbug",
1013+ "unlinkbug",
1014+ "makebug",
1015+ "linkfaq",
1016+ "createfaq",
1017+ "edit",
1018+ "changestatus",
1019+ "subscription",
1020+ "addsubscriber",
1021+ ]
1022
1023 def initialize(self):
1024 """Initialize the menu from the Question's state."""
1025 self.has_bugs = bool(self.context.bugs)
1026
1027- @enabled_with_permission('launchpad.Admin')
1028+ @enabled_with_permission("launchpad.Admin")
1029 def changestatus(self):
1030 """Return a Link to the change status view."""
1031- return Link('+change-status', _('Change status'), icon='edit')
1032+ return Link("+change-status", _("Change status"), icon="edit")
1033
1034 def history(self):
1035 """Return a Link to the history view."""
1036- text = 'History'
1037- return Link('+history', text, icon='list',
1038- enabled=bool(self.context.messages))
1039+ text = "History"
1040+ return Link(
1041+ "+history", text, icon="list", enabled=bool(self.context.messages)
1042+ )
1043
1044 def linkbug(self):
1045 """Return a Link to the link bug view."""
1046- text = 'Link existing bug'
1047- return Link('+linkbug', text, icon='add')
1048+ text = "Link existing bug"
1049+ return Link("+linkbug", text, icon="add")
1050
1051 def unlinkbug(self):
1052 """Return a Link to the unlink bug view."""
1053- text = 'Remove bug link'
1054- return Link('+unlinkbug', text, icon='remove', enabled=self.has_bugs)
1055+ text = "Remove bug link"
1056+ return Link("+unlinkbug", text, icon="remove", enabled=self.has_bugs)
1057
1058 def makebug(self):
1059 """Return a Link to the make bug view."""
1060- text = 'Create bug report'
1061- summary = 'Create a bug report from this question.'
1062- return Link('+makebug', text, summary, icon='add',
1063- enabled=not self.has_bugs)
1064+ text = "Create bug report"
1065+ summary = "Create a bug report from this question."
1066+ return Link(
1067+ "+makebug", text, summary, icon="add", enabled=not self.has_bugs
1068+ )
1069
1070 def linkfaq(self):
1071 """Link for linking to a FAQ."""
1072- text = 'Link to a FAQ'
1073- summary = 'Link this question to a FAQ.'
1074+ text = "Link to a FAQ"
1075+ summary = "Link this question to a FAQ."
1076 if self.context.faq is None:
1077- icon = 'add'
1078+ icon = "add"
1079 else:
1080- icon = 'edit'
1081- return Link('+linkfaq', text, summary, icon=icon)
1082+ icon = "edit"
1083+ return Link("+linkfaq", text, summary, icon=icon)
1084
1085 def createfaq(self):
1086 """LInk for creating a FAQ."""
1087- text = 'Create a new FAQ'
1088- summary = 'Create a new FAQ from this question.'
1089- return Link('+createfaq', text, summary, icon='add')
1090+ text = "Create a new FAQ"
1091+ summary = "Create a new FAQ from this question."
1092+ return Link("+createfaq", text, summary, icon="add")
1093
1094
1095 class QuestionSetContextMenu(ContextMenu):
1096 """Context menu of actions that can be preformed upon a QuestionSet."""
1097+
1098 usedfor = IQuestionSet
1099- links = ['findproduct', 'finddistro']
1100+ links = ["findproduct", "finddistro"]
1101
1102 def findproduct(self):
1103 """Return a Link to the find product view."""
1104- text = 'Find upstream project'
1105- return Link('/projects', text, icon='search')
1106+ text = "Find upstream project"
1107+ return Link("/projects", text, icon="search")
1108
1109 def finddistro(self):
1110 """Return a Link to the find distribution view."""
1111- text = 'Find distribution'
1112- return Link('/distros', text, icon='search')
1113+ text = "Find distribution"
1114+ return Link("/distros", text, icon="search")
1115
1116
1117 class QuestionSetNavigation(Navigation):
1118@@ -254,7 +254,8 @@ class QuestionSetNavigation(Navigation):
1119 if question is None:
1120 raise NotFoundError(name)
1121 return self.redirectSubTree(
1122- canonical_url(question, self.request), status=301)
1123+ canonical_url(question, self.request), status=301
1124+ )
1125
1126
1127 class QuestionNavigation(Navigation):
1128@@ -262,7 +263,7 @@ class QuestionNavigation(Navigation):
1129
1130 usedfor = IQuestion
1131
1132- @stepthrough('messages')
1133+ @stepthrough("messages")
1134 def traverse_messages(self, index):
1135 try:
1136 index = int(index) - 1
1137@@ -279,7 +280,7 @@ class QuestionMessageNavigation(Navigation):
1138
1139 usedfor = IQuestionMessage
1140
1141- @stepthrough('revisions')
1142+ @stepthrough("revisions")
1143 def traverse_revisions(self, revision):
1144 try:
1145 revision = int(revision)
1146@@ -293,7 +294,7 @@ class QuestionBreadcrumb(Breadcrumb):
1147
1148 @property
1149 def text(self):
1150- return 'Question #%d' % self.context.id
1151+ return "Question #%d" % self.context.id
1152
1153
1154 class QuestionSetView(LaunchpadFormView):
1155@@ -302,67 +303,73 @@ class QuestionSetView(LaunchpadFormView):
1156 schema = IAnswersFrontPageSearchForm
1157 custom_widget_scope = ProjectScopeWidget
1158
1159- page_title = 'Launchpad Answers'
1160- label = 'Questions and Answers'
1161+ page_title = "Launchpad Answers"
1162+ label = "Questions and Answers"
1163
1164 @property
1165 def scope_css_class(self):
1166 """The CSS class for used in the scope widget."""
1167 if self.scope_error:
1168- return 'error'
1169+ return "error"
1170 else:
1171 return None
1172
1173 @property
1174 def scope_error(self):
1175 """The error message for the scope widget."""
1176- return self.getFieldError('scope')
1177+ return self.getFieldError("scope")
1178
1179 @safe_action
1180- @action('Find Answers', name="search")
1181+ @action("Find Answers", name="search")
1182 def search_action(self, action, data):
1183 """Redirect to the proper search page based on the scope widget."""
1184 # For the scope to be absent from the form, the user must
1185 # build the query string themselves - most likely because they
1186 # are a bot. In that case we just assume they want to search
1187 # all projects.
1188- scope = self.widgets['scope'].getScope()
1189- if scope is None or scope == 'all':
1190+ scope = self.widgets["scope"].getScope()
1191+ if scope is None or scope == "all":
1192 # Use 'All projects' scope.
1193 scope = self.context
1194 else:
1195- scope = self.widgets['scope'].getInputValue()
1196+ scope = self.widgets["scope"].getInputValue()
1197 self.next_url = "%s/+tickets?%s" % (
1198- canonical_url(scope), self.request['QUERY_STRING'])
1199+ canonical_url(scope),
1200+ self.request["QUERY_STRING"],
1201+ )
1202
1203 @property
1204 def question_count(self):
1205 """Return the number of questions in the system."""
1206- return getUtility(ILaunchpadStatisticSet).value('question_count')
1207+ return getUtility(ILaunchpadStatisticSet).value("question_count")
1208
1209 @property
1210 def answered_question_count(self):
1211 """Return the number of answered questions in the system."""
1212 return getUtility(ILaunchpadStatisticSet).value(
1213- 'answered_question_count')
1214+ "answered_question_count"
1215+ )
1216
1217 @property
1218 def solved_question_count(self):
1219 """Return the number of solved questions in the system."""
1220 return getUtility(ILaunchpadStatisticSet).value(
1221- 'solved_question_count')
1222+ "solved_question_count"
1223+ )
1224
1225 @property
1226 def projects_with_questions_count(self):
1227 """Return the number of projects with questions in the system."""
1228 return getUtility(ILaunchpadStatisticSet).value(
1229- 'projects_with_questions_count')
1230+ "projects_with_questions_count"
1231+ )
1232
1233 @property
1234 def latest_questions_asked(self):
1235 """Return the 5 latest questions asked."""
1236 return self.context.searchQuestions(
1237- status=QuestionStatus.OPEN, sort=QuestionSort.NEWEST_FIRST)[:5]
1238+ status=QuestionStatus.OPEN, sort=QuestionSort.NEWEST_FIRST
1239+ )[:5]
1240
1241 @property
1242 def latest_questions_solved(self):
1243@@ -370,7 +377,8 @@ class QuestionSetView(LaunchpadFormView):
1244 # XXX flacoste 2006-11-28: We should probably define a new
1245 # QuestionSort value allowing us to sort on dateanswered descending.
1246 return self.context.searchQuestions(
1247- status=QuestionStatus.SOLVED, sort=QuestionSort.NEWEST_FIRST)[:5]
1248+ status=QuestionStatus.SOLVED, sort=QuestionSort.NEWEST_FIRST
1249+ )[:5]
1250
1251 @property
1252 def most_active_projects(self):
1253@@ -394,30 +402,32 @@ class QuestionSubscriptionView(LaunchpadView):
1254 with notify_modified(self.context, modified_fields):
1255 response = self.request.response
1256 # Establish if a subscription form was posted.
1257- newsub = self.request.form.get('subscribe', None)
1258+ newsub = self.request.form.get("subscribe", None)
1259 if newsub is not None:
1260- if newsub == 'Subscribe':
1261+ if newsub == "Subscribe":
1262 self.context.subscribe(self.user)
1263 response.addNotification(
1264- _("You have subscribed to this question."))
1265- modified_fields.add('subscribers')
1266- elif newsub == 'Unsubscribe':
1267+ _("You have subscribed to this question.")
1268+ )
1269+ modified_fields.add("subscribers")
1270+ elif newsub == "Unsubscribe":
1271 self.context.unsubscribe(self.user, self.user)
1272 response.addNotification(
1273- _("You have unsubscribed from this question."))
1274- modified_fields.add('subscribers')
1275+ _("You have unsubscribed from this question.")
1276+ )
1277+ modified_fields.add("subscribers")
1278 response.redirect(canonical_url(self.context))
1279
1280 @property
1281 def page_title(self):
1282- return 'Subscription'
1283+ return "Subscription"
1284
1285 @property
1286 def label(self):
1287 if self.subscription:
1288- return 'Unsubscribe from question'
1289+ return "Unsubscribe from question"
1290 else:
1291- return 'Subscribe to question'
1292+ return "Subscribe to question"
1293
1294 @property
1295 def subscription(self):
1296@@ -468,8 +478,10 @@ class QuestionLanguageVocabularyFactory:
1297 if context is not None and not IProjectGroup.providedBy(context):
1298 question_target = IQuestionTarget(context)
1299 supported_languages = question_target.getSupportedLanguages()
1300- elif (IProjectGroup.providedBy(context) and
1301- self.view.question_target is not None):
1302+ elif (
1303+ IProjectGroup.providedBy(context)
1304+ and self.view.question_target is not None
1305+ ):
1306 # ProjectGroups do not implement IQuestionTarget--the user must
1307 # choose a product while asking a question.
1308 question_target = IQuestionTarget(self.view.question_target)
1309@@ -498,13 +510,14 @@ class QuestionSupportLanguageMixin:
1310 """
1311
1312 supported_languages_macros = ViewPageTemplateFile(
1313- '../templates/question-supported-languages-macros.pt')
1314+ "../templates/question-supported-languages-macros.pt"
1315+ )
1316
1317 @property
1318 def chosen_language(self):
1319 """Return the language chosen by the user."""
1320- if self.widgets['language'].hasInput():
1321- return self.widgets['language'].getInputValue()
1322+ if self.widgets["language"].hasInput():
1323+ return self.widgets["language"].getInputValue()
1324 else:
1325 return self.context.language
1326
1327@@ -512,7 +525,7 @@ class QuestionSupportLanguageMixin:
1328 def unsupported_languages_warning(self):
1329 """Macro displaying a warning in case of unsupported languages."""
1330 macros = self.supported_languages_macros.macros
1331- return macros['unsupported_languages_warning']
1332+ return macros["unsupported_languages_warning"]
1333
1334 @property
1335 def question_target(self):
1336@@ -524,7 +537,8 @@ class QuestionSupportLanguageMixin:
1337 """Return the list of supported languages ordered by name."""
1338 return sorted(
1339 self.question_target.getSupportedLanguages(),
1340- key=attrgetter('englishname'))
1341+ key=attrgetter("englishname"),
1342+ )
1343
1344 def createLanguageField(self):
1345 """Create a field with a vocabulary to edit a question language.
1346@@ -533,16 +547,19 @@ class QuestionSupportLanguageMixin:
1347 :return: A form.Fields instance containing the language field.
1348 """
1349 return form.Fields(
1350- Choice(
1351- __name__='language',
1352- source=QuestionLanguageVocabularyFactory(view=self),
1353- title=_('Language'),
1354- description=_(
1355- "The language in which this question is written. "
1356- "The languages marked with a star (*) are the "
1357- "languages spoken by at least one answer contact in "
1358- "the community.")),
1359- render_context=self.render_context)
1360+ Choice(
1361+ __name__="language",
1362+ source=QuestionLanguageVocabularyFactory(view=self),
1363+ title=_("Language"),
1364+ description=_(
1365+ "The language in which this question is written. "
1366+ "The languages marked with a star (*) are the "
1367+ "languages spoken by at least one answer contact in "
1368+ "the community."
1369+ ),
1370+ ),
1371+ render_context=self.render_context,
1372+ )
1373
1374 def shouldWarnAboutUnsupportedLanguage(self):
1375 """Test if the warning about unsupported language should be displayed.
1376@@ -552,11 +569,13 @@ class QuestionSupportLanguageMixin:
1377 will only be displayed one time, except if the user changes the
1378 request language to another unsupported value.
1379 """
1380- if (self.chosen_language in
1381- self.question_target.getSupportedLanguages()):
1382+ if (
1383+ self.chosen_language
1384+ in self.question_target.getSupportedLanguages()
1385+ ):
1386 return False
1387
1388- old_chosen_language = self.request.form.get('chosen_language')
1389+ old_chosen_language = self.request.form.get("chosen_language")
1390 return self.chosen_language.code != old_chosen_language
1391
1392
1393@@ -565,7 +584,7 @@ class QuestionHistoryView(LaunchpadView):
1394
1395 @property
1396 def page_title(self):
1397- return 'History of question #%s' % self.context.id
1398+ return "History of question #%s" % self.context.id
1399
1400 label = page_title
1401
1402@@ -576,22 +595,25 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
1403 The user enters first their question summary and then they are shown a
1404 list of similar results before adding the question.
1405 """
1406- label = _('Ask a question')
1407+
1408+ label = _("Ask a question")
1409
1410 schema = IQuestion
1411
1412- field_names = ['title', 'description']
1413+ field_names = ["title", "description"]
1414
1415 # The fields displayed on the search page.
1416- search_field_names = ['language', 'title']
1417+ search_field_names = ["language", "title"]
1418
1419 custom_widget_title = CustomWidgetFactory(
1420- TextWidget, displayWidth=40, displayMaxWidth=250)
1421+ TextWidget, displayWidth=40, displayMaxWidth=250
1422+ )
1423
1424 search_template = ViewPageTemplateFile(
1425- '../templates/question-add-search.pt')
1426+ "../templates/question-add-search.pt"
1427+ )
1428
1429- add_template = ViewPageTemplateFile('../templates/question-add.pt')
1430+ add_template = ViewPageTemplateFile("../templates/question-add.pt")
1431
1432 template = search_template
1433
1434@@ -629,7 +651,7 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
1435 else:
1436 fields = self.form_fields
1437 for field in fields:
1438- widget = getattr(self, 'custom_widget_%s' % field.__name__, None)
1439+ widget = getattr(self, "custom_widget_%s" % field.__name__, None)
1440 if widget is not None:
1441 if IWidgetFactory.providedBy(widget):
1442 field.custom_widget = widget
1443@@ -642,32 +664,42 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
1444 """Set up the widgets using the view's form fields and the context."""
1445 fields = self._getFieldsForWidgets()
1446 self.widgets = form.setUpWidgets(
1447- fields, self.prefix, self.context, self.request,
1448- data=self.initial_values, ignore_request=False)
1449+ fields,
1450+ self.prefix,
1451+ self.context,
1452+ self.request,
1453+ data=self.initial_values,
1454+ ignore_request=False,
1455+ )
1456
1457 def validate(self, data):
1458 """Validate hook.
1459
1460 This validation method sets the chosen_language attribute.
1461 """
1462- if 'title' not in data:
1463+ if "title" not in data:
1464 self.setFieldError(
1465- 'title', _('You must enter a summary of your problem.'))
1466+ "title", _("You must enter a summary of your problem.")
1467+ )
1468 else:
1469- if len(data['title']) > 250:
1470+ if len(data["title"]) > 250:
1471 self.setFieldError(
1472- 'title', _('The summary cannot exceed 250 characters.'))
1473- if self.widgets.get('description'):
1474- if 'description' not in data:
1475+ "title", _("The summary cannot exceed 250 characters.")
1476+ )
1477+ if self.widgets.get("description"):
1478+ if "description" not in data:
1479 self.setFieldError(
1480- 'description',
1481- _('You must provide details about your problem.'))
1482+ "description",
1483+ _("You must provide details about your problem."),
1484+ )
1485
1486 @property
1487 def page_title(self):
1488 """The current page title."""
1489- return _('Ask a question about ${context}',
1490- mapping=dict(context=self.context.displayname))
1491+ return _(
1492+ "Ask a question about ${context}",
1493+ mapping=dict(context=self.context.displayname),
1494+ )
1495
1496 @property
1497 def has_similar_items(self):
1498@@ -683,21 +715,25 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
1499 else:
1500 return False
1501
1502- @action(_('Continue'))
1503+ @action(_("Continue"))
1504 def continue_action(self, action, data):
1505 """Search for questions and FAQs similar to the entered summary."""
1506 # If the description widget wasn't setup, add it here
1507- if self.widgets.get('description') is None:
1508+ if self.widgets.get("description") is None:
1509 self.widgets += form.setUpWidgets(
1510- self.form_fields.select('description'), self.prefix,
1511- self.context, self.request, data=self.initial_values,
1512- ignore_request=False)
1513+ self.form_fields.select("description"),
1514+ self.prefix,
1515+ self.context,
1516+ self.request,
1517+ data=self.initial_values,
1518+ ignore_request=False,
1519+ )
1520
1521- faqs = IFAQTarget(self.question_target).findSimilarFAQs(data['title'])
1522- self.similar_faqs = list(faqs[:self._MAX_SIMILAR_FAQS])
1523+ faqs = IFAQTarget(self.question_target).findSimilarFAQs(data["title"])
1524+ self.similar_faqs = list(faqs[: self._MAX_SIMILAR_FAQS])
1525
1526- questions = self.question_target.findSimilarQuestions(data['title'])
1527- self.similar_questions = list(questions[:self._MAX_SIMILAR_QUESTIONS])
1528+ questions = self.question_target.findSimilarQuestions(data["title"])
1529+ self.similar_questions = list(questions[: self._MAX_SIMILAR_QUESTIONS])
1530
1531 return self.add_template()
1532
1533@@ -706,15 +742,16 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
1534 to the search template when the summary is missing or delegate to
1535 the continue action handler to do the search.
1536 """
1537- if 'title' not in data:
1538+ if "title" not in data:
1539 # Remove the description widget.
1540- widgets = [(True, self.widgets[name])
1541- for name in self.search_field_names]
1542+ widgets = [
1543+ (True, self.widgets[name]) for name in self.search_field_names
1544+ ]
1545 self.widgets = form.Widgets(widgets, len(self.prefix) + 1)
1546 return self.search_template()
1547 return self.continue_action.success(data)
1548
1549- @action(_('Post Question'), name='add', failure='handleAddError')
1550+ @action(_("Post Question"), name="add", failure="handleAddError")
1551 def add_action(self, action, data):
1552 """Add a Question to an `IQuestionTarget`."""
1553 if self.shouldWarnAboutUnsupportedLanguage():
1554@@ -723,41 +760,42 @@ class QuestionAddView(QuestionSupportLanguageMixin, LaunchpadFormView):
1555 return self.continue_action.success(data)
1556
1557 question = self.question_target.newQuestion(
1558- self.user, data['title'], data['description'], data['language'])
1559+ self.user, data["title"], data["description"], data["language"]
1560+ )
1561
1562 self.request.response.redirect(canonical_url(question))
1563- return ''
1564+ return ""
1565
1566
1567 class QuestionChangeStatusView(LaunchpadFormView):
1568 """View for changing a question status."""
1569+
1570 schema = IQuestionChangeStatusForm
1571- label = 'Change question status'
1572+ label = "Change question status"
1573
1574 @property
1575 def page_title(self):
1576- return 'Change status of question #%s' % self.context.id
1577+ return "Change status of question #%s" % self.context.id
1578
1579 def validate(self, data):
1580 """Check that the status and message are valid."""
1581- if data.get('status') == self.context.status:
1582+ if data.get("status") == self.context.status:
1583+ self.setFieldError("status", _("You didn't change the status."))
1584+ if not data.get("message"):
1585 self.setFieldError(
1586- 'status', _("You didn't change the status."))
1587- if not data.get('message'):
1588- self.setFieldError(
1589- 'message', _('You must provide an explanation message.'))
1590+ "message", _("You must provide an explanation message.")
1591+ )
1592
1593 @property
1594 def initial_values(self):
1595 """Return the initial view values."""
1596- return {'status': self.context.status}
1597+ return {"status": self.context.status}
1598
1599- @action(_('Change Status'), name='change-status')
1600+ @action(_("Change Status"), name="change-status")
1601 def change_status_action(self, action, data):
1602 """Change the Question status."""
1603- self.context.setStatus(self.user, data['status'], data['message'])
1604- self.request.response.addNotification(
1605- _('Question status updated.'))
1606+ self.context.setStatus(self.user, data["status"], data["message"])
1607+ self.request.response.addNotification(_("Question status updated."))
1608
1609 @property
1610 def next_url(self):
1611@@ -770,7 +808,7 @@ class QuestionTargetWidget(LaunchpadTargetWidget):
1612 """A targeting widget that is aware of pillars that use Answers."""
1613
1614 def getProductVocabulary(self):
1615- return 'UsesAnswersProduct'
1616+ return "UsesAnswersProduct"
1617
1618 def getDistributionVocabulary(self):
1619 distro = self.context.context.distribution
1620@@ -780,11 +818,17 @@ class QuestionTargetWidget(LaunchpadTargetWidget):
1621
1622 class QuestionEditView(LaunchpadEditFormView):
1623 """View for editing a Question."""
1624+
1625 schema = IQuestion
1626- label = 'Edit question'
1627+ label = "Edit question"
1628 field_names = [
1629- "language", "title", "description", "target", "assignee",
1630- "whiteboard"]
1631+ "language",
1632+ "title",
1633+ "description",
1634+ "target",
1635+ "assignee",
1636+ "whiteboard",
1637+ ]
1638
1639 custom_widget_title = CustomWidgetFactory(TextWidget, displayWidth=40)
1640 custom_widget_whiteboard = CustomWidgetFactory(TextAreaWidget, height=5)
1641@@ -792,7 +836,7 @@ class QuestionEditView(LaunchpadEditFormView):
1642
1643 @property
1644 def page_title(self):
1645- return 'Edit question #%s details' % self.context.id
1646+ return "Edit question #%s details" % self.context.id
1647
1648 label = page_title
1649
1650@@ -803,8 +847,9 @@ class QuestionEditView(LaunchpadEditFormView):
1651 """
1652 LaunchpadEditFormView.setUpFields(self)
1653
1654- self.form_fields = self.form_fields.omit("distribution",
1655- "sourcepackagename", "product")
1656+ self.form_fields = self.form_fields.omit(
1657+ "distribution", "sourcepackagename", "product"
1658+ )
1659
1660 editable_fields = []
1661 for field in self.form_fields:
1662@@ -817,12 +862,12 @@ class QuestionEditView(LaunchpadEditFormView):
1663 """Update the Question from the request form data."""
1664 # Target must be the last field processed because it implicitly
1665 # changes the user's permissions.
1666- target_data = {'target': self.context.target}
1667- if 'target' in data:
1668- target_data['target'] = data['target']
1669- del data['target']
1670+ target_data = {"target": self.context.target}
1671+ if "target" in data:
1672+ target_data["target"] = data["target"]
1673+ del data["target"]
1674 self.updateContextFromData(data)
1675- if target_data['target'] != self.context.target:
1676+ if target_data["target"] != self.context.target:
1677 self.updateContextFromData(target_data)
1678
1679 @property
1680@@ -834,26 +879,29 @@ class QuestionEditView(LaunchpadEditFormView):
1681
1682 class QuestionRejectView(LaunchpadFormView):
1683 """View for rejecting a question."""
1684+
1685 schema = IQuestionChangeStatusForm
1686- field_names = ['message']
1687- label = 'Reject question'
1688+ field_names = ["message"]
1689+ label = "Reject question"
1690
1691 @property
1692 def page_title(self):
1693- return 'Reject question #%s' % self.context.id
1694+ return "Reject question #%s" % self.context.id
1695
1696 def validate(self, data):
1697 """Check that required information was provided."""
1698- if 'message' not in data:
1699+ if "message" not in data:
1700 self.setFieldError(
1701- 'message', _('You must provide an explanation message.'))
1702+ "message", _("You must provide an explanation message.")
1703+ )
1704
1705- @action(_('Reject Question'), name="reject")
1706+ @action(_("Reject Question"), name="reject")
1707 def reject_action(self, action, data):
1708 """Reject the Question."""
1709- self.context.reject(self.user, data['message'])
1710+ self.context.reject(self.user, data["message"])
1711 self.request.response.addNotification(
1712- _('You have rejected this question.'))
1713+ _("You have rejected this question.")
1714+ )
1715
1716 def initialize(self):
1717 """See `LaunchpadFormView`.
1718@@ -862,7 +910,8 @@ class QuestionRejectView(LaunchpadFormView):
1719 """
1720 if self.context.status == QuestionStatus.INVALID:
1721 self.request.response.addNotification(
1722- _('The question is already rejected.'))
1723+ _("The question is already rejected.")
1724+ )
1725 self.request.response.redirect(canonical_url(self.context))
1726 return
1727
1728@@ -886,8 +935,9 @@ class LinkFAQMixin:
1729 @property
1730 def default_message(self):
1731 """The default link message to use."""
1732- return '%s suggests this article as an answer to your question:' % (
1733- self.user.displayname)
1734+ return "%s suggests this article as an answer to your question:" % (
1735+ self.user.displayname
1736+ )
1737
1738 def getFAQMessageReference(self, faq):
1739 """Return the reference for the FAQ to use in the linking message."""
1740@@ -898,6 +948,7 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
1741 """View managing the question workflow action, i.e. action changing
1742 its status.
1743 """
1744+
1745 schema = IQuestionAddMessageForm
1746
1747 # Do not autofocus the message widget.
1748@@ -912,18 +963,19 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
1749 return smartquote('%s question #%d: "%s"') % (
1750 self.context.target.displayname,
1751 self.context.id,
1752- self.context.title)
1753+ self.context.title,
1754+ )
1755
1756 def setUpFields(self):
1757 """See `LaunchpadFormView`."""
1758 LaunchpadFormView.setUpFields(self)
1759 if self.context.isSubscribed(self.user):
1760- self.form_fields = self.form_fields.omit('subscribe_me')
1761+ self.form_fields = self.form_fields.omit("subscribe_me")
1762
1763 def setUpWidgets(self):
1764 """See `LaunchpadFormView`."""
1765 LaunchpadFormView.setUpWidgets(self)
1766- alsoProvides(self.widgets['message'], IAlwaysSubmittedWidget)
1767+ alsoProvides(self.widgets["message"], IAlwaysSubmittedWidget)
1768
1769 def validate(self, data):
1770 """Form validatation hook.
1771@@ -935,8 +987,8 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
1772 if self.confirm_action.submitted():
1773 self.validateConfirmAnswer(data)
1774 else:
1775- if not data.get('message'):
1776- self.setFieldError('message', _('Please enter a message.'))
1777+ if not data.get("message"):
1778+ self.setFieldError("message", _("Please enter a message."))
1779
1780 @property
1781 def lang(self):
1782@@ -969,8 +1021,10 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
1783 strip_invisible = not (role.in_admin or role.in_registry_experts)
1784 if strip_invisible:
1785 messages = [
1786- message for message in messages
1787- if message.visible or message.owner == self.user]
1788+ message
1789+ for message in messages
1790+ if message.visible or message.owner == self.user
1791+ ]
1792 return messages
1793
1794 def canAddComment(self, action):
1795@@ -981,79 +1035,92 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
1796 """
1797 return self.user is not None
1798
1799- @action(_('Just Add a Comment'), name='comment', condition=canAddComment)
1800+ @action(_("Just Add a Comment"), name="comment", condition=canAddComment)
1801 def comment_action(self, action, data):
1802 """Add a comment to a resolved question."""
1803- self.context.addComment(self.user, data['message'])
1804+ self.context.addComment(self.user, data["message"])
1805 self._addNotificationAndHandlePossibleSubscription(
1806- _('Thanks for your comment.'), data)
1807+ _("Thanks for your comment."), data
1808+ )
1809
1810 @property
1811 def show_call_to_answer(self):
1812 """Return whether the call to answer should be displayed."""
1813- return (self.user != self.context.owner and
1814- self.context.can_give_answer)
1815+ return self.user != self.context.owner and self.context.can_give_answer
1816
1817 def canAddAnswer(self, action):
1818 """Return whether the answer action should be displayed."""
1819- return (self.user is not None and
1820- self.user != self.context.owner and
1821- self.context.can_give_answer)
1822+ return (
1823+ self.user is not None
1824+ and self.user != self.context.owner
1825+ and self.context.can_give_answer
1826+ )
1827
1828- @action(_('Propose Answer'), name='answer', condition=canAddAnswer)
1829+ @action(_("Propose Answer"), name="answer", condition=canAddAnswer)
1830 def answer_action(self, action, data):
1831 """Add an answer to the question."""
1832- self.context.giveAnswer(self.user, data['message'])
1833+ self.context.giveAnswer(self.user, data["message"])
1834 self._addNotificationAndHandlePossibleSubscription(
1835- _('Thanks for your answer.'), data)
1836+ _("Thanks for your answer."), data
1837+ )
1838
1839 def canSelfAnswer(self, action):
1840 """Return whether the selfanswer action should be displayed."""
1841- return (self.user == self.context.owner and
1842- self.context.can_give_answer)
1843+ return self.user == self.context.owner and self.context.can_give_answer
1844
1845- @action(_('Problem Solved'), name="selfanswer",
1846- condition=canSelfAnswer)
1847+ @action(_("Problem Solved"), name="selfanswer", condition=canSelfAnswer)
1848 def selfanswer_action(self, action, data):
1849 """Action called when the owner provides the solution."""
1850- self.context.giveAnswer(self.user, data['message'])
1851+ self.context.giveAnswer(self.user, data["message"])
1852 # Owners frequently solve their questions, but their messages imply
1853 # that another user provided an answer. When a question has answers
1854 # that can be confirmed, suggest to the owner that they use the
1855 # confirmation button.
1856 if self.context.can_confirm_answer:
1857- msgid = _("Your question is solved. If a particular message "
1858- "helped you solve the problem, use the <em>'This "
1859- "solved my problem'</em> button.")
1860+ msgid = _(
1861+ "Your question is solved. If a particular message "
1862+ "helped you solve the problem, use the <em>'This "
1863+ "solved my problem'</em> button."
1864+ )
1865 self._addNotificationAndHandlePossibleSubscription(
1866- structured(msgid), data)
1867+ structured(msgid), data
1868+ )
1869
1870 def canRequestInfo(self, action):
1871 """Return if the requestinfo action should be displayed."""
1872- return (self.user is not None and
1873- self.user != self.context.owner and
1874- self.context.can_request_info)
1875-
1876- @action(_('Add Information Request'), name='requestinfo',
1877- condition=canRequestInfo)
1878+ return (
1879+ self.user is not None
1880+ and self.user != self.context.owner
1881+ and self.context.can_request_info
1882+ )
1883+
1884+ @action(
1885+ _("Add Information Request"),
1886+ name="requestinfo",
1887+ condition=canRequestInfo,
1888+ )
1889 def requestinfo_action(self, action, data):
1890 """Add a request for more information to the question."""
1891- self.context.requestInfo(self.user, data['message'])
1892+ self.context.requestInfo(self.user, data["message"])
1893 self._addNotificationAndHandlePossibleSubscription(
1894- _('Thanks for your information request.'), data)
1895+ _("Thanks for your information request."), data
1896+ )
1897
1898 def canGiveInfo(self, action):
1899 """Return whether the giveinfo action should be displayed."""
1900- return (self.user == self.context.owner and
1901- self.context.can_give_info)
1902+ return self.user == self.context.owner and self.context.can_give_info
1903
1904- @action(_("I'm Providing More Information"), name='giveinfo',
1905- condition=canGiveInfo)
1906+ @action(
1907+ _("I'm Providing More Information"),
1908+ name="giveinfo",
1909+ condition=canGiveInfo,
1910+ )
1911 def giveinfo_action(self, action, data):
1912 """Give additional informatin on the request."""
1913- self.context.giveInfo(data['message'])
1914+ self.context.giveInfo(data["message"])
1915 self._addNotificationAndHandlePossibleSubscription(
1916- _('Thanks for adding more information to your question.'), data)
1917+ _("Thanks for adding more information to your question."), data
1918+ )
1919
1920 def validateConfirmAnswer(self, data):
1921 """Make sure that a valid message id was provided as the confirmed
1922@@ -1061,47 +1128,48 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
1923 # No widget is used for the answer, we are using hidden fields
1924 # in the template for that. So, if the answer is missing, it's
1925 # either a programming error or an invalid handcrafted URL
1926- msgid = self.request.form.get('answer_id')
1927+ msgid = self.request.form.get("answer_id")
1928 if msgid is None:
1929- raise UnexpectedFormData('missing answer_id')
1930+ raise UnexpectedFormData("missing answer_id")
1931 try:
1932- data['answer'] = self.context.messages[int(msgid)]
1933+ data["answer"] = self.context.messages[int(msgid)]
1934 except ValueError:
1935- raise UnexpectedFormData('invalid answer_id: %s' % msgid)
1936+ raise UnexpectedFormData("invalid answer_id: %s" % msgid)
1937 except IndexError:
1938 raise UnexpectedFormData("unknown answer: %s" % msgid)
1939
1940 def canConfirm(self, action):
1941 """Return whether the confirm action should be displayed."""
1942- return (self.user == self.context.owner and
1943- self.context.can_confirm_answer)
1944+ return (
1945+ self.user == self.context.owner and self.context.can_confirm_answer
1946+ )
1947
1948- @action(_("This Solved My Problem"), name='confirm',
1949- condition=canConfirm)
1950+ @action(_("This Solved My Problem"), name="confirm", condition=canConfirm)
1951 def confirm_action(self, action, data):
1952 """Confirm that an answer solved the request."""
1953 # The confirmation message is not given by the user when the
1954 # 'This Solved My Problem' button on the main question view.
1955- if not data['message']:
1956- data['message'] = 'Thanks %s, that solved my question.' % (
1957- data['answer'].owner.displayname)
1958- self.context.confirmAnswer(data['message'], answer=data['answer'])
1959+ if not data["message"]:
1960+ data["message"] = "Thanks %s, that solved my question." % (
1961+ data["answer"].owner.displayname
1962+ )
1963+ self.context.confirmAnswer(data["message"], answer=data["answer"])
1964 self._addNotificationAndHandlePossibleSubscription(
1965- _('Thanks for your feedback.'), data)
1966+ _("Thanks for your feedback."), data
1967+ )
1968
1969 def canReopen(self, action):
1970 """Return whether the reopen action should be displayed."""
1971- return (self.user == self.context.owner and
1972- self.context.can_reopen)
1973+ return self.user == self.context.owner and self.context.can_reopen
1974
1975- @action(_("I Still Need an Answer"), name='reopen',
1976- condition=canReopen)
1977+ @action(_("I Still Need an Answer"), name="reopen", condition=canReopen)
1978 def reopen_action(self, action, data):
1979 """State that the problem is still occuring and provide new
1980 information about it."""
1981- self.context.reopen(data['message'])
1982+ self.context.reopen(data["message"])
1983 self._addNotificationAndHandlePossibleSubscription(
1984- _('Your question was reopened.'), data)
1985+ _("Your question was reopened."), data
1986+ )
1987
1988 def _addNotificationAndHandlePossibleSubscription(self, message, data):
1989 """Post-processing work common to all workflow actions.
1990@@ -1111,26 +1179,30 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
1991 """
1992 self.request.response.addNotification(message)
1993
1994- if data.get('subscribe_me'):
1995+ if data.get("subscribe_me"):
1996 self.context.subscribe(self.user)
1997 self.request.response.addNotification(
1998- _("You have subscribed to this question."))
1999+ _("You have subscribed to this question.")
2000+ )
2001
2002 self.next_url = canonical_url(self.context)
2003
2004 @property
2005 def new_question_url(self):
2006 """Return a URL to add a new question for the QuestionTarget."""
2007- return '%s/+addquestion' % canonical_url(self.context.target,
2008- rootsite='answers')
2009+ return "%s/+addquestion" % canonical_url(
2010+ self.context.target, rootsite="answers"
2011+ )
2012
2013 @property
2014 def original_bug(self):
2015 """Return the bug that the question was created from or None."""
2016 for bug in self.context.bugs:
2017- if (check_permission('launchpad.View', bug)
2018+ if (
2019+ check_permission("launchpad.View", bug)
2020 and bug.owner == self.context.owner
2021- and bug.datecreated == self.context.datecreated):
2022+ and bug.datecreated == self.context.datecreated
2023+ ):
2024 return bug
2025
2026 return None
2027@@ -1145,9 +1217,12 @@ class QuestionConfirmAnswerView(QuestionWorkflowView):
2028 """Initialize the view from the Question state."""
2029 # This page is only accessible when a confirmation is possible.
2030 if not self.context.can_confirm_answer:
2031- self.request.response.addErrorNotification(_(
2032- "The question is not in a state where you can confirm "
2033- "an answer."))
2034+ self.request.response.addErrorNotification(
2035+ _(
2036+ "The question is not in a state where you can confirm "
2037+ "an answer."
2038+ )
2039+ )
2040 self.request.response.redirect(canonical_url(self.context))
2041 return
2042
2043@@ -1157,7 +1232,7 @@ class QuestionConfirmAnswerView(QuestionWorkflowView):
2044 """Return the message that should be confirmed."""
2045 data = {}
2046 self.validateConfirmAnswer(data)
2047- return data['answer']
2048+ return data["answer"]
2049
2050
2051 class QuestionMessageDisplayView(LaunchpadView):
2052@@ -1172,14 +1247,17 @@ class QuestionMessageDisplayView(LaunchpadView):
2053 @cachedproperty
2054 def isBestAnswer(self):
2055 """Return True when this message is marked as solving the question."""
2056- return (self.context == self.question.answer
2057- and self.context.action in [
2058- QuestionAction.ANSWER, QuestionAction.CONFIRM])
2059+ return (
2060+ self.context == self.question.answer
2061+ and self.context.action
2062+ in [QuestionAction.ANSWER, QuestionAction.CONFIRM]
2063+ )
2064
2065 def renderAnswerIdFormElement(self):
2066 """Return the hidden form element to refer to that message."""
2067 return '<input type="hidden" name="answer_id" value="%d" />' % list(
2068- self.context.question.messages).index(self.context)
2069+ self.context.question.messages
2070+ ).index(self.context)
2071
2072 def getBodyCSSClass(self):
2073 """Return the CSS class to use for this message's body."""
2074@@ -1190,7 +1268,7 @@ class QuestionMessageDisplayView(LaunchpadView):
2075
2076 @cachedproperty
2077 def canSeeSpamControls(self):
2078- return check_permission('launchpad.Moderate', self.context)
2079+ return check_permission("launchpad.Moderate", self.context)
2080
2081 def getBoardCommentCSSClass(self):
2082 css_classes = ["boardComment", "editable-message"]
2083@@ -1202,14 +1280,16 @@ class QuestionMessageDisplayView(LaunchpadView):
2084
2085 @property
2086 def can_edit(self):
2087- return check_permission('launchpad.Edit', self.context)
2088+ return check_permission("launchpad.Edit", self.context)
2089
2090 def canConfirmAnswer(self):
2091 """Return True if the user can confirm this answer."""
2092- return (self.display_confirm_button and
2093- self.user == self.question.owner and
2094- self.question.can_confirm_answer and
2095- self.context.action == QuestionAction.ANSWER)
2096+ return (
2097+ self.display_confirm_button
2098+ and self.user == self.question.owner
2099+ and self.question.can_confirm_answer
2100+ and self.context.action == QuestionAction.ANSWER
2101+ )
2102
2103 def renderWithoutConfirmButton(self):
2104 """Display the message without any confirm button."""
2105@@ -1222,29 +1302,33 @@ class SearchAllQuestionsView(SearchQuestionsView):
2106
2107 display_target_column = True
2108 # Match contiguous digits, optionally prefixed with a '#'.
2109- id_pattern = re.compile(r'^#?(\d+)$')
2110+ id_pattern = re.compile(r"^#?(\d+)$")
2111
2112 @property
2113 def pageheading(self):
2114 """See `SearchQuestionsView`."""
2115 if self.search_text:
2116- return _('Questions matching "${search_text}"',
2117- mapping=dict(search_text=self.search_text))
2118+ return _(
2119+ 'Questions matching "${search_text}"',
2120+ mapping=dict(search_text=self.search_text),
2121+ )
2122 else:
2123- return _('Search all questions')
2124+ return _("Search all questions")
2125
2126 @property
2127 def empty_listing_message(self):
2128 """See `SearchQuestionsView`."""
2129 if self.search_text:
2130- return _("There are no questions matching "
2131- '"${search_text}" with the requested statuses.',
2132- mapping=dict(search_text=self.search_text))
2133+ return _(
2134+ "There are no questions matching "
2135+ '"${search_text}" with the requested statuses.',
2136+ mapping=dict(search_text=self.search_text),
2137+ )
2138 else:
2139- return _('There are no questions with the requested statuses.')
2140+ return _("There are no questions with the requested statuses.")
2141
2142 @safe_action
2143- @action(_('Search'), name='search')
2144+ @action(_("Search"), name="search")
2145 def search_action(self, action, data):
2146 """Action executed when the user clicked the 'Find Answers' button.
2147
2148@@ -1266,13 +1350,13 @@ class QuestionCreateFAQView(LinkFAQMixin, LaunchpadFormView):
2149 """View to create a new FAQ."""
2150
2151 schema = IFAQ
2152- label = _('Create a new FAQ')
2153+ label = _("Create a new FAQ")
2154
2155 @property
2156 def page_title(self):
2157- return 'Create a FAQ for %s' % self.context.product.displayname
2158+ return "Create a FAQ for %s" % self.context.product.displayname
2159
2160- field_names = ['title', 'keywords', 'content']
2161+ field_names = ["title", "keywords", "content"]
2162
2163 custom_widget_keywords = TokensTextWidget
2164 custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
2165@@ -1282,10 +1366,10 @@ class QuestionCreateFAQView(LinkFAQMixin, LaunchpadFormView):
2166 """Fill title and content based on the question."""
2167 question = self.context
2168 return {
2169- 'title': question.title,
2170- 'content': question.description,
2171- 'message': self.default_message,
2172- }
2173+ "title": question.title,
2174+ "content": question.description,
2175+ "message": self.default_message,
2176+ }
2177
2178 def setUpFields(self):
2179 """See `LaunchpadFormView`.
2180@@ -1294,23 +1378,28 @@ class QuestionCreateFAQView(LinkFAQMixin, LaunchpadFormView):
2181 """
2182 super().setUpFields()
2183 self.form_fields += form.Fields(
2184- copy_field(IQuestionLinkFAQForm['message']))
2185- self.form_fields['message'].field.title = _(
2186- 'Additional comment for question #%s' % self.context.id)
2187- self.form_fields['message'].custom_widget = self.custom_widget_message
2188-
2189- @action(_('Create and Link'), name='create_and_link')
2190+ copy_field(IQuestionLinkFAQForm["message"])
2191+ )
2192+ self.form_fields["message"].field.title = _(
2193+ "Additional comment for question #%s" % self.context.id
2194+ )
2195+ self.form_fields["message"].custom_widget = self.custom_widget_message
2196+
2197+ @action(_("Create and Link"), name="create_and_link")
2198 def create_and_link_action(self, action, data):
2199 """Creates the FAQ and link it to the question."""
2200
2201 faq = self.faq_target.newFAQ(
2202- self.user, data['title'], data['content'],
2203- keywords=data['keywords'])
2204+ self.user,
2205+ data["title"],
2206+ data["content"],
2207+ keywords=data["keywords"],
2208+ )
2209
2210 # Append FAQ link to message.
2211- data['message'] += '\n' + self.getFAQMessageReference(faq)
2212+ data["message"] += "\n" + self.getFAQMessageReference(faq)
2213
2214- self.context.linkFAQ(self.user, faq, data['message'])
2215+ self.context.linkFAQ(self.user, faq, data["message"])
2216
2217 # Redirect to the question.
2218 self.next_url = canonical_url(self.context)
2219@@ -1323,21 +1412,21 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
2220 select an element from this set using the radio buttons.
2221 """
2222
2223- _messageNoValue = _('No existing FAQs are relevant')
2224+ _messageNoValue = _("No existing FAQs are relevant")
2225
2226 searchDisplayWidth = 30
2227
2228- searchButtonLabel = _('Search')
2229+ searchButtonLabel = _("Search")
2230
2231 @property
2232 def search_field_name(self):
2233 """Return the name to use for the search field."""
2234- return self.name + '-query'
2235+ return self.name + "-query"
2236
2237 @property
2238 def search_button_name(self):
2239 """Return the name to use for the search button."""
2240- return self.name + '-search'
2241+ return self.name + "-search"
2242
2243 def renderValue(self, value):
2244 """Render the widget with the value."""
2245@@ -1383,11 +1472,13 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
2246 else:
2247 render = self.renderItem
2248
2249- missing_item = render(count,
2250+ missing_item = render(
2251+ count,
2252 self.translate(self._messageNoValue),
2253 missing,
2254 self.name,
2255- self.cssClass)
2256+ self.cssClass,
2257+ )
2258 rendered_items.insert(0, missing_item)
2259 count += 1
2260
2261@@ -1396,7 +1487,8 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
2262 def getSearchQuery(self):
2263 """Return the search query."""
2264 return self.request.form_ng.getOne(
2265- self.search_field_name, self.default_query)
2266+ self.search_field_name, self.default_query
2267+ )
2268
2269 def renderTerm(self, index, term, selected):
2270 """Render a term as a radio button.
2271@@ -1404,47 +1496,51 @@ class SearchableFAQRadioWidget(LaunchpadRadioWidget):
2272 The term's token is used as the radio button label. A link to the
2273 term's value is added beside the button.
2274 """
2275- id = '%s.%s' % (self.name, index)
2276+ id = "%s.%s" % (self.name, index)
2277 attributes = dict(
2278 value=term.token,
2279 id=id,
2280 name=self.name,
2281 cssClass=self.cssClass,
2282- type='radio')
2283+ type="radio",
2284+ )
2285 if selected:
2286- attributes['checked'] = 'checked'
2287- input = renderElement('input', **attributes)
2288+ attributes["checked"] = "checked"
2289+ input = renderElement("input", **attributes)
2290 button = structured(
2291 '<label style="font-weight: normal">%s&nbsp;%s:</label>',
2292- structured(input), term.token)
2293+ structured(input),
2294+ term.token,
2295+ )
2296 link = structured(
2297- '<a href="%s">%s</a>', canonical_url(term.value), term.title)
2298+ '<a href="%s">%s</a>', canonical_url(term.value), term.title
2299+ )
2300
2301 return "\n".join([button.escapedtext, link.escapedtext])
2302
2303 def renderSearchWidget(self):
2304 """Render the search entry field and the button."""
2305- return " ".join([
2306- self.renderSearchField(),
2307- self.renderSearchButton()])
2308+ return " ".join([self.renderSearchField(), self.renderSearchButton()])
2309
2310 def renderSearchField(self):
2311 """Render the search field."""
2312 return renderElement(
2313- 'input',
2314+ "input",
2315 type="text",
2316 cssClass=self.cssClass,
2317 value=self.getSearchQuery(),
2318 name=self.search_field_name,
2319- size=self.searchDisplayWidth)
2320+ size=self.searchDisplayWidth,
2321+ )
2322
2323 def renderSearchButton(self):
2324 """Render the search button."""
2325 return renderElement(
2326- 'input',
2327- type='submit',
2328+ "input",
2329+ type="submit",
2330 name=self.search_button_name,
2331- value=self.searchButtonLabel)
2332+ value=self.searchButtonLabel,
2333+ )
2334
2335
2336 class QuestionLinkFAQView(LinkFAQMixin, LaunchpadFormView):
2337@@ -1456,36 +1552,36 @@ class QuestionLinkFAQView(LinkFAQMixin, LaunchpadFormView):
2338
2339 custom_widget_message = CustomWidgetFactory(TextAreaWidget, height=5)
2340
2341- label = _('Is this a FAQ?')
2342+ label = _("Is this a FAQ?")
2343
2344 @property
2345 def page_title(self):
2346- return _('Is question #%s a FAQ?' % self.context.id)
2347+ return _("Is question #%s a FAQ?" % self.context.id)
2348
2349 @property
2350 def initial_values(self):
2351 """Sets initial form values."""
2352 return {
2353- 'faq': self.context.faq,
2354- 'message': self.default_message,
2355- }
2356+ "faq": self.context.faq,
2357+ "message": self.default_message,
2358+ }
2359
2360 def setUpWidgets(self):
2361 """Set the query on the search widget to the question title."""
2362 super().setUpWidgets()
2363- self.widgets['faq'].default_query = self.context.title
2364+ self.widgets["faq"].default_query = self.context.title
2365
2366 def validate(self, data):
2367 """Make sure that the FAQ link was changed."""
2368- if self.context.faq == data.get('faq'):
2369- self.setFieldError('faq', _("You didn't modify the linked FAQ."))
2370+ if self.context.faq == data.get("faq"):
2371+ self.setFieldError("faq", _("You didn't modify the linked FAQ."))
2372
2373- @action(_('Link to FAQ'), name="link")
2374+ @action(_("Link to FAQ"), name="link")
2375 def link_action(self, action, data):
2376 """Link the selected FAQ to the question."""
2377- if data['faq'] is not None:
2378- data['message'] += '\n' + self.getFAQMessageReference(data['faq'])
2379- self.context.linkFAQ(self.user, data['faq'], data['message'])
2380+ if data["faq"] is not None:
2381+ data["message"] += "\n" + self.getFAQMessageReference(data["faq"])
2382+ self.context.linkFAQ(self.user, data["faq"], data["message"])
2383
2384 @property
2385 def next_url(self):
2386diff --git a/lib/lp/answers/browser/questionsubscription.py b/lib/lp/answers/browser/questionsubscription.py
2387index 8883f85..cc2ea58 100644
2388--- a/lib/lp/answers/browser/questionsubscription.py
2389+++ b/lib/lp/answers/browser/questionsubscription.py
2390@@ -4,8 +4,8 @@
2391 """Views for QuestionSubscription."""
2392
2393 __all__ = [
2394- 'QuestionPortletSubscribersWithDetails',
2395- ]
2396+ "QuestionPortletSubscribersWithDetails",
2397+]
2398
2399 from lazr.delegates import delegate_to
2400 from lazr.restful.interfaces import IWebServiceClientRequest
2401@@ -15,10 +15,7 @@ from zope.traversing.browser import absoluteURL
2402 from lp.answers.interfaces.question import IQuestion
2403 from lp.answers.interfaces.questionsubscription import IQuestionSubscription
2404 from lp.services.propertycache import cachedproperty
2405-from lp.services.webapp import (
2406- canonical_url,
2407- LaunchpadView,
2408- )
2409+from lp.services.webapp import LaunchpadView, canonical_url
2410
2411
2412 class QuestionPortletSubscribersWithDetails(LaunchpadView):
2413@@ -43,17 +40,17 @@ class QuestionPortletSubscribersWithDetails(LaunchpadView):
2414 continue
2415
2416 subscriber = {
2417- 'name': person.name,
2418- 'display_name': person.displayname,
2419- 'web_link': canonical_url(person, rootsite='mainsite'),
2420- 'self_link': absoluteURL(person, self.api_request),
2421- 'is_team': person.is_team,
2422- 'can_edit': can_edit
2423- }
2424+ "name": person.name,
2425+ "display_name": person.displayname,
2426+ "web_link": canonical_url(person, rootsite="mainsite"),
2427+ "self_link": absoluteURL(person, self.api_request),
2428+ "is_team": person.is_team,
2429+ "can_edit": can_edit,
2430+ }
2431 record = {
2432- 'subscriber': subscriber,
2433- 'subscription_level': 'Direct',
2434- }
2435+ "subscriber": subscriber,
2436+ "subscription_level": "Direct",
2437+ }
2438 data.append(record)
2439 return data
2440
2441@@ -69,27 +66,27 @@ class QuestionPortletSubscribersWithDetails(LaunchpadView):
2442 # Skip the current user viewing the page.
2443 continue
2444 subscriber = {
2445- 'name': person.name,
2446- 'display_name': person.displayname,
2447- 'web_link': canonical_url(person, rootsite='mainsite'),
2448- 'self_link': absoluteURL(person, self.api_request),
2449- 'is_team': person.is_team,
2450- 'can_edit': False,
2451- }
2452+ "name": person.name,
2453+ "display_name": person.displayname,
2454+ "web_link": canonical_url(person, rootsite="mainsite"),
2455+ "self_link": absoluteURL(person, self.api_request),
2456+ "is_team": person.is_team,
2457+ "can_edit": False,
2458+ }
2459 record = {
2460- 'subscriber': subscriber,
2461- 'subscription_level': 'Indirect',
2462- }
2463+ "subscriber": subscriber,
2464+ "subscription_level": "Indirect",
2465+ }
2466 data.append(record)
2467 return dumps(data)
2468
2469 def render(self):
2470 """Override the default render() to return only JSON."""
2471- self.request.response.setHeader('content-type', 'application/json')
2472+ self.request.response.setHeader("content-type", "application/json")
2473 return self.subscriber_data_js
2474
2475
2476-@delegate_to(IQuestionSubscription, context='subscription')
2477+@delegate_to(IQuestionSubscription, context="subscription")
2478 class SubscriptionAttrDecorator:
2479 """A QuestionSubscription with added attributes for HTML/JS."""
2480
2481@@ -98,4 +95,4 @@ class SubscriptionAttrDecorator:
2482
2483 @property
2484 def css_name(self):
2485- return 'subscriber-%s' % self.subscription.person.id
2486+ return "subscriber-%s" % self.subscription.person.id
2487diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py
2488index 5eb0c08..488c860 100644
2489--- a/lib/lp/answers/browser/questiontarget.py
2490+++ b/lib/lp/answers/browser/questiontarget.py
2491@@ -4,46 +4,32 @@
2492 """IQuestionTarget browser views."""
2493
2494 __all__ = [
2495- 'AskAQuestionButtonPortlet',
2496- 'ManageAnswerContactView',
2497- 'SearchQuestionsView',
2498- 'QuestionCollectionByLanguageView',
2499- 'QuestionCollectionLatestQuestionsPortlet',
2500- 'QuestionCollectionMyQuestionsView',
2501- 'QuestionCollectionNeedAttentionView',
2502- 'QuestionCollectionAnswersMenu',
2503- 'QuestionTargetPortletAnswerContactsWithDetails',
2504- 'QuestionTargetTraversalMixin',
2505- 'QuestionTargetAnswersMenu',
2506- 'UserSupportLanguagesMixin',
2507- ]
2508+ "AskAQuestionButtonPortlet",
2509+ "ManageAnswerContactView",
2510+ "SearchQuestionsView",
2511+ "QuestionCollectionByLanguageView",
2512+ "QuestionCollectionLatestQuestionsPortlet",
2513+ "QuestionCollectionMyQuestionsView",
2514+ "QuestionCollectionNeedAttentionView",
2515+ "QuestionCollectionAnswersMenu",
2516+ "QuestionTargetPortletAnswerContactsWithDetails",
2517+ "QuestionTargetTraversalMixin",
2518+ "QuestionTargetAnswersMenu",
2519+ "UserSupportLanguagesMixin",
2520+]
2521
2522 from operator import attrgetter
2523 from urllib.parse import urlencode
2524
2525-from lazr.restful.interfaces import (
2526- IJSONRequestCache,
2527- IWebServiceClientRequest,
2528- )
2529+from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
2530 from simplejson import dumps
2531 from zope.browserpage import ViewPageTemplateFile
2532-from zope.component import (
2533- getMultiAdapter,
2534- getUtility,
2535- queryMultiAdapter,
2536- )
2537+from zope.component import getMultiAdapter, getUtility, queryMultiAdapter
2538 from zope.formlib import form
2539 from zope.formlib.widget import CustomWidgetFactory
2540 from zope.formlib.widgets import DropdownWidget
2541-from zope.schema import (
2542- Bool,
2543- Choice,
2544- List,
2545- )
2546-from zope.schema.vocabulary import (
2547- SimpleTerm,
2548- SimpleVocabulary,
2549- )
2550+from zope.schema import Bool, Choice, List
2551+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
2552 from zope.traversing.browser import absoluteURL
2553
2554 from lp import _
2555@@ -54,16 +40,12 @@ from lp.answers.interfaces.questioncollection import (
2556 IQuestionCollection,
2557 IQuestionSet,
2558 ISearchableByQuestionOwner,
2559- )
2560+)
2561 from lp.answers.interfaces.questiontarget import (
2562 IQuestionTarget,
2563 ISearchQuestionsForm,
2564- )
2565-from lp.app.browser.launchpadform import (
2566- action,
2567- LaunchpadFormView,
2568- safe_action,
2569- )
2570+)
2571+from lp.app.browser.launchpadform import LaunchpadFormView, action, safe_action
2572 from lp.app.enums import service_uses_launchpad
2573 from lp.app.errors import NotFoundError
2574 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
2575@@ -74,12 +56,12 @@ from lp.registry.interfaces.projectgroup import IProjectGroup
2576 from lp.services.fields import PublicPersonChoice
2577 from lp.services.propertycache import cachedproperty
2578 from lp.services.webapp import (
2579- canonical_url,
2580 Link,
2581+ canonical_url,
2582 stepthrough,
2583 stepto,
2584 urlappend,
2585- )
2586+)
2587 from lp.services.webapp.authorization import check_permission
2588 from lp.services.webapp.batching import BatchNavigator
2589 from lp.services.webapp.escaping import structured
2590@@ -88,7 +70,7 @@ from lp.services.worlddata.helpers import (
2591 browser_languages,
2592 is_english_variant,
2593 preferred_or_request_languages,
2594- )
2595+)
2596 from lp.services.worlddata.interfaces.language import ILanguageSet
2597
2598
2599@@ -98,7 +80,8 @@ class AskAQuestionButtonPortlet:
2600 def __call__(self):
2601 # Check if the context has an +addquestion view available...
2602 if queryMultiAdapter(
2603- (self.context, self.request), name='+addquestion'):
2604+ (self.context, self.request), name="+addquestion"
2605+ ):
2606 target = self.context
2607 else:
2608 # otherwise find an adapter to IQuestionTarget which will.
2609@@ -114,7 +97,8 @@ class AskAQuestionButtonPortlet:
2610 </ul>
2611 </div>
2612 """ % canonical_url(
2613- target, view_name='+addquestion', rootsite='answers')
2614+ target, view_name="+addquestion", rootsite="answers"
2615+ )
2616
2617
2618 class UserSupportLanguagesMixin:
2619@@ -153,7 +137,7 @@ class QuestionCollectionLatestQuestionsPortlet:
2620
2621 @property
2622 def page_title(self):
2623- return 'Latest questions for %s' % (self.context.displayname)
2624+ return "Latest questions for %s" % (self.context.displayname)
2625
2626 @cachedproperty
2627 def getLatestQuestions(self, quantity=5):
2628@@ -173,15 +157,17 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2629 schema = ISearchQuestionsForm
2630
2631 custom_widget_language = CustomWidgetFactory(
2632- LabeledMultiCheckBoxWidget, orientation='horizontal')
2633+ LabeledMultiCheckBoxWidget, orientation="horizontal"
2634+ )
2635 custom_widget_sort = CustomWidgetFactory(
2636- DropdownWidget, cssClass='inlined-widget')
2637+ DropdownWidget, cssClass="inlined-widget"
2638+ )
2639 custom_widget_status = CustomWidgetFactory(
2640- LabeledMultiCheckBoxWidget, orientation='horizontal')
2641+ LabeledMultiCheckBoxWidget, orientation="horizontal"
2642+ )
2643
2644- default_template = ViewPageTemplateFile(
2645- '../templates/question-listing.pt')
2646- unknown_template = ViewPageTemplateFile('../templates/unknown-support.pt')
2647+ default_template = ViewPageTemplateFile("../templates/question-listing.pt")
2648+ unknown_template = ViewPageTemplateFile("../templates/unknown-support.pt")
2649
2650 @property
2651 def template(self):
2652@@ -192,7 +178,8 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2653 if IQuestionSet.providedBy(self.context):
2654 return self.default_template
2655 involvement = getMultiAdapter(
2656- (self.context, self.request), name='+get-involved')
2657+ (self.context, self.request), name="+get-involved"
2658+ )
2659 if service_uses_launchpad(involvement.answers_usage):
2660 # Primary contexts that officially use answers have a
2661 # search and listing presentation.
2662@@ -213,29 +200,36 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2663 if IQuestionSet.providedBy(self.context):
2664 return _(
2665 'Questions matching "${search_text}"',
2666- mapping=dict(search_text=self.search_text))
2667+ mapping=dict(search_text=self.search_text),
2668+ )
2669
2670 replacements = dict(
2671- context=self.context.displayname,
2672- search_text=self.search_text)
2673+ context=self.context.displayname, search_text=self.search_text
2674+ )
2675 # Check if the set of selected status has a special title.
2676 status_set_title = self.status_title_map.get(
2677- frozenset(self.status_filter))
2678+ frozenset(self.status_filter)
2679+ )
2680 if status_set_title:
2681- replacements['status'] = status_set_title
2682+ replacements["status"] = status_set_title
2683 if self.search_text:
2684- return _('${status} questions matching "${search_text}" '
2685- 'for ${context}', mapping=replacements)
2686+ return _(
2687+ '${status} questions matching "${search_text}" '
2688+ "for ${context}",
2689+ mapping=replacements,
2690+ )
2691 else:
2692- return _('${status} questions for ${context}',
2693- mapping=replacements)
2694+ return _(
2695+ "${status} questions for ${context}", mapping=replacements
2696+ )
2697 else:
2698 if self.search_text:
2699- return _('Questions matching "${search_text}" for '
2700- '${context}', mapping=replacements)
2701+ return _(
2702+ 'Questions matching "${search_text}" for ' "${context}",
2703+ mapping=replacements,
2704+ )
2705 else:
2706- return _('Questions for ${context}',
2707- mapping=replacements)
2708+ return _("Questions for ${context}", mapping=replacements)
2709
2710 label = page_title
2711
2712@@ -273,26 +267,31 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2713 languages = set(self.user_support_languages)
2714 languages.intersection_update(self.context_question_languages)
2715 terms = []
2716- for lang in sorted(languages, key=attrgetter('code')):
2717+ for lang in sorted(languages, key=attrgetter("code")):
2718 terms.append(SimpleTerm(lang, lang.code, lang.displayname))
2719 return form.Fields(
2720- List(__name__='language',
2721- title=_('Languages filter'),
2722- value_type=Choice(vocabulary=SimpleVocabulary(terms)),
2723- required=False,
2724- default=self.user_support_languages,
2725- description=_(
2726- 'The languages to filter the search results by.')),
2727- render_context=self.render_context)
2728+ List(
2729+ __name__="language",
2730+ title=_("Languages filter"),
2731+ value_type=Choice(vocabulary=SimpleVocabulary(terms)),
2732+ required=False,
2733+ default=self.user_support_languages,
2734+ description=_(
2735+ "The languages to filter the search results by."
2736+ ),
2737+ ),
2738+ render_context=self.render_context,
2739+ )
2740
2741 def validate(self, data):
2742 """Validate hook.
2743
2744 This validation method checks that a valid status is submitted.
2745 """
2746- if not data.get('status', []):
2747+ if not data.get("status", []):
2748 self.setFieldError(
2749- 'status', _('You must choose at least one status.'))
2750+ "status", _("You must choose at least one status.")
2751+ )
2752
2753 @cachedproperty
2754 def status_title_map(self):
2755@@ -306,8 +305,9 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2756 for status in QuestionStatus.items:
2757 mapping[frozenset([status])] = status.title
2758
2759- mapping[frozenset(
2760- [QuestionStatus.ANSWERED, QuestionStatus.SOLVED])] = _('Answered')
2761+ mapping[
2762+ frozenset([QuestionStatus.ANSWERED, QuestionStatus.SOLVED])
2763+ ] = _("Answered")
2764
2765 return mapping
2766
2767@@ -331,48 +331,63 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2768 provide their own implementation.
2769 """
2770 if not IQuestionTarget.providedBy(self.context):
2771- return ''
2772+ return ""
2773 language_counts = {}
2774 questions = self.context.searchQuestions(
2775- unsupported=self.context, status=[QuestionStatus.OPEN])
2776+ unsupported=self.context, status=[QuestionStatus.OPEN]
2777+ )
2778 for question in questions:
2779 lang = question.language
2780 language_counts[lang] = language_counts.get(lang, 0) + 1
2781 if len(language_counts) == 0:
2782- return ''
2783- url = canonical_url(self.context, rootsite='answers')
2784- format = ('%s in <a href="' + url + '/+by-language'
2785- '?field.language=%s&field.status=Open">%s</a>')
2786- links = [format % (language_counts[key], key.code, key.englishname)
2787- for key in language_counts]
2788- return ', '.join(links)
2789+ return ""
2790+ url = canonical_url(self.context, rootsite="answers")
2791+ format = (
2792+ '%s in <a href="' + url + "/+by-language"
2793+ '?field.language=%s&field.status=Open">%s</a>'
2794+ )
2795+ links = [
2796+ format % (language_counts[key], key.code, key.englishname)
2797+ for key in language_counts
2798+ ]
2799+ return ", ".join(links)
2800
2801 @property
2802 def empty_listing_message(self):
2803 """Message shown when there is no questions matching the filter."""
2804 replacements = dict(
2805- context=self.context.displayname,
2806- search_text=self.search_text)
2807+ context=self.context.displayname, search_text=self.search_text
2808+ )
2809 # Check if the set of selected status has a special title.
2810 status_set_title = self.status_title_map.get(
2811- frozenset(self.status_filter))
2812+ frozenset(self.status_filter)
2813+ )
2814 if status_set_title:
2815- replacements['status'] = status_set_title.lower()
2816+ replacements["status"] = status_set_title.lower()
2817 if self.search_text:
2818- return _('There are no ${status} questions matching '
2819- '"${search_text}" for ${context}.',
2820- mapping=replacements)
2821+ return _(
2822+ "There are no ${status} questions matching "
2823+ '"${search_text}" for ${context}.',
2824+ mapping=replacements,
2825+ )
2826 else:
2827- return _('There are no ${status} questions for '
2828- '${context}.', mapping=replacements)
2829+ return _(
2830+ "There are no ${status} questions for " "${context}.",
2831+ mapping=replacements,
2832+ )
2833 else:
2834 if self.search_text:
2835- return _('There are no questions matching "${search_text}" '
2836- 'for ${context} with the requested statuses.',
2837- mapping=replacements)
2838+ return _(
2839+ 'There are no questions matching "${search_text}" '
2840+ "for ${context} with the requested statuses.",
2841+ mapping=replacements,
2842+ )
2843 else:
2844- return _('There are no questions for ${context} with '
2845- 'the requested statuses.', mapping=replacements)
2846+ return _(
2847+ "There are no questions for ${context} with "
2848+ "the requested statuses.",
2849+ mapping=replacements,
2850+ )
2851
2852 def getDefaultFilter(self):
2853 """Hook for subclass to provide a default search filter."""
2854@@ -382,17 +397,17 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2855 def search_text(self):
2856 """Search text used by the filter."""
2857 if self.search_params:
2858- return self.search_params.get('search_text')
2859+ return self.search_params.get("search_text")
2860 else:
2861- return self.getDefaultFilter().get('search_text')
2862+ return self.getDefaultFilter().get("search_text")
2863
2864 @property
2865 def status_filter(self):
2866 """Set of statuses to filter the search with."""
2867 if self.search_params:
2868- return set(self.search_params.get('status', []))
2869+ return set(self.search_params.get("status", []))
2870 else:
2871- return set(self.getDefaultFilter().get('status', []))
2872+ return set(self.getDefaultFilter().get("status", []))
2873
2874 @cachedproperty
2875 def context_question_languages(self):
2876@@ -408,13 +423,14 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2877 and that language is among the user's languages, we do not render
2878 the language control because there are no choices to be made.
2879 """
2880- if not check_permission('launchpad.View', self.context):
2881+ if not check_permission("launchpad.View", self.context):
2882 return False
2883 languages = list(self.context_question_languages)
2884 if len(languages) == 0:
2885 return False
2886- elif (len(languages) == 1
2887- and languages[0] in self.user_support_languages):
2888+ elif (
2889+ len(languages) == 1 and languages[0] in self.user_support_languages
2890+ ):
2891 return False
2892 else:
2893 return True
2894@@ -434,16 +450,23 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2895 @property
2896 def matching_faqs_url(self):
2897 """Return the URL to use to display the list of matching FAQs."""
2898- assert self.matching_faqs_count > 0, (
2899- "can't call matching_faqs_url when matching_faqs_count == 0")
2900+ assert (
2901+ self.matching_faqs_count > 0
2902+ ), "can't call matching_faqs_url when matching_faqs_count == 0"
2903 collection = IFAQCollection(self.context)
2904- return canonical_url(collection) + '/+faqs?' + urlencode({
2905- 'field.search_text': self.search_text.encode('utf-8'),
2906- 'field.actions.search': 'Search',
2907- })
2908+ return (
2909+ canonical_url(collection)
2910+ + "/+faqs?"
2911+ + urlencode(
2912+ {
2913+ "field.search_text": self.search_text.encode("utf-8"),
2914+ "field.actions.search": "Search",
2915+ }
2916+ )
2917+ )
2918
2919 @safe_action
2920- @action(_('Search'))
2921+ @action(_("Search"))
2922 def search_action(self, action, data):
2923 """Action executed when the user clicked the search button.
2924
2925@@ -452,9 +475,9 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2926 """
2927 self.search_params = dict(self.getDefaultFilter())
2928 self.search_params.update(**data)
2929- search_text = self.search_params.get('search_text', None)
2930+ search_text = self.search_params.get("search_text", None)
2931 if search_text is not None:
2932- self.search_params['search_text'] = search_text.strip()
2933+ self.search_params["search_text"] = search_text.strip()
2934
2935 def searchResults(self):
2936 """Return the questions corresponding to the search."""
2937@@ -468,8 +491,10 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2938 # ones defined in getDefaultFilter() which varies based on the
2939 # concrete view class.
2940 question_collection = IQuestionCollection(self.context)
2941- return BatchNavigator(question_collection.searchQuestions(
2942- **self.search_params), self.request)
2943+ return BatchNavigator(
2944+ question_collection.searchQuestions(**self.search_params),
2945+ self.request,
2946+ )
2947
2948 @property
2949 def display_sourcepackage_column(self):
2950@@ -486,22 +511,25 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
2951 # SQLObject can refetch the question, so we are comparing ids.
2952 assert self.context.id == question.distribution.id, (
2953 "The question.distribution (%s) must be equal to the context (%s)"
2954- % (question.distribution, self.context))
2955+ % (question.distribution, self.context)
2956+ )
2957 if not question.sourcepackagename:
2958 return "&mdash;"
2959 else:
2960 sourcepackage = self.context.getSourcePackage(
2961- question.sourcepackagename)
2962+ question.sourcepackagename
2963+ )
2964 return '<a href="%s">%s</a>' % (
2965- canonical_url(sourcepackage, rootsite='answers'),
2966- question.sourcepackagename.name)
2967+ canonical_url(sourcepackage, rootsite="answers"),
2968+ question.sourcepackagename.name,
2969+ )
2970
2971 @property
2972 def can_configure_answers(self):
2973 """Can the user configure answers for the `IQuestionTarget`."""
2974 target = self.context
2975 if IProduct.providedBy(target) or IDistribution.providedBy(target):
2976- return check_permission('launchpad.Edit', self.context)
2977+ return check_permission("launchpad.Edit", self.context)
2978 else:
2979 return False
2980
2981@@ -520,13 +548,19 @@ class QuestionCollectionMyQuestionsView(SearchQuestionsView):
2982 def page_title(self):
2983 """See `SearchQuestionsView`."""
2984 if self.search_text:
2985- return _('Questions you asked matching "${search_text}" for '
2986- '${context}', mapping=dict(
2987- context=self.context.displayname,
2988- search_text=self.search_text))
2989+ return _(
2990+ 'Questions you asked matching "${search_text}" for '
2991+ "${context}",
2992+ mapping=dict(
2993+ context=self.context.displayname,
2994+ search_text=self.search_text,
2995+ ),
2996+ )
2997 else:
2998- return _('Questions you asked about ${context}',
2999- mapping={'context': self.context.displayname})
3000+ return _(
3001+ "Questions you asked about ${context}",
3002+ mapping={"context": self.context.displayname},
3003+ )
3004
3005 label = page_title
3006
3007@@ -534,13 +568,19 @@ class QuestionCollectionMyQuestionsView(SearchQuestionsView):
3008 def empty_listing_message(self):
3009 """See `SearchQuestionsView`."""
3010 if self.search_text:
3011- return _("You didn't ask any questions matching "
3012- '"${search_text}" for ${context}.', mapping=dict(
3013- context=self.context.displayname,
3014- search_text=self.search_text))
3015+ return _(
3016+ "You didn't ask any questions matching "
3017+ '"${search_text}" for ${context}.',
3018+ mapping=dict(
3019+ context=self.context.displayname,
3020+ search_text=self.search_text,
3021+ ),
3022+ )
3023 else:
3024- return _("You didn't ask any questions about ${context}.",
3025- mapping={'context': self.context.displayname})
3026+ return _(
3027+ "You didn't ask any questions about ${context}.",
3028+ mapping={"context": self.context.displayname},
3029+ )
3030
3031 def getDefaultFilter(self):
3032 """See `SearchQuestionsView`."""
3033@@ -561,13 +601,19 @@ class QuestionCollectionNeedAttentionView(SearchQuestionsView):
3034 def page_title(self):
3035 """See `SearchQuestionsView`."""
3036 if self.search_text:
3037- return _('Questions matching "${search_text}" needing your '
3038- 'attention for ${context}', mapping=dict(
3039- context=self.context.displayname,
3040- search_text=self.search_text))
3041+ return _(
3042+ 'Questions matching "${search_text}" needing your '
3043+ "attention for ${context}",
3044+ mapping=dict(
3045+ context=self.context.displayname,
3046+ search_text=self.search_text,
3047+ ),
3048+ )
3049 else:
3050- return _('Questions needing your attention for ${context}',
3051- mapping={'context': self.context.displayname})
3052+ return _(
3053+ "Questions needing your attention for ${context}",
3054+ mapping={"context": self.context.displayname},
3055+ )
3056
3057 label = page_title
3058
3059@@ -575,29 +621,38 @@ class QuestionCollectionNeedAttentionView(SearchQuestionsView):
3060 def empty_listing_message(self):
3061 """See `SearchQuestionsView`."""
3062 if self.search_text:
3063- return _('No questions matching "${search_text}" need your '
3064- 'attention for ${context}.', mapping=dict(
3065- context=self.context.displayname,
3066- search_text=self.search_text))
3067+ return _(
3068+ 'No questions matching "${search_text}" need your '
3069+ "attention for ${context}.",
3070+ mapping=dict(
3071+ context=self.context.displayname,
3072+ search_text=self.search_text,
3073+ ),
3074+ )
3075 else:
3076- return _("No questions need your attention for ${context}.",
3077- mapping={'context': self.context.displayname})
3078+ return _(
3079+ "No questions need your attention for ${context}.",
3080+ mapping={"context": self.context.displayname},
3081+ )
3082
3083 def getDefaultFilter(self):
3084 """See `SearchQuestionsView`."""
3085- return dict(needs_attention_from=self.user,
3086- language=self.user_support_languages)
3087+ return dict(
3088+ needs_attention_from=self.user,
3089+ language=self.user_support_languages,
3090+ )
3091
3092
3093 class QuestionCollectionByLanguageView(SearchQuestionsView):
3094 """Search for questions in a specific language.
3095
3096- This view displays questions that are asked in the specified language
3097- for the QuestionTarget context.
3098- """
3099+ This view displays questions that are asked in the specified language
3100+ for the QuestionTarget context.
3101+ """
3102
3103 custom_widget_language = CustomWidgetFactory(
3104- LabeledMultiCheckBoxWidget, visible=False)
3105+ LabeledMultiCheckBoxWidget, visible=False
3106+ )
3107
3108 # No point showing a matching FAQs link on this report.
3109 matching_faqs_count = 0
3110@@ -611,43 +666,53 @@ class QuestionCollectionByLanguageView(SearchQuestionsView):
3111 SearchQuestionsView.__init__(self, context, request)
3112 # Language is intrinsic to this view; it manages the language
3113 # field without the help of formlib.
3114- lang_code = request.get('field.language', '')
3115+ lang_code = request.get("field.language", "")
3116 try:
3117 self.language = getUtility(ILanguageSet)[lang_code]
3118 except NotFoundError:
3119 self.request.response.redirect(
3120- canonical_url(self.context, rootsite='answers'))
3121+ canonical_url(self.context, rootsite="answers")
3122+ )
3123
3124 @property
3125 def page_title(self):
3126 """See `SearchQuestionsView`."""
3127- mapping = dict(context=self.context.displayname,
3128- search_text=self.search_text,
3129- language=self.language.englishname)
3130+ mapping = dict(
3131+ context=self.context.displayname,
3132+ search_text=self.search_text,
3133+ language=self.language.englishname,
3134+ )
3135 if self.search_text:
3136- return _('${language} questions matching "${search_text}" '
3137- 'in ${context}',
3138- mapping=mapping)
3139+ return _(
3140+ '${language} questions matching "${search_text}" '
3141+ "in ${context}",
3142+ mapping=mapping,
3143+ )
3144 else:
3145- return _('${language} questions in ${context}',
3146- mapping=mapping)
3147+ return _("${language} questions in ${context}", mapping=mapping)
3148
3149 label = page_title
3150
3151 @property
3152 def empty_listing_message(self):
3153 """See `SearchQuestionsView`."""
3154- mapping = dict(context=self.context.displayname,
3155- search_text=self.search_text,
3156- language=self.language.englishname)
3157+ mapping = dict(
3158+ context=self.context.displayname,
3159+ search_text=self.search_text,
3160+ language=self.language.englishname,
3161+ )
3162 if self.search_text:
3163- return _('No ${language} questions matching "${search_text}" '
3164- 'in ${context} for the selected status.',
3165- mapping=mapping)
3166+ return _(
3167+ 'No ${language} questions matching "${search_text}" '
3168+ "in ${context} for the selected status.",
3169+ mapping=mapping,
3170+ )
3171 else:
3172- return _('No ${language} questions in ${context} for the '
3173- 'selected status.',
3174- mapping=mapping)
3175+ return _(
3176+ "No ${language} questions in ${context} for the "
3177+ "selected status.",
3178+ mapping=mapping,
3179+ )
3180
3181 @property
3182 def show_language_control(self):
3183@@ -669,7 +734,7 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
3184
3185 @property
3186 def page_title(self):
3187- return 'Answer contact for %s' % self.context.title
3188+ return "Answer contact for %s" % self.context.title
3189
3190 label = page_title
3191 custom_widget_answer_contact_teams = LabeledMultiCheckBoxWidget
3192@@ -678,86 +743,114 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
3193 """See `LaunchpadFormView`."""
3194 self.form_fields = form.Fields(
3195 self._createUserAnswerContactField(),
3196- self._createTeamAnswerContactsField())
3197+ self._createTeamAnswerContactsField(),
3198+ )
3199
3200 def _createUserAnswerContactField(self):
3201 """Create the want_to_be_answer_contact field."""
3202 return Bool(
3203- __name__='want_to_be_answer_contact',
3204- title=_("I want to be an answer contact for $context",
3205- mapping=dict(context=self.context.displayname)),
3206- required=False)
3207+ __name__="want_to_be_answer_contact",
3208+ title=_(
3209+ "I want to be an answer contact for $context",
3210+ mapping=dict(context=self.context.displayname),
3211+ ),
3212+ required=False,
3213+ )
3214
3215 def _createTeamAnswerContactsField(self):
3216 """Create a list of teams the user is an administrator of."""
3217- sort_key = attrgetter('displayname')
3218+ sort_key = attrgetter("displayname")
3219 terms = []
3220 for team in sorted(self.administrated_teams, key=sort_key):
3221 terms.append(SimpleTerm(team, team.name, team.displayname))
3222
3223 public_person_choice = PublicPersonChoice(
3224- vocabulary=SimpleVocabulary(terms))
3225+ vocabulary=SimpleVocabulary(terms)
3226+ )
3227 return form.FormField(
3228 List(
3229- __name__='answer_contact_teams',
3230- title=_("Let the following teams be an answer contact for "
3231- "$context",
3232- mapping=dict(context=self.context.displayname)),
3233+ __name__="answer_contact_teams",
3234+ title=_(
3235+ "Let the following teams be an answer contact for "
3236+ "$context",
3237+ mapping=dict(context=self.context.displayname),
3238+ ),
3239 value_type=public_person_choice,
3240- required=False))
3241+ required=False,
3242+ )
3243+ )
3244
3245 @property
3246 def initial_values(self):
3247 """Return a dictionary of the default values for the form_fields."""
3248 user = self.user
3249 answer_contacts = self.context.direct_answer_contacts
3250- answer_contact_teams = set(
3251- answer_contacts).intersection(self.administrated_teams)
3252+ answer_contact_teams = set(answer_contacts).intersection(
3253+ self.administrated_teams
3254+ )
3255 return {
3256- 'want_to_be_answer_contact': user in answer_contacts,
3257- 'answer_contact_teams': list(answer_contact_teams),
3258- }
3259+ "want_to_be_answer_contact": user in answer_contacts,
3260+ "answer_contact_teams": list(answer_contact_teams),
3261+ }
3262
3263- @action(_('Continue'), name='update')
3264+ @action(_("Continue"), name="update")
3265 def update_action(self, action, data):
3266 """Update the answer contact registration."""
3267- want_to_be_answer_contact = data['want_to_be_answer_contact']
3268- answer_contact_teams = data.get('answer_contact_teams', [])
3269+ want_to_be_answer_contact = data["want_to_be_answer_contact"]
3270+ answer_contact_teams = data.get("answer_contact_teams", [])
3271 response = self.request.response
3272- replacements = {'context': self.context.displayname}
3273+ replacements = {"context": self.context.displayname}
3274 if want_to_be_answer_contact:
3275 self._updatePreferredLanguages(self.user)
3276 if self.context.addAnswerContact(self.user, self.user):
3277 response.addNotification(
3278- _('You have been added as an answer contact for '
3279- '$context.', mapping=replacements))
3280+ _(
3281+ "You have been added as an answer contact for "
3282+ "$context.",
3283+ mapping=replacements,
3284+ )
3285+ )
3286 else:
3287 if self.context.removeAnswerContact(self.user, self.user):
3288 response.addNotification(
3289- _('You have been removed as an answer contact for '
3290- '$context.', mapping=replacements))
3291+ _(
3292+ "You have been removed as an answer contact for "
3293+ "$context.",
3294+ mapping=replacements,
3295+ )
3296+ )
3297
3298 for team in self.administrated_teams:
3299- replacements['teamname'] = team.displayname
3300+ replacements["teamname"] = team.displayname
3301 if team in answer_contact_teams:
3302 self._updatePreferredLanguages(team)
3303 if self.context.addAnswerContact(team, self.user):
3304 response.addNotification(
3305- _('$teamname has been added as an answer contact '
3306- 'for $context.', mapping=replacements))
3307+ _(
3308+ "$teamname has been added as an answer contact "
3309+ "for $context.",
3310+ mapping=replacements,
3311+ )
3312+ )
3313 else:
3314 if self.context.removeAnswerContact(team, self.user):
3315 response.addNotification(
3316- _('$teamname has been removed as an answer contact '
3317- 'for $context.', mapping=replacements))
3318+ _(
3319+ "$teamname has been removed as an answer contact "
3320+ "for $context.",
3321+ mapping=replacements,
3322+ )
3323+ )
3324
3325- self.next_url = canonical_url(self.context, rootsite='answers')
3326+ self.next_url = canonical_url(self.context, rootsite="answers")
3327
3328 @property
3329 def administrated_teams(self):
3330 from lp.registry.browser.person import RestrictedMembershipsPersonView
3331- restricted_view = RestrictedMembershipsPersonView(self.user,
3332- self.request)
3333+
3334+ restricted_view = RestrictedMembershipsPersonView(
3335+ self.user, self.request
3336+ )
3337 return restricted_view.administrated_teams
3338
3339 def _updatePreferredLanguages(self, person_or_team):
3340@@ -776,12 +869,16 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
3341 english = getUtility(ILaunchpadCelebrities).english
3342 if person_or_team.is_team:
3343 person_or_team.addLanguage(english)
3344- team_mapping = {'name': person_or_team.name,
3345- 'displayname': person_or_team.displayname}
3346- msgid = _("English was added to ${displayname}'s "
3347- '<a href="/~${name}/+editlanguages">preferred '
3348- 'languages</a>.',
3349- mapping=team_mapping)
3350+ team_mapping = {
3351+ "name": person_or_team.name,
3352+ "displayname": person_or_team.displayname,
3353+ }
3354+ msgid = _(
3355+ "English was added to ${displayname}'s "
3356+ '<a href="/~${name}/+editlanguages">preferred '
3357+ "languages</a>.",
3358+ mapping=team_mapping,
3359+ )
3360 response.addNotification(structured(msgid))
3361 else:
3362 if len(browser_languages(self.request)) > 0:
3363@@ -790,11 +887,13 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
3364 languages = [english]
3365 for language in languages:
3366 person_or_team.addLanguage(language)
3367- language_str = ', '.join([lang.displayname for lang in languages])
3368- msgid = _('<a href="/people/+me/+editlanguages">Your preferred '
3369- 'languages</a> were updated to include your browser '
3370- 'languages: $languages.',
3371- mapping={'languages': language_str})
3372+ language_str = ", ".join([lang.displayname for lang in languages])
3373+ msgid = _(
3374+ '<a href="/people/+me/+editlanguages">Your preferred '
3375+ "languages</a> were updated to include your browser "
3376+ "languages: $languages.",
3377+ mapping={"languages": language_str},
3378+ )
3379 response.addNotification(structured(msgid))
3380
3381
3382@@ -808,11 +907,12 @@ class QuestionTargetPortletAnswerContacts(LaunchpadView):
3383 def initialize(self):
3384 cache = IJSONRequestCache(self.request).objects
3385 context_url_data = {
3386- 'web_link': canonical_url(self.context, rootsite='mainsite'),
3387- 'self_link': absoluteURL(self.context, self.api_request),
3388- }
3389- cache[self.context.name + '_answer_portlet_url_data'] = (
3390- context_url_data)
3391+ "web_link": canonical_url(self.context, rootsite="mainsite"),
3392+ "self_link": absoluteURL(self.context, self.api_request),
3393+ }
3394+ cache[
3395+ self.context.name + "_answer_portlet_url_data"
3396+ ] = context_url_data
3397
3398
3399 class QuestionTargetPortletAnswerContactsWithDetails(LaunchpadView):
3400@@ -832,22 +932,23 @@ class QuestionTargetPortletAnswerContactsWithDetails(LaunchpadView):
3401 answer_contacts = list(questiontarget.direct_answer_contacts)
3402 for person in answer_contacts:
3403 can_edit = questiontarget.canUserAlterAnswerContact(
3404- person, self.user)
3405+ person, self.user
3406+ )
3407 if person.private and not can_edit:
3408 # Skip private teams user is not a member of.
3409 continue
3410
3411 answer_contact = {
3412- 'name': person.name,
3413- 'display_name': person.displayname,
3414- 'web_link': canonical_url(person, rootsite='mainsite'),
3415- 'self_link': absoluteURL(person, self.api_request),
3416- 'is_team': person.is_team,
3417- 'can_edit': can_edit
3418- }
3419+ "name": person.name,
3420+ "display_name": person.displayname,
3421+ "web_link": canonical_url(person, rootsite="mainsite"),
3422+ "self_link": absoluteURL(person, self.api_request),
3423+ "is_team": person.is_team,
3424+ "can_edit": can_edit,
3425+ }
3426 record = {
3427- 'subscriber': answer_contact,
3428- }
3429+ "subscriber": answer_contact,
3430+ }
3431 data.append(record)
3432 return data
3433
3434@@ -860,14 +961,14 @@ class QuestionTargetPortletAnswerContactsWithDetails(LaunchpadView):
3435
3436 def render(self):
3437 """Override the default render() to return only JSON."""
3438- self.request.response.setHeader('content-type', 'application/json')
3439+ self.request.response.setHeader("content-type", "application/json")
3440 return self.answercontact_data_js
3441
3442
3443 class QuestionTargetTraversalMixin:
3444 """Navigation mixin for IQuestionTarget."""
3445
3446- @stepthrough('+question')
3447+ @stepthrough("+question")
3448 def traverse_question(self, name):
3449 """Return the question."""
3450 # questions should be ints
3451@@ -885,9 +986,10 @@ class QuestionTargetTraversalMixin:
3452 if question is None:
3453 raise NotFoundError(name)
3454 return self.redirectSubTree(
3455- canonical_url(question, request=self.request))
3456+ canonical_url(question, request=self.request)
3457+ )
3458
3459- @stepto('+ticket')
3460+ @stepto("+ticket")
3461 def redirect_ticket(self):
3462 """Use RedirectionNavigation to redirect to +question.
3463
3464@@ -895,68 +997,79 @@ class QuestionTargetTraversalMixin:
3465 """
3466 target = urlappend(
3467 canonical_url(
3468- self.context, request=self.request, rootsite='answers'),
3469- '+question')
3470+ self.context, request=self.request, rootsite="answers"
3471+ ),
3472+ "+question",
3473+ )
3474 return self.redirectSubTree(target)
3475
3476
3477 class QuestionCollectionAnswersMenu(FAQCollectionMenu):
3478 """Base menu definition for QuestionCollection searchable by owner."""
3479+
3480 # XXX flacoste 2007-07-08 bug=125851:
3481 # This menu shouldn't "extend" FAQCollectionMenu.
3482 # architecture. But this is needed because of limitations in the current
3483 # menu Menu should be built by merging all menus applying to the context
3484 # object (-based on the interfaces it provides).
3485 usedfor = ISearchableByQuestionOwner
3486- facet = 'answers'
3487+ facet = "answers"
3488 links = FAQCollectionMenu.links + [
3489- 'open', 'answered', 'myrequests', 'need_attention']
3490+ "open",
3491+ "answered",
3492+ "myrequests",
3493+ "need_attention",
3494+ ]
3495
3496- def makeSearchLink(self, statuses, sort='by relevancy'):
3497+ def makeSearchLink(self, statuses, sort="by relevancy"):
3498 """Return the search parameters for a search link."""
3499 return "+questions?" + urlencode(
3500- {'field.status': statuses,
3501- 'field.sort': sort,
3502- 'field.search_text': '',
3503- 'field.actions.search': 'Search',
3504- 'field.status': statuses}, doseq=True)
3505+ {
3506+ "field.status": statuses,
3507+ "field.sort": sort,
3508+ "field.search_text": "",
3509+ "field.actions.search": "Search",
3510+ "field.status": statuses,
3511+ },
3512+ doseq=True,
3513+ )
3514
3515 def open(self):
3516 """Return a Link that opens a question."""
3517- url = self.makeSearchLink('Open', sort='recently updated first')
3518- return Link(url, 'Open', icon='question')
3519+ url = self.makeSearchLink("Open", sort="recently updated first")
3520+ return Link(url, "Open", icon="question")
3521
3522 def answered(self):
3523 """Return a Link to display questions that are open."""
3524- text = 'Answered'
3525+ text = "Answered"
3526 return Link(
3527- self.makeSearchLink(['Answered', 'Solved']),
3528- text, icon='question')
3529+ self.makeSearchLink(["Answered", "Solved"]), text, icon="question"
3530+ )
3531
3532 def myrequests(self):
3533 """Return a Link to display the user's questions."""
3534- text = 'My questions'
3535- return Link('+myquestions', text, icon='question')
3536+ text = "My questions"
3537+ return Link("+myquestions", text, icon="question")
3538
3539 def need_attention(self):
3540 """Return a Link to display questions that need attention."""
3541- text = 'Need attention'
3542- return Link('+need-attention', text, icon='question')
3543+ text = "Need attention"
3544+ return Link("+need-attention", text, icon="question")
3545
3546
3547 class QuestionTargetAnswersMenu(QuestionCollectionAnswersMenu):
3548 """Base menu definition for QuestionTargets."""
3549
3550 usedfor = IQuestionTarget
3551- facet = 'answers'
3552- links = QuestionCollectionAnswersMenu.links + ['new', 'answer_contact']
3553+ facet = "answers"
3554+ links = QuestionCollectionAnswersMenu.links + ["new", "answer_contact"]
3555
3556 def new(self):
3557 """Return a link to ask a question."""
3558- text = 'Ask a question'
3559- return Link('+addquestion', text, icon='add')
3560+ text = "Ask a question"
3561+ return Link("+addquestion", text, icon="add")
3562
3563 def answer_contact(self):
3564 """Return a link to the manage answer contact view."""
3565- text = 'Set answer contact'
3566- return Link('+answer-contact', text, icon='edit')
3567+ text = "Set answer contact"
3568+ return Link("+answer-contact", text, icon="edit")
3569diff --git a/lib/lp/answers/browser/tests/test_breadcrumbs.py b/lib/lp/answers/browser/tests/test_breadcrumbs.py
3570index cca8dd6..23e6b60 100644
3571--- a/lib/lp/answers/browser/tests/test_breadcrumbs.py
3572+++ b/lib/lp/answers/browser/tests/test_breadcrumbs.py
3573@@ -7,7 +7,8 @@ from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
3574
3575
3576 class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersFacet(
3577- BaseBreadcrumbTestCase):
3578+ BaseBreadcrumbTestCase
3579+):
3580 """Test Breadcrumbs for IQuestionTarget, IProjectGroup and IPerson on the
3581 answers vhost.
3582
3583@@ -20,33 +21,34 @@ class TestQuestionTargetProjectAndPersonBreadcrumbOnAnswersFacet(
3584 super().setUp()
3585 self.person = self.factory.makePerson()
3586 self.person_questions_url = canonical_url(
3587- self.person, rootsite='answers')
3588+ self.person, rootsite="answers"
3589+ )
3590 self.product = self.factory.makeProduct()
3591 self.product_questions_url = canonical_url(
3592- self.product, rootsite='answers')
3593+ self.product, rootsite="answers"
3594+ )
3595 self.project = self.factory.makeProject()
3596 self.project_questions_url = canonical_url(
3597- self.project, rootsite='answers')
3598+ self.project, rootsite="answers"
3599+ )
3600
3601 def test_product(self):
3602- crumbs = self.getBreadcrumbsForObject(
3603- self.product, rootsite='answers')
3604+ crumbs = self.getBreadcrumbsForObject(self.product, rootsite="answers")
3605 last_crumb = crumbs[-1]
3606 self.assertEqual(last_crumb.url, self.product_questions_url)
3607- self.assertEqual(last_crumb.text, 'Questions')
3608+ self.assertEqual(last_crumb.text, "Questions")
3609
3610 def test_project(self):
3611- crumbs = self.getBreadcrumbsForObject(
3612- self.project, rootsite='answers')
3613+ crumbs = self.getBreadcrumbsForObject(self.project, rootsite="answers")
3614 last_crumb = crumbs[-1]
3615 self.assertEqual(last_crumb.url, self.project_questions_url)
3616- self.assertEqual(last_crumb.text, 'Questions')
3617+ self.assertEqual(last_crumb.text, "Questions")
3618
3619 def test_person(self):
3620- crumbs = self.getBreadcrumbsForObject(self.person, rootsite='answers')
3621+ crumbs = self.getBreadcrumbsForObject(self.person, rootsite="answers")
3622 last_crumb = crumbs[-1]
3623 self.assertEqual(last_crumb.url, self.person_questions_url)
3624- self.assertEqual(last_crumb.text, 'Questions')
3625+ self.assertEqual(last_crumb.text, "Questions")
3626
3627
3628 class TestAnswersBreadcrumb(BaseBreadcrumbTestCase):
3629@@ -59,15 +61,16 @@ class TestAnswersBreadcrumb(BaseBreadcrumbTestCase):
3630
3631 def test_question(self):
3632 self.question = self.factory.makeQuestion(
3633- target=self.product, title='Seeds are hard to chew')
3634- self.question_url = canonical_url(self.question, rootsite='answers')
3635+ target=self.product, title="Seeds are hard to chew"
3636+ )
3637+ self.question_url = canonical_url(self.question, rootsite="answers")
3638 crumbs = self.getBreadcrumbsForObject(self.question)
3639 last_crumb = crumbs[-1]
3640- self.assertEqual(last_crumb.text, 'Question #%d' % self.question.id)
3641+ self.assertEqual(last_crumb.text, "Question #%d" % self.question.id)
3642
3643 def test_faq(self):
3644- self.faq = self.factory.makeFAQ(target=self.product, title='Seedless')
3645- self.faq_url = canonical_url(self.faq, rootsite='answers')
3646+ self.faq = self.factory.makeFAQ(target=self.product, title="Seedless")
3647+ self.faq_url = canonical_url(self.faq, rootsite="answers")
3648 crumbs = self.getBreadcrumbsForObject(self.faq)
3649 last_crumb = crumbs[-1]
3650- self.assertEqual(last_crumb.text, 'FAQ #%d' % self.faq.id)
3651+ self.assertEqual(last_crumb.text, "FAQ #%d" % self.faq.id)
3652diff --git a/lib/lp/answers/browser/tests/test_menus.py b/lib/lp/answers/browser/tests/test_menus.py
3653index a0fdf48..622faee 100644
3654--- a/lib/lp/answers/browser/tests/test_menus.py
3655+++ b/lib/lp/answers/browser/tests/test_menus.py
3656@@ -3,21 +3,16 @@
3657
3658 from zope.component import getUtility
3659
3660-from lp.answers.browser.question import (
3661- QuestionEditMenu,
3662- QuestionExtrasMenu,
3663- )
3664+from lp.answers.browser.question import QuestionEditMenu, QuestionExtrasMenu
3665 from lp.services.worlddata.interfaces.language import ILanguageSet
3666-from lp.testing import (
3667- login_person,
3668- TestCaseWithFactory,
3669- )
3670+from lp.testing import TestCaseWithFactory, login_person
3671 from lp.testing.layers import DatabaseFunctionalLayer
3672 from lp.testing.menu import check_menu_links
3673
3674
3675 class TestQuestionMenus(TestCaseWithFactory):
3676 """Test specification menus links."""
3677+
3678 layer = DatabaseFunctionalLayer
3679
3680 def setUp(self):
3681@@ -38,12 +33,12 @@ class TestQuestionMenus(TestCaseWithFactory):
3682 # A question without a linked FAQ has an 'add' icon.
3683 menu = QuestionExtrasMenu(self.question)
3684 link = menu.linkfaq()
3685- self.assertEqual('add', link.icon)
3686+ self.assertEqual("add", link.icon)
3687 # A question with a linked FAQ has an 'edit' icon.
3688- self.person.addLanguage(getUtility(ILanguageSet)['en'])
3689+ self.person.addLanguage(getUtility(ILanguageSet)["en"])
3690 target = self.question.target
3691 target.addAnswerContact(self.person, self.person)
3692 faq = self.factory.makeFAQ(target=target)
3693- self.question.linkFAQ(self.person, faq, 'message')
3694+ self.question.linkFAQ(self.person, faq, "message")
3695 link = menu.linkfaq()
3696- self.assertEqual('edit', link.icon)
3697+ self.assertEqual("edit", link.icon)
3698diff --git a/lib/lp/answers/browser/tests/test_question.py b/lib/lp/answers/browser/tests/test_question.py
3699index 9178619..0619fc3 100644
3700--- a/lib/lp/answers/browser/tests/test_question.py
3701+++ b/lib/lp/answers/browser/tests/test_question.py
3702@@ -11,11 +11,7 @@ from lp.answers.browser.question import QuestionTargetWidget
3703 from lp.answers.interfaces.question import IQuestion
3704 from lp.app.enums import ServiceUsage
3705 from lp.services.webapp.servers import LaunchpadTestRequest
3706-from lp.testing import (
3707- login_person,
3708- person_logged_in,
3709- TestCaseWithFactory,
3710- )
3711+from lp.testing import TestCaseWithFactory, login_person, person_logged_in
3712 from lp.testing.layers import DatabaseFunctionalLayer
3713 from lp.testing.views import create_initialized_view
3714
3715@@ -31,40 +27,48 @@ class TestQuestionAddView(TestCaseWithFactory):
3716 self.user = self.factory.makePerson()
3717 login_person(self.user)
3718
3719- def getSearchForm(self, title, language='en'):
3720+ def getSearchForm(self, title, language="en"):
3721 return {
3722- 'field.title': title,
3723- 'field.language': language,
3724- 'field.actions.continue': 'Continue',
3725- }
3726+ "field.title": title,
3727+ "field.language": language,
3728+ "field.actions.continue": "Continue",
3729+ }
3730
3731 def test_question_title_within_max_display_width(self):
3732 # Titles (summary in the view) less than 250 characters are accepted.
3733- form = self.getSearchForm('123456789 ' * 10)
3734+ form = self.getSearchForm("123456789 " * 10)
3735 view = create_initialized_view(
3736- self.question_target, name='+addquestion', form=form,
3737- principal=self.user)
3738+ self.question_target,
3739+ name="+addquestion",
3740+ form=form,
3741+ principal=self.user,
3742+ )
3743 self.assertEqual([], view.errors)
3744
3745 def test_question_title_exceeds_max_display_width(self):
3746 # Titles (summary in the view) cannot exceed 250 characters.
3747- form = self.getSearchForm('123456789 ' * 26)
3748+ form = self.getSearchForm("123456789 " * 26)
3749 view = create_initialized_view(
3750- self.question_target, name='+addquestion', form=form,
3751- principal=self.user)
3752+ self.question_target,
3753+ name="+addquestion",
3754+ form=form,
3755+ principal=self.user,
3756+ )
3757 self.assertEqual(1, len(view.errors))
3758 self.assertEqual(
3759- 'The summary cannot exceed 250 characters.', view.errors[0])
3760+ "The summary cannot exceed 250 characters.", view.errors[0]
3761+ )
3762
3763 def test_context_uses_answers(self):
3764 # If a target doesn't use answers, it doesn't provide the form.
3765- #logout()
3766+ # logout()
3767 owner = removeSecurityProxy(self.question_target).owner
3768 with person_logged_in(owner):
3769 self.question_target.answers_usage = ServiceUsage.NOT_APPLICABLE
3770 login_person(self.user)
3771 view = create_initialized_view(
3772- self.question_target, name='+addquestion', principal=self.user)
3773+ self.question_target, name="+addquestion", principal=self.user
3774+ )
3775 self.assertFalse(view.context_uses_answers)
3776 contents = view.render()
3777 msg = "<strong>does not use</strong> Launchpad as its answer forum"
3778@@ -78,21 +82,21 @@ class QuestionEditViewTestCase(TestCaseWithFactory):
3779
3780 def getForm(self, question):
3781 if question.assignee is None:
3782- assignee = ''
3783+ assignee = ""
3784 else:
3785 assignee = question.assignee.name
3786 return {
3787- 'field.title': question.title,
3788- 'field.description': question.description,
3789- 'field.language': question.language.code,
3790- 'field.assignee': assignee,
3791- 'field.target': 'product',
3792- 'field.target.distribution': '',
3793- 'field.target.package': '',
3794- 'field.target.product': question.target.name,
3795- 'field.whiteboard': question.whiteboard,
3796- 'field.actions.change': 'Change',
3797- }
3798+ "field.title": question.title,
3799+ "field.description": question.description,
3800+ "field.language": question.language.code,
3801+ "field.assignee": assignee,
3802+ "field.target": "product",
3803+ "field.target.distribution": "",
3804+ "field.target.package": "",
3805+ "field.target.product": question.target.name,
3806+ "field.whiteboard": question.whiteboard,
3807+ "field.actions.change": "Change",
3808+ }
3809
3810 def test_retarget_with_other_changed(self):
3811 # Retargeting must be the last change made to the question
3812@@ -103,20 +107,21 @@ class QuestionEditViewTestCase(TestCaseWithFactory):
3813 other_target = self.factory.makeProduct()
3814 login_person(target.owner)
3815 form = self.getForm(question)
3816- form['field.whiteboard'] = 'comment'
3817- form['field.target.product'] = other_target.name
3818- view = create_initialized_view(question, name='+edit', form=form)
3819+ form["field.whiteboard"] = "comment"
3820+ form["field.target.product"] = other_target.name
3821+ view = create_initialized_view(question, name="+edit", form=form)
3822 self.assertEqual([], view.errors)
3823 self.assertEqual(other_target, question.target)
3824- self.assertEqual('comment', question.whiteboard)
3825+ self.assertEqual("comment", question.whiteboard)
3826
3827
3828 class QuestionTargetWidgetTestCase(TestCaseWithFactory):
3829 """Test that QuestionTargetWidgetTestCase behaves as expected."""
3830+
3831 layer = DatabaseFunctionalLayer
3832
3833 def getWidget(self, question):
3834- field = IQuestion['target']
3835+ field = IQuestion["target"]
3836 bound_field = field.bind(question)
3837 request = LaunchpadTestRequest()
3838 return QuestionTargetWidget(bound_field, request)
3839@@ -132,7 +137,8 @@ class QuestionTargetWidgetTestCase(TestCaseWithFactory):
3840 self.assertEqual(None, vocabulary.distribution)
3841 self.assertFalse(
3842 distribution in vocabulary,
3843- "Vocabulary contains distros that do not use Launchpad Answers.")
3844+ "Vocabulary contains distros that do not use Launchpad Answers.",
3845+ )
3846
3847 def test_getDistributionVocabulary_with_distribution_question(self):
3848 # The vocabulary does not contain distros that do not use
3849@@ -145,7 +151,9 @@ class QuestionTargetWidgetTestCase(TestCaseWithFactory):
3850 self.assertEqual(distribution, vocabulary.distribution)
3851 self.assertTrue(
3852 distribution in vocabulary,
3853- "Vocabulary missing context distribution.")
3854+ "Vocabulary missing context distribution.",
3855+ )
3856 self.assertFalse(
3857 other_distribution in vocabulary,
3858- "Vocabulary contains distros that do not use Launchpad Answers.")
3859+ "Vocabulary contains distros that do not use Launchpad Answers.",
3860+ )
3861diff --git a/lib/lp/answers/browser/tests/test_questionmessages.py b/lib/lp/answers/browser/tests/test_questionmessages.py
3862index bd4f5d6..0e1bb02 100644
3863--- a/lib/lp/answers/browser/tests/test_questionmessages.py
3864+++ b/lib/lp/answers/browser/tests/test_questionmessages.py
3865@@ -10,17 +10,15 @@ from lp.app.interfaces.launchpad import ILaunchpadCelebrities
3866 from lp.coop.answersbugs.visibility import (
3867 TestHideMessageControlMixin,
3868 TestMessageVisibilityMixin,
3869- )
3870-from lp.testing import (
3871- BrowserTestCase,
3872- person_logged_in,
3873- )
3874+)
3875+from lp.testing import BrowserTestCase, person_logged_in
3876 from lp.testing.layers import DatabaseFunctionalLayer
3877 from lp.testing.pages import find_tag_by_id
3878
3879
3880 class TestQuestionMessageVisibility(
3881- BrowserTestCase, TestMessageVisibilityMixin):
3882+ BrowserTestCase, TestMessageVisibilityMixin
3883+):
3884
3885 layer = DatabaseFunctionalLayer
3886
3887@@ -38,18 +36,18 @@ class TestQuestionMessageVisibility(
3888 def getView(self, context, user=None, no_login=False):
3889 """Required by the mixin."""
3890 view = self.getViewBrowser(
3891- context=context,
3892- user=user,
3893- no_login=no_login)
3894+ context=context, user=user, no_login=no_login
3895+ )
3896 return view
3897
3898
3899 class TestHideQuestionMessageControls(
3900- BrowserTestCase, TestHideMessageControlMixin):
3901+ BrowserTestCase, TestHideMessageControlMixin
3902+):
3903
3904 layer = DatabaseFunctionalLayer
3905
3906- control_text = 'mark-spam-0'
3907+ control_text = "mark-spam-0"
3908
3909 def getContext(self, comment_owner=None):
3910 """Required by the mixin."""
3911@@ -64,9 +62,8 @@ class TestHideQuestionMessageControls(
3912 def getView(self, context, user=None, no_login=False):
3913 """Required by the mixin."""
3914 view = self.getViewBrowser(
3915- context=context,
3916- user=user,
3917- no_login=no_login)
3918+ context=context, user=user, no_login=no_login
3919+ )
3920 return view
3921
3922 def test_comment_owner_sees_hide_control(self):
3923diff --git a/lib/lp/answers/browser/tests/test_questionsubscription_views.py b/lib/lp/answers/browser/tests/test_questionsubscription_views.py
3924index 8c7002e..fc0b918 100644
3925--- a/lib/lp/answers/browser/tests/test_questionsubscription_views.py
3926+++ b/lib/lp/answers/browser/tests/test_questionsubscription_views.py
3927@@ -14,10 +14,10 @@ from zope.traversing.browser import absoluteURL
3928 from lp.registry.interfaces.person import IPersonSet
3929 from lp.services.webapp import canonical_url
3930 from lp.testing import (
3931- person_logged_in,
3932 StormStatementRecorder,
3933 TestCaseWithFactory,
3934- )
3935+ person_logged_in,
3936+)
3937 from lp.testing.layers import LaunchpadFunctionalLayer
3938 from lp.testing.matchers import HasQueryCount
3939 from lp.testing.sampledata import ADMIN_EMAIL
3940@@ -26,18 +26,19 @@ from lp.testing.views import create_view
3941
3942 class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
3943 """Tests for IQuestion:+portlet-subscribers-details view."""
3944+
3945 layer = LaunchpadFunctionalLayer
3946
3947 def test_content_type(self):
3948 question = self.factory.makeQuestion()
3949
3950 # It works even for anonymous users, so no log-in is needed.
3951- view = create_view(question, '+portlet-subscribers-details')
3952+ view = create_view(question, "+portlet-subscribers-details")
3953 view.render()
3954
3955 self.assertEqual(
3956- view.request.response.getHeader('content-type'),
3957- 'application/json')
3958+ view.request.response.getHeader("content-type"), "application/json"
3959+ )
3960
3961 def _makeQuestionWithNoSubscribers(self):
3962 question = self.factory.makeQuestion()
3963@@ -48,7 +49,7 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
3964
3965 def test_data_no_subscriptions(self):
3966 question = self._makeQuestionWithNoSubscribers()
3967- view = create_view(question, '+portlet-subscribers-details')
3968+ view = create_view(question, "+portlet-subscribers-details")
3969 self.assertEqual([], json.loads(view.subscriber_data_js))
3970
3971 def test_data_person_subscription(self):
3972@@ -57,37 +58,40 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
3973 # subscribers_list.js subscribers loading.
3974 question = self._makeQuestionWithNoSubscribers()
3975 subscriber = self.factory.makePerson(
3976- name='user', displayname='Subscriber Name')
3977+ name="user", displayname="Subscriber Name"
3978+ )
3979 with person_logged_in(subscriber):
3980 question.subscribe(subscriber, subscriber)
3981- view = create_view(question, '+portlet-subscribers-details')
3982+ view = create_view(question, "+portlet-subscribers-details")
3983 api_request = IWebServiceClientRequest(view.request)
3984
3985 expected_result = {
3986- 'subscriber': {
3987- 'name': 'user',
3988- 'display_name': 'Subscriber Name',
3989- 'is_team': False,
3990- 'can_edit': False,
3991- 'web_link': canonical_url(subscriber),
3992- 'self_link': absoluteURL(subscriber, api_request)
3993- },
3994- 'subscription_level': "Direct",
3995- }
3996+ "subscriber": {
3997+ "name": "user",
3998+ "display_name": "Subscriber Name",
3999+ "is_team": False,
4000+ "can_edit": False,
4001+ "web_link": canonical_url(subscriber),
4002+ "self_link": absoluteURL(subscriber, api_request),
4003+ },
4004+ "subscription_level": "Direct",
4005+ }
4006 self.assertEqual(
4007- [expected_result], json.loads(view.subscriber_data_js))
4008+ [expected_result], json.loads(view.subscriber_data_js)
4009+ )
4010
4011 def test_data_person_subscription_other_subscriber_query_count(self):
4012 # All subscriber data should be retrieved with a single query.
4013 question = self._makeQuestionWithNoSubscribers()
4014 subscribed_by = self.factory.makePerson(
4015- name="someone", displayname='Someone')
4016+ name="someone", displayname="Someone"
4017+ )
4018 subscriber = self.factory.makePerson(
4019- name='user', displayname='Subscriber Name')
4020+ name="user", displayname="Subscriber Name"
4021+ )
4022 with person_logged_in(subscriber):
4023- question.subscribe(person=subscriber,
4024- subscribed_by=subscribed_by)
4025- view = create_view(question, '+portlet-subscribers-details')
4026+ question.subscribe(person=subscriber, subscribed_by=subscribed_by)
4027+ view = create_view(question, "+portlet-subscribers-details")
4028 # Invoke the view method, ignoring the results.
4029 Store.of(question).invalidate()
4030 with StormStatementRecorder() as recorder:
4031@@ -99,55 +103,61 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
4032 # to true.
4033 question = self._makeQuestionWithNoSubscribers()
4034 teamowner = self.factory.makePerson(
4035- name="team-owner", displayname="Team Owner")
4036+ name="team-owner", displayname="Team Owner"
4037+ )
4038 subscriber = self.factory.makeTeam(
4039- name='team', displayname='Team Name', owner=teamowner)
4040+ name="team", displayname="Team Name", owner=teamowner
4041+ )
4042 with person_logged_in(subscriber.teamowner):
4043 question.subscribe(subscriber, subscriber.teamowner)
4044- view = create_view(question, '+portlet-subscribers-details')
4045+ view = create_view(question, "+portlet-subscribers-details")
4046 api_request = IWebServiceClientRequest(view.request)
4047
4048 expected_result = {
4049- 'subscriber': {
4050- 'name': 'team',
4051- 'display_name': 'Team Name',
4052- 'is_team': True,
4053- 'can_edit': False,
4054- 'web_link': canonical_url(subscriber),
4055- 'self_link': absoluteURL(subscriber, api_request)
4056- },
4057- 'subscription_level': "Direct",
4058- }
4059+ "subscriber": {
4060+ "name": "team",
4061+ "display_name": "Team Name",
4062+ "is_team": True,
4063+ "can_edit": False,
4064+ "web_link": canonical_url(subscriber),
4065+ "self_link": absoluteURL(subscriber, api_request),
4066+ },
4067+ "subscription_level": "Direct",
4068+ }
4069 self.assertEqual(
4070- [expected_result], json.loads(view.subscriber_data_js))
4071+ [expected_result], json.loads(view.subscriber_data_js)
4072+ )
4073
4074 def test_data_team_subscription_owner_looks(self):
4075 # For a team subscription, subscriber_data_js has can_edit
4076 # set to true for team owner.
4077 question = self._makeQuestionWithNoSubscribers()
4078 teamowner = self.factory.makePerson(
4079- name="team-owner", displayname="Team Owner")
4080+ name="team-owner", displayname="Team Owner"
4081+ )
4082 subscriber = self.factory.makeTeam(
4083- name='team', displayname='Team Name', owner=teamowner)
4084+ name="team", displayname="Team Name", owner=teamowner
4085+ )
4086 with person_logged_in(subscriber.teamowner):
4087 question.subscribe(subscriber, subscriber.teamowner)
4088- view = create_view(question, '+portlet-subscribers-details')
4089+ view = create_view(question, "+portlet-subscribers-details")
4090 api_request = IWebServiceClientRequest(view.request)
4091
4092 expected_result = {
4093- 'subscriber': {
4094- 'name': 'team',
4095- 'display_name': 'Team Name',
4096- 'is_team': True,
4097- 'can_edit': True,
4098- 'web_link': canonical_url(subscriber),
4099- 'self_link': absoluteURL(subscriber, api_request)
4100- },
4101- 'subscription_level': "Direct",
4102- }
4103+ "subscriber": {
4104+ "name": "team",
4105+ "display_name": "Team Name",
4106+ "is_team": True,
4107+ "can_edit": True,
4108+ "web_link": canonical_url(subscriber),
4109+ "self_link": absoluteURL(subscriber, api_request),
4110+ },
4111+ "subscription_level": "Direct",
4112+ }
4113 with person_logged_in(subscriber.teamowner):
4114 self.assertEqual(
4115- [expected_result], json.loads(view.subscriber_data_js))
4116+ [expected_result], json.loads(view.subscriber_data_js)
4117+ )
4118
4119 def test_data_team_subscription_member_looks(self):
4120 # For a team subscription, subscriber_data_js has can_edit
4121@@ -155,29 +165,34 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
4122 question = self._makeQuestionWithNoSubscribers()
4123 member = self.factory.makePerson()
4124 teamowner = self.factory.makePerson(
4125- name="team-owner", displayname="Team Owner")
4126+ name="team-owner", displayname="Team Owner"
4127+ )
4128 subscriber = self.factory.makeTeam(
4129- name='team', displayname='Team Name', owner=teamowner,
4130- members=[member])
4131+ name="team",
4132+ displayname="Team Name",
4133+ owner=teamowner,
4134+ members=[member],
4135+ )
4136 with person_logged_in(subscriber.teamowner):
4137 question.subscribe(subscriber, subscriber.teamowner)
4138- view = create_view(question, '+portlet-subscribers-details')
4139+ view = create_view(question, "+portlet-subscribers-details")
4140 api_request = IWebServiceClientRequest(view.request)
4141
4142 expected_result = {
4143- 'subscriber': {
4144- 'name': 'team',
4145- 'display_name': 'Team Name',
4146- 'is_team': True,
4147- 'can_edit': True,
4148- 'web_link': canonical_url(subscriber),
4149- 'self_link': absoluteURL(subscriber, api_request)
4150- },
4151- 'subscription_level': "Direct",
4152- }
4153+ "subscriber": {
4154+ "name": "team",
4155+ "display_name": "Team Name",
4156+ "is_team": True,
4157+ "can_edit": True,
4158+ "web_link": canonical_url(subscriber),
4159+ "self_link": absoluteURL(subscriber, api_request),
4160+ },
4161+ "subscription_level": "Direct",
4162+ }
4163 with person_logged_in(subscriber.teamowner):
4164 self.assertEqual(
4165- [expected_result], json.loads(view.subscriber_data_js))
4166+ [expected_result], json.loads(view.subscriber_data_js)
4167+ )
4168
4169 def test_data_subscription_lp_admin(self):
4170 # For a subscription, subscriber_data_js has can_edit
4171@@ -185,26 +200,28 @@ class QuestionPortletSubscribersWithDetailsTests(TestCaseWithFactory):
4172 question = self._makeQuestionWithNoSubscribers()
4173 member = self.factory.makePerson()
4174 subscriber = self.factory.makePerson(
4175- name='user', displayname='Subscriber Name')
4176+ name="user", displayname="Subscriber Name"
4177+ )
4178 with person_logged_in(member):
4179 question.subscribe(subscriber, subscriber)
4180- view = create_view(question, '+portlet-subscribers-details')
4181+ view = create_view(question, "+portlet-subscribers-details")
4182 api_request = IWebServiceClientRequest(view.request)
4183
4184 expected_result = {
4185- 'subscriber': {
4186- 'name': 'user',
4187- 'display_name': 'Subscriber Name',
4188- 'is_team': False,
4189- 'can_edit': True,
4190- 'web_link': canonical_url(subscriber),
4191- 'self_link': absoluteURL(subscriber, api_request)
4192- },
4193- 'subscription_level': "Direct",
4194- }
4195+ "subscriber": {
4196+ "name": "user",
4197+ "display_name": "Subscriber Name",
4198+ "is_team": False,
4199+ "can_edit": True,
4200+ "web_link": canonical_url(subscriber),
4201+ "self_link": absoluteURL(subscriber, api_request),
4202+ },
4203+ "subscription_level": "Direct",
4204+ }
4205
4206 # Login as admin
4207 admin = getUtility(IPersonSet).find(ADMIN_EMAIL).any()
4208 with person_logged_in(admin):
4209 self.assertEqual(
4210- [expected_result], json.loads(view.subscriber_data_js))
4211+ [expected_result], json.loads(view.subscriber_data_js)
4212+ )
4213diff --git a/lib/lp/answers/browser/tests/test_questiontarget.py b/lib/lp/answers/browser/tests/test_questiontarget.py
4214index 386846f..5a289c0 100644
4215--- a/lib/lp/answers/browser/tests/test_questiontarget.py
4216+++ b/lib/lp/answers/browser/tests/test_questiontarget.py
4217@@ -7,10 +7,7 @@ import json
4218 import os
4219 from urllib.parse import quote
4220
4221-from lazr.restful.interfaces import (
4222- IJSONRequestCache,
4223- IWebServiceClientRequest,
4224- )
4225+from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
4226 from zope.component import getUtility
4227 from zope.security.proxy import removeSecurityProxy
4228 from zope.traversing.browser import absoluteURL
4229@@ -22,22 +19,12 @@ from lp.registry.interfaces.person import IPersonSet
4230 from lp.services.beautifulsoup import BeautifulSoup
4231 from lp.services.webapp import canonical_url
4232 from lp.services.worlddata.interfaces.language import ILanguageSet
4233-from lp.testing import (
4234- login_person,
4235- person_logged_in,
4236- TestCaseWithFactory,
4237- )
4238-from lp.testing.layers import (
4239- DatabaseFunctionalLayer,
4240- LaunchpadFunctionalLayer,
4241- )
4242+from lp.testing import TestCaseWithFactory, login_person, person_logged_in
4243+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
4244 from lp.testing.matchers import BrowsesWithQueryLimit
4245 from lp.testing.pages import find_tag_by_id
4246 from lp.testing.sampledata import ADMIN_EMAIL
4247-from lp.testing.views import (
4248- create_initialized_view,
4249- create_view,
4250- )
4251+from lp.testing.views import create_initialized_view, create_view
4252
4253
4254 class TestSearchQuestionsView(TestCaseWithFactory):
4255@@ -48,18 +35,19 @@ class TestSearchQuestionsView(TestCaseWithFactory):
4256 product = self.factory.makeProduct()
4257 # Avoid non-ascii character in unicode literal to not upset
4258 # pocket-lint. Bug #776389.
4259- non_ascii_string = 'portugu\xeas'
4260+ non_ascii_string = "portugu\xeas"
4261 with person_logged_in(product.owner):
4262 self.factory.makeFAQ(product, non_ascii_string)
4263 form = {
4264- 'field.search_text': non_ascii_string,
4265- 'field.status': 'OPEN',
4266- 'field.actions.search': 'Search',
4267- }
4268+ "field.search_text": non_ascii_string,
4269+ "field.status": "OPEN",
4270+ "field.actions.search": "Search",
4271+ }
4272 view = create_initialized_view(
4273- product, '+questions', form=form, method='GET')
4274+ product, "+questions", form=form, method="GET"
4275+ )
4276
4277- encoded_string = quote(non_ascii_string.encode('utf-8'))
4278+ encoded_string = quote(non_ascii_string.encode("utf-8"))
4279 # This must not raise UnicodeEncodeError.
4280 self.assertIn(encoded_string, view.matching_faqs_url)
4281
4282@@ -68,11 +56,11 @@ class TestSearchQuestionsView(TestCaseWithFactory):
4283 owner = self.factory.makePerson()
4284 distro = self.factory.makeDistribution()
4285 removeSecurityProxy(distro).official_answers = True
4286- dsp = self.factory.makeDistributionSourcePackage(
4287- distribution=distro)
4288+ dsp = self.factory.makeDistributionSourcePackage(distribution=distro)
4289 [self.factory.makeQuestion(target=dsp, owner=owner) for i in range(5)]
4290 browses_under_limit = BrowsesWithQueryLimit(
4291- 31, owner, view_name="+questions")
4292+ 31, owner, view_name="+questions"
4293+ )
4294 self.assertThat(dsp, browses_under_limit)
4295
4296
4297@@ -82,38 +70,38 @@ class TestSearchQuestionsViewCanConfigureAnswers(TestCaseWithFactory):
4298
4299 def test_cannot_configure_answers_product_no_edit_permission(self):
4300 product = self.factory.makeProduct()
4301- view = create_initialized_view(product, '+questions')
4302+ view = create_initialized_view(product, "+questions")
4303 self.assertEqual(False, view.can_configure_answers)
4304
4305 def test_can_configure_answers_product_with_edit_permission(self):
4306 product = self.factory.makeProduct()
4307 login_person(product.owner)
4308- view = create_initialized_view(product, '+questions')
4309+ view = create_initialized_view(product, "+questions")
4310 self.assertEqual(True, view.can_configure_answers)
4311
4312 def test_cannot_configure_answers_distribution_no_edit_permission(self):
4313 distribution = self.factory.makeDistribution()
4314- view = create_initialized_view(distribution, '+questions')
4315+ view = create_initialized_view(distribution, "+questions")
4316 self.assertEqual(False, view.can_configure_answers)
4317
4318 def test_can_configure_answers_distribution_with_edit_permission(self):
4319 distribution = self.factory.makeDistribution()
4320 login_person(distribution.owner)
4321- view = create_initialized_view(distribution, '+questions')
4322+ view = create_initialized_view(distribution, "+questions")
4323 self.assertEqual(True, view.can_configure_answers)
4324
4325 def test_cannot_configure_answers_projectgroup_with_edit_permission(self):
4326 # Project groups inherit Launchpad usage from their projects.
4327 project_group = self.factory.makeProject()
4328 login_person(project_group.owner)
4329- view = create_initialized_view(project_group, '+questions')
4330+ view = create_initialized_view(project_group, "+questions")
4331 self.assertEqual(False, view.can_configure_answers)
4332
4333 def test_cannot_configure_answers_dsp_with_edit_permission(self):
4334 # DSPs inherit Launchpad usage from their distribution.
4335 dsp = self.factory.makeDistributionSourcePackage()
4336 login_person(dsp.distribution.owner)
4337- view = create_initialized_view(dsp, '+questions')
4338+ view = create_initialized_view(dsp, "+questions")
4339 self.assertEqual(False, view.can_configure_answers)
4340
4341
4342@@ -123,26 +111,25 @@ class TestSearchQuestionsViewTemplate(TestCaseWithFactory):
4343 layer = DatabaseFunctionalLayer
4344
4345 def assertViewTemplate(self, context, file_name):
4346- view = create_initialized_view(context, '+questions')
4347- self.assertEqual(
4348- file_name, os.path.basename(view.template.filename))
4349+ view = create_initialized_view(context, "+questions")
4350+ self.assertEqual(file_name, os.path.basename(view.template.filename))
4351
4352 def test_template_product_answers_usage_unknown(self):
4353 product = self.factory.makeProduct()
4354- self.assertViewTemplate(product, 'unknown-support.pt')
4355+ self.assertViewTemplate(product, "unknown-support.pt")
4356
4357 def test_template_product_answers_usage_launchpad(self):
4358 product = self.factory.makeProduct()
4359 with person_logged_in(product.owner):
4360 product.answers_usage = ServiceUsage.LAUNCHPAD
4361- self.assertViewTemplate(product, 'question-listing.pt')
4362+ self.assertViewTemplate(product, "question-listing.pt")
4363
4364 def test_template_projectgroup_answers_usage_unknown(self):
4365 product = self.factory.makeProduct()
4366 project_group = self.factory.makeProject(owner=product.owner)
4367 with person_logged_in(product.owner):
4368 product.projectgroup = project_group
4369- self.assertViewTemplate(project_group, 'unknown-support.pt')
4370+ self.assertViewTemplate(project_group, "unknown-support.pt")
4371
4372 def test_template_projectgroup_answers_usage_launchpad(self):
4373 product = self.factory.makeProduct()
4374@@ -150,31 +137,31 @@ class TestSearchQuestionsViewTemplate(TestCaseWithFactory):
4375 with person_logged_in(product.owner):
4376 product.projectgroup = project_group
4377 product.answers_usage = ServiceUsage.LAUNCHPAD
4378- self.assertViewTemplate(project_group, 'question-listing.pt')
4379+ self.assertViewTemplate(project_group, "question-listing.pt")
4380
4381 def test_template_distribution_answers_usage_unknown(self):
4382 distribution = self.factory.makeDistribution()
4383- self.assertViewTemplate(distribution, 'unknown-support.pt')
4384+ self.assertViewTemplate(distribution, "unknown-support.pt")
4385
4386 def test_template_distribution_answers_usage_launchpad(self):
4387 distribution = self.factory.makeDistribution()
4388 with person_logged_in(distribution.owner):
4389 distribution.answers_usage = ServiceUsage.LAUNCHPAD
4390- self.assertViewTemplate(distribution, 'question-listing.pt')
4391+ self.assertViewTemplate(distribution, "question-listing.pt")
4392
4393 def test_template_DSP_answers_usage_unknown(self):
4394 dsp = self.factory.makeDistributionSourcePackage()
4395- self.assertViewTemplate(dsp, 'unknown-support.pt')
4396+ self.assertViewTemplate(dsp, "unknown-support.pt")
4397
4398 def test_template_DSP_answers_usage_launchpad(self):
4399 dsp = self.factory.makeDistributionSourcePackage()
4400 with person_logged_in(dsp.distribution.owner):
4401 dsp.distribution.answers_usage = ServiceUsage.LAUNCHPAD
4402- self.assertViewTemplate(dsp, 'question-listing.pt')
4403+ self.assertViewTemplate(dsp, "question-listing.pt")
4404
4405 def test_template_question_set(self):
4406 question_set = getUtility(IQuestionSet)
4407- self.assertViewTemplate(question_set, 'question-listing.pt')
4408+ self.assertViewTemplate(question_set, "question-listing.pt")
4409
4410
4411 class TestSearchQuestionsViewUnknown(TestCaseWithFactory):
4412@@ -185,43 +172,46 @@ class TestSearchQuestionsViewUnknown(TestCaseWithFactory):
4413 def linkPackage(self, product, name):
4414 # A helper to setup a legitimate Packaging link between a product
4415 # and an Ubuntu source package.
4416- hoary = getUtility(ILaunchpadCelebrities).ubuntu['hoary']
4417+ hoary = getUtility(ILaunchpadCelebrities).ubuntu["hoary"]
4418 sourcepackagename = self.factory.makeSourcePackageName(name)
4419 self.factory.makeSourcePackage(
4420- sourcepackagename=sourcepackagename, distroseries=hoary)
4421+ sourcepackagename=sourcepackagename, distroseries=hoary
4422+ )
4423 self.factory.makeSourcePackagePublishingHistory(
4424- sourcepackagename=sourcepackagename, distroseries=hoary)
4425+ sourcepackagename=sourcepackagename, distroseries=hoary
4426+ )
4427 product.development_focus.setPackaging(
4428- hoary, sourcepackagename, product.owner)
4429+ hoary, sourcepackagename, product.owner
4430+ )
4431
4432 def setUp(self):
4433 super().setUp()
4434 self.product = self.factory.makeProduct()
4435- self.view = create_initialized_view(self.product, '+questions')
4436+ self.view = create_initialized_view(self.product, "+questions")
4437
4438 def assertCommonPageElements(self, content):
4439- robots = content.find('meta', attrs={'name': 'robots'})
4440- self.assertEqual('noindex,nofollow', robots['content'])
4441- self.assertTrue(content.find(True, id='support-unknown') is not None)
4442+ robots = content.find("meta", attrs={"name": "robots"})
4443+ self.assertEqual("noindex,nofollow", robots["content"])
4444+ self.assertTrue(content.find(True, id="support-unknown") is not None)
4445
4446 def test_any_question_target_any_user(self):
4447 content = BeautifulSoup(self.view())
4448 self.assertCommonPageElements(content)
4449
4450 def test_product_with_packaging_elements(self):
4451- self.linkPackage(self.product, 'cow')
4452+ self.linkPackage(self.product, "cow")
4453 content = BeautifulSoup(self.view())
4454 self.assertCommonPageElements(content)
4455- self.assertTrue(content.find(True, id='ubuntu-support') is not None)
4456+ self.assertTrue(content.find(True, id="ubuntu-support") is not None)
4457
4458 def test_product_with_edit_permission(self):
4459 login_person(self.product.owner)
4460 self.view = create_initialized_view(
4461- self.product, '+questions', principal=self.product.owner)
4462+ self.product, "+questions", principal=self.product.owner
4463+ )
4464 content = BeautifulSoup(self.view())
4465 self.assertCommonPageElements(content)
4466- self.assertTrue(
4467- content.find(True, id='configure-support') is not None)
4468+ self.assertTrue(content.find(True, id="configure-support") is not None)
4469
4470
4471 class QuestionSetViewTestCase(TestCaseWithFactory):
4472@@ -232,19 +222,19 @@ class QuestionSetViewTestCase(TestCaseWithFactory):
4473 def test_search_questions_form_rendering(self):
4474 # The view's template directly renders the form widgets.
4475 question_set = getUtility(IQuestionSet)
4476- view = create_initialized_view(question_set, '+index')
4477- content = find_tag_by_id(view.render(), 'search-all-questions')
4478- self.assertEqual('form', content.name)
4479- self.assertIsNot(None, content.find(True, id='text'))
4480- self.assertIsNot(
4481- None, content.find(True, id='field.actions.search'))
4482- self.assertIsNot(
4483- None, content.find(True, id='field.scope.option.all'))
4484+ view = create_initialized_view(question_set, "+index")
4485+ content = find_tag_by_id(view.render(), "search-all-questions")
4486+ self.assertEqual("form", content.name)
4487+ self.assertIsNot(None, content.find(True, id="text"))
4488+ self.assertIsNot(None, content.find(True, id="field.actions.search"))
4489+ self.assertIsNot(None, content.find(True, id="field.scope.option.all"))
4490 self.assertIsNot(
4491- None, content.find(True, id='field.scope.option.project'))
4492- target_widget = view.widgets['scope'].target_widget
4493+ None, content.find(True, id="field.scope.option.project")
4494+ )
4495+ target_widget = view.widgets["scope"].target_widget
4496 self.assertIsNot(
4497- None, content.find(True, id=target_widget.show_widget_id))
4498+ None, content.find(True, id=target_widget.show_widget_id)
4499+ )
4500 text = str(content)
4501 picker_vocab = "DistributionOrProductOrProjectGroup"
4502 self.assertIn(picker_vocab, text)
4503@@ -252,25 +242,25 @@ class QuestionSetViewTestCase(TestCaseWithFactory):
4504 self.assertIn(focus_script, text)
4505
4506
4507-class QuestionTargetPortletAnswerContactsWithDetailsTests(
4508- TestCaseWithFactory):
4509+class QuestionTargetPortletAnswerContactsWithDetailsTests(TestCaseWithFactory):
4510 """Tests for IQuestionTarget:+portlet-answercontacts-details view."""
4511+
4512 layer = LaunchpadFunctionalLayer
4513
4514 def test_content_type(self):
4515 question = self.factory.makeQuestion()
4516
4517 # It works even for anonymous users, so no log-in is needed.
4518- view = create_view(question.target, '+portlet-answercontacts-details')
4519+ view = create_view(question.target, "+portlet-answercontacts-details")
4520 view.render()
4521
4522 self.assertEqual(
4523- view.request.response.getHeader('content-type'),
4524- 'application/json')
4525+ view.request.response.getHeader("content-type"), "application/json"
4526+ )
4527
4528 def test_data_no_answer_contacts(self):
4529 question = self.factory.makeQuestion()
4530- view = create_view(question.target, '+portlet-answercontacts-details')
4531+ view = create_view(question.target, "+portlet-answercontacts-details")
4532 self.assertEqual([], json.loads(view.answercontact_data_js))
4533
4534 def test_data_person_answercontact(self):
4535@@ -279,80 +269,88 @@ class QuestionTargetPortletAnswerContactsWithDetailsTests(
4536 # subscribers_list.js loading.
4537 question = self.factory.makeQuestion()
4538 contact = self.factory.makePerson(
4539- name='user', displayname='Contact Name')
4540+ name="user", displayname="Contact Name"
4541+ )
4542 with person_logged_in(contact):
4543- contact.addLanguage(getUtility(ILanguageSet)['en'])
4544+ contact.addLanguage(getUtility(ILanguageSet)["en"])
4545 question.target.addAnswerContact(contact, contact)
4546- view = create_view(question.target, '+portlet-answercontacts-details')
4547+ view = create_view(question.target, "+portlet-answercontacts-details")
4548 api_request = IWebServiceClientRequest(view.request)
4549
4550 expected_result = {
4551- 'subscriber': {
4552- 'name': 'user',
4553- 'display_name': 'Contact Name',
4554- 'is_team': False,
4555- 'can_edit': False,
4556- 'web_link': canonical_url(contact),
4557- 'self_link': absoluteURL(contact, api_request)
4558- }
4559+ "subscriber": {
4560+ "name": "user",
4561+ "display_name": "Contact Name",
4562+ "is_team": False,
4563+ "can_edit": False,
4564+ "web_link": canonical_url(contact),
4565+ "self_link": absoluteURL(contact, api_request),
4566 }
4567+ }
4568 self.assertEqual(
4569- [expected_result], json.loads(view.answercontact_data_js))
4570+ [expected_result], json.loads(view.answercontact_data_js)
4571+ )
4572
4573 def test_data_team_answer_contact(self):
4574 # For a team answer contacts, answercontact_data_js has is_team set
4575 # to true.
4576 question = self.factory.makeQuestion()
4577 teamowner = self.factory.makePerson(
4578- name="team-owner", displayname="Team Owner")
4579+ name="team-owner", displayname="Team Owner"
4580+ )
4581 contact = self.factory.makeTeam(
4582- name='team', displayname='Team Name', owner=teamowner)
4583+ name="team", displayname="Team Name", owner=teamowner
4584+ )
4585 with person_logged_in(contact.teamowner):
4586- contact.addLanguage(getUtility(ILanguageSet)['en'])
4587+ contact.addLanguage(getUtility(ILanguageSet)["en"])
4588 question.target.addAnswerContact(contact, contact)
4589- view = create_view(question.target, '+portlet-answercontacts-details')
4590+ view = create_view(question.target, "+portlet-answercontacts-details")
4591 api_request = IWebServiceClientRequest(view.request)
4592
4593 expected_result = {
4594- 'subscriber': {
4595- 'name': 'team',
4596- 'display_name': 'Team Name',
4597- 'is_team': True,
4598- 'can_edit': False,
4599- 'web_link': canonical_url(contact),
4600- 'self_link': absoluteURL(contact, api_request)
4601- }
4602+ "subscriber": {
4603+ "name": "team",
4604+ "display_name": "Team Name",
4605+ "is_team": True,
4606+ "can_edit": False,
4607+ "web_link": canonical_url(contact),
4608+ "self_link": absoluteURL(contact, api_request),
4609 }
4610+ }
4611 self.assertEqual(
4612- [expected_result], json.loads(view.answercontact_data_js))
4613+ [expected_result], json.loads(view.answercontact_data_js)
4614+ )
4615
4616 def test_data_team_answercontact_owner_looks(self):
4617 # For a team subscription, answercontact_data_js has can_edit
4618 # set to true for team owner.
4619 question = self.factory.makeQuestion()
4620 teamowner = self.factory.makePerson(
4621- name="team-owner", displayname="Team Owner")
4622+ name="team-owner", displayname="Team Owner"
4623+ )
4624 contact = self.factory.makeTeam(
4625- name='team', displayname='Team Name', owner=teamowner)
4626+ name="team", displayname="Team Name", owner=teamowner
4627+ )
4628 with person_logged_in(contact.teamowner):
4629- contact.addLanguage(getUtility(ILanguageSet)['en'])
4630+ contact.addLanguage(getUtility(ILanguageSet)["en"])
4631 question.target.addAnswerContact(contact, contact.teamowner)
4632- view = create_view(question.target, '+portlet-answercontacts-details')
4633+ view = create_view(question.target, "+portlet-answercontacts-details")
4634 api_request = IWebServiceClientRequest(view.request)
4635
4636 expected_result = {
4637- 'subscriber': {
4638- 'name': 'team',
4639- 'display_name': 'Team Name',
4640- 'is_team': True,
4641- 'can_edit': True,
4642- 'web_link': canonical_url(contact),
4643- 'self_link': absoluteURL(contact, api_request)
4644- }
4645+ "subscriber": {
4646+ "name": "team",
4647+ "display_name": "Team Name",
4648+ "is_team": True,
4649+ "can_edit": True,
4650+ "web_link": canonical_url(contact),
4651+ "self_link": absoluteURL(contact, api_request),
4652 }
4653+ }
4654 with person_logged_in(contact.teamowner):
4655 self.assertEqual(
4656- [expected_result], json.loads(view.answercontact_data_js))
4657+ [expected_result], json.loads(view.answercontact_data_js)
4658+ )
4659
4660 def test_data_team_subscription_member_looks(self):
4661 # For a team subscription, answercontact_data_js has can_edit
4662@@ -360,55 +358,62 @@ class QuestionTargetPortletAnswerContactsWithDetailsTests(
4663 question = self.factory.makeQuestion()
4664 member = self.factory.makePerson()
4665 teamowner = self.factory.makePerson(
4666- name="team-owner", displayname="Team Owner")
4667+ name="team-owner", displayname="Team Owner"
4668+ )
4669 contact = self.factory.makeTeam(
4670- name='team', displayname='Team Name', owner=teamowner,
4671- members=[member])
4672+ name="team",
4673+ displayname="Team Name",
4674+ owner=teamowner,
4675+ members=[member],
4676+ )
4677 with person_logged_in(contact.teamowner):
4678- contact.addLanguage(getUtility(ILanguageSet)['en'])
4679+ contact.addLanguage(getUtility(ILanguageSet)["en"])
4680 question.target.addAnswerContact(contact, contact.teamowner)
4681- view = create_view(question.target, '+portlet-answercontacts-details')
4682+ view = create_view(question.target, "+portlet-answercontacts-details")
4683 api_request = IWebServiceClientRequest(view.request)
4684
4685 expected_result = {
4686- 'subscriber': {
4687- 'name': 'team',
4688- 'display_name': 'Team Name',
4689- 'is_team': True,
4690- 'can_edit': True,
4691- 'web_link': canonical_url(contact),
4692- 'self_link': absoluteURL(contact, api_request)
4693- }
4694+ "subscriber": {
4695+ "name": "team",
4696+ "display_name": "Team Name",
4697+ "is_team": True,
4698+ "can_edit": True,
4699+ "web_link": canonical_url(contact),
4700+ "self_link": absoluteURL(contact, api_request),
4701 }
4702+ }
4703 with person_logged_in(contact.teamowner):
4704 self.assertEqual(
4705- [expected_result], json.loads(view.answercontact_data_js))
4706+ [expected_result], json.loads(view.answercontact_data_js)
4707+ )
4708
4709 def test_data_target_owner_answercontact_looks(self):
4710 # Answercontact_data_js has can_edit set to true for target owner.
4711 distro = self.factory.makeDistribution()
4712 question = self.factory.makeQuestion(target=distro)
4713 contact = self.factory.makePerson(
4714- name='user', displayname='Contact Name')
4715+ name="user", displayname="Contact Name"
4716+ )
4717 with person_logged_in(contact):
4718- contact.addLanguage(getUtility(ILanguageSet)['en'])
4719+ contact.addLanguage(getUtility(ILanguageSet)["en"])
4720 question.target.addAnswerContact(contact, contact)
4721- view = create_view(question.target, '+portlet-answercontacts-details')
4722+ view = create_view(question.target, "+portlet-answercontacts-details")
4723 api_request = IWebServiceClientRequest(view.request)
4724
4725 expected_result = {
4726- 'subscriber': {
4727- 'name': 'user',
4728- 'display_name': 'Contact Name',
4729- 'is_team': False,
4730- 'can_edit': True,
4731- 'web_link': canonical_url(contact),
4732- 'self_link': absoluteURL(contact, api_request)
4733- }
4734+ "subscriber": {
4735+ "name": "user",
4736+ "display_name": "Contact Name",
4737+ "is_team": False,
4738+ "can_edit": True,
4739+ "web_link": canonical_url(contact),
4740+ "self_link": absoluteURL(contact, api_request),
4741 }
4742+ }
4743 with person_logged_in(distro.owner):
4744 self.assertEqual(
4745- [expected_result], json.loads(view.answercontact_data_js))
4746+ [expected_result], json.loads(view.answercontact_data_js)
4747+ )
4748
4749 def test_data_subscription_lp_admin(self):
4750 # For a subscription, answercontact_data_js has can_edit
4751@@ -416,34 +421,37 @@ class QuestionTargetPortletAnswerContactsWithDetailsTests(
4752 question = self.factory.makeQuestion()
4753 member = self.factory.makePerson()
4754 contact = self.factory.makePerson(
4755- name='user', displayname='Contact Name')
4756+ name="user", displayname="Contact Name"
4757+ )
4758 with person_logged_in(contact):
4759- contact.addLanguage(getUtility(ILanguageSet)['en'])
4760+ contact.addLanguage(getUtility(ILanguageSet)["en"])
4761 with person_logged_in(member):
4762 question.target.addAnswerContact(contact, contact)
4763- view = create_view(question.target, '+portlet-answercontacts-details')
4764+ view = create_view(question.target, "+portlet-answercontacts-details")
4765 api_request = IWebServiceClientRequest(view.request)
4766
4767 expected_result = {
4768- 'subscriber': {
4769- 'name': 'user',
4770- 'display_name': 'Contact Name',
4771- 'is_team': False,
4772- 'can_edit': True,
4773- 'web_link': canonical_url(contact),
4774- 'self_link': absoluteURL(contact, api_request)
4775- }
4776+ "subscriber": {
4777+ "name": "user",
4778+ "display_name": "Contact Name",
4779+ "is_team": False,
4780+ "can_edit": True,
4781+ "web_link": canonical_url(contact),
4782+ "self_link": absoluteURL(contact, api_request),
4783 }
4784+ }
4785
4786 # Login as admin
4787 admin = getUtility(IPersonSet).find(ADMIN_EMAIL).any()
4788 with person_logged_in(admin):
4789 self.assertEqual(
4790- [expected_result], json.loads(view.answercontact_data_js))
4791+ [expected_result], json.loads(view.answercontact_data_js)
4792+ )
4793
4794
4795 class TestQuestionTargetPortletAnswerContacts(TestCaseWithFactory):
4796 """Tests for IQuestionTarget:+portlet-answercontacts."""
4797+
4798 layer = LaunchpadFunctionalLayer
4799
4800 def test_jsoncache_contents(self):
4801@@ -453,13 +461,16 @@ class TestQuestionTargetPortletAnswerContacts(TestCaseWithFactory):
4802
4803 # It works even for anonymous users, so no log-in is needed.
4804 view = create_initialized_view(
4805- question.target, '+portlet-answercontacts', rootsite='answers')
4806+ question.target, "+portlet-answercontacts", rootsite="answers"
4807+ )
4808
4809 cache = IJSONRequestCache(view.request).objects
4810 context_url_data = {
4811- 'web_link': canonical_url(product, rootsite='mainsite'),
4812- 'self_link': absoluteURL(product,
4813- IWebServiceClientRequest(view.request)),
4814- }
4815- self.assertEqual(cache[product.name + '_answer_portlet_url_data'],
4816- context_url_data)
4817+ "web_link": canonical_url(product, rootsite="mainsite"),
4818+ "self_link": absoluteURL(
4819+ product, IWebServiceClientRequest(view.request)
4820+ ),
4821+ }
4822+ self.assertEqual(
4823+ cache[product.name + "_answer_portlet_url_data"], context_url_data
4824+ )
4825diff --git a/lib/lp/answers/browser/tests/test_views.py b/lib/lp/answers/browser/tests/test_views.py
4826index eeee41a..91ab009 100644
4827--- a/lib/lp/answers/browser/tests/test_views.py
4828+++ b/lib/lp/answers/browser/tests/test_views.py
4829@@ -9,11 +9,7 @@ import unittest
4830
4831 from lp.testing import BrowserTestCase
4832 from lp.testing.layers import DatabaseFunctionalLayer
4833-from lp.testing.systemdocs import (
4834- LayeredDocFileSuite,
4835- setUp,
4836- tearDown,
4837- )
4838+from lp.testing.systemdocs import LayeredDocFileSuite, setUp, tearDown
4839
4840
4841 class TestEmailObfuscated(BrowserTestCase):
4842@@ -24,22 +20,26 @@ class TestEmailObfuscated(BrowserTestCase):
4843 def getBrowserForQuestionWithEmail(self, email_address, no_login):
4844 question = self.factory.makeQuestion(
4845 title="Title with %s contained" % email_address,
4846- description="Description with %s contained." % email_address)
4847+ description="Description with %s contained." % email_address,
4848+ )
4849 return self.getViewBrowser(
4850- question, rootsite="answers", no_login=no_login)
4851+ question, rootsite="answers", no_login=no_login
4852+ )
4853
4854 def test_user_sees_email_address(self):
4855 """A logged-in user can see the email address on the page."""
4856 email_address = "mark@example.com"
4857 browser = self.getBrowserForQuestionWithEmail(
4858- email_address, no_login=False)
4859+ email_address, no_login=False
4860+ )
4861 self.assertEqual(4, browser.contents.count(email_address))
4862
4863 def test_anonymous_sees_not_email_address(self):
4864 """The anonymous user cannot see the email address on the page."""
4865 email_address = "mark@example.com"
4866 browser = self.getBrowserForQuestionWithEmail(
4867- email_address, no_login=True)
4868+ email_address, no_login=True
4869+ )
4870 self.assertEqual(0, browser.contents.count(email_address))
4871
4872
4873@@ -47,13 +47,28 @@ def test_suite():
4874 suite = unittest.TestSuite()
4875 loader = unittest.TestLoader()
4876 suite.addTest(loader.loadTestsFromTestCase(TestEmailObfuscated))
4877- suite.addTest(LayeredDocFileSuite(
4878- 'question-subscribe_me.txt', setUp=setUp, tearDown=tearDown,
4879- layer=DatabaseFunctionalLayer))
4880- suite.addTest(LayeredDocFileSuite(
4881- 'views.txt', setUp=setUp, tearDown=tearDown,
4882- layer=DatabaseFunctionalLayer))
4883- suite.addTest(LayeredDocFileSuite(
4884- 'faq-views.txt', setUp=setUp, tearDown=tearDown,
4885- layer=DatabaseFunctionalLayer))
4886+ suite.addTest(
4887+ LayeredDocFileSuite(
4888+ "question-subscribe_me.txt",
4889+ setUp=setUp,
4890+ tearDown=tearDown,
4891+ layer=DatabaseFunctionalLayer,
4892+ )
4893+ )
4894+ suite.addTest(
4895+ LayeredDocFileSuite(
4896+ "views.txt",
4897+ setUp=setUp,
4898+ tearDown=tearDown,
4899+ layer=DatabaseFunctionalLayer,
4900+ )
4901+ )
4902+ suite.addTest(
4903+ LayeredDocFileSuite(
4904+ "faq-views.txt",
4905+ setUp=setUp,
4906+ tearDown=tearDown,
4907+ layer=DatabaseFunctionalLayer,
4908+ )
4909+ )
4910 return suite
4911diff --git a/pyproject.toml b/pyproject.toml
4912new file mode 100644
4913index 0000000..1f331da
4914--- /dev/null
4915+++ b/pyproject.toml
4916@@ -0,0 +1,3 @@
4917+[tool.black]
4918+line-length = 79
4919+target-version = ['py35']
4920diff --git a/setup.cfg b/setup.cfg
4921index db4af9b..fe3f8eb 100644
4922--- a/setup.cfg
4923+++ b/setup.cfg
4924@@ -197,7 +197,6 @@ exclude =
4925 # Code here is imported from elsewhere and may not necessarily conform
4926 # to Launchpad's style.
4927 lib/contrib
4928-hang-closing = true
4929 ignore =
4930 # Skip all the pure whitespace issues for now. There are too many of
4931 # them to be worth fixing manually, and most of them will get sorted out
4932@@ -209,6 +208,7 @@ ignore =
4933 E117,
4934 E121,
4935 E122,
4936+ E123,
4937 E124,
4938 E125,
4939 E126,
4940@@ -249,15 +249,8 @@ ignore =
4941 W504
4942
4943 [isort]
4944-combine_as_imports = true
4945-force_grid_wrap = 2
4946-force_sort_within_sections = true
4947-include_trailing_comma = true
4948 # database/* have some implicit relative imports.
4949 known_first_party = canonical,lp,launchpad_loggerhead,devscripts,fti,replication,preflight,security,upgrade,dbcontroller
4950 known_pythonpath = _pythonpath
4951-line_length = 78
4952-lines_after_imports = 2
4953-multi_line_output = 8
4954-order_by_type = false
4955+line_length = 79
4956 sections = FUTURE,PYTHONPATH,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER

Subscribers

People subscribed via source and target branches

to status/vote changes: