Merge lp:~wgrant/launchpad/unify-person-questions into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 18208
Proposed branch: lp:~wgrant/launchpad/unify-person-questions
Merge into: lp:launchpad
Diff against target: 676 lines (+288/-267)
3 files modified
lib/lp/answers/browser/configure.zcml (+10/-10)
lib/lp/answers/browser/person.py (+278/-0)
lib/lp/registry/browser/person.py (+0/-257)
To merge this branch: bzr merge lp:~wgrant/launchpad/unify-person-questions
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+306685@code.launchpad.net

Commit message

Move Person questions views from lp.registry to lp.answers.

Description of the change

Move Person questions views from lp.registry to lp.answers.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/browser/configure.zcml'
2--- lib/lp/answers/browser/configure.zcml 2014-04-24 02:53:05 +0000
3+++ lib/lp/answers/browser/configure.zcml 2016-09-24 06:34:14 +0000
4@@ -385,14 +385,14 @@
5 />
6 <browser:page
7 for="lp.registry.interfaces.person.IPerson"
8- class="lp.registry.browser.person.PersonLatestQuestionsView"
9+ class="lp.answers.browser.person.PersonLatestQuestionsView"
10 name="+portlet-latestquestions"
11 permission="zope.Public"
12 template="../templates/questiontarget-portlet-latestquestions.pt"
13 />
14 <browser:page
15 for="lp.registry.interfaces.person.IPerson"
16- class="lp.registry.browser.person.PersonSearchQuestionsView"
17+ class="lp.answers.browser.person.PersonSearchQuestionsView"
18 name="+questions"
19 permission="zope.Public"
20 />
21@@ -404,7 +404,7 @@
22 />
23 <browser:page
24 for="lp.registry.interfaces.person.IPerson"
25- class="lp.registry.browser.person.SearchAnsweredQuestionsView"
26+ class="lp.answers.browser.person.SearchAnsweredQuestionsView"
27 name="+answeredquestions"
28 permission="zope.Public"
29 />
30@@ -416,7 +416,7 @@
31 />
32 <browser:page
33 for="lp.registry.interfaces.person.IPerson"
34- class="lp.registry.browser.person.SearchAssignedQuestionsView"
35+ class="lp.answers.browser.person.SearchAssignedQuestionsView"
36 name="+assignedquestions"
37 permission="zope.Public"
38 />
39@@ -428,7 +428,7 @@
40 />
41 <browser:page
42 for="lp.registry.interfaces.person.IPerson"
43- class="lp.registry.browser.person.SearchCommentedQuestionsView"
44+ class="lp.answers.browser.person.SearchCommentedQuestionsView"
45 name="+commentedquestions"
46 permission="zope.Public"
47 />
48@@ -440,7 +440,7 @@
49 />
50 <browser:page
51 for="lp.registry.interfaces.person.IPerson"
52- class="lp.registry.browser.person.SearchCreatedQuestionsView"
53+ class="lp.answers.browser.person.SearchCreatedQuestionsView"
54 name="+createdquestions"
55 permission="zope.Public"
56 />
57@@ -452,7 +452,7 @@
58 />
59 <browser:page
60 for="lp.registry.interfaces.person.IPerson"
61- class="lp.registry.browser.person.SearchNeedAttentionQuestionsView"
62+ class="lp.answers.browser.person.SearchNeedAttentionQuestionsView"
63 name="+needattentionquestions"
64 permission="zope.Public"
65 />
66@@ -464,7 +464,7 @@
67 />
68 <browser:page
69 for="lp.registry.interfaces.person.IPerson"
70- class="lp.registry.browser.person.SearchSubscribedQuestionsView"
71+ class="lp.answers.browser.person.SearchSubscribedQuestionsView"
72 name="+subscribedquestions"
73 permission="zope.Public"
74 />
75@@ -476,13 +476,13 @@
76 />
77 <browser:page
78 for="lp.registry.interfaces.person.IPerson"
79- class="lp.registry.browser.person.PersonAnswerContactForView"
80+ class="lp.answers.browser.person.PersonAnswerContactForView"
81 name="+answer-contact-for"
82 permission="zope.Public"
83 template="../templates/person-answer-contact-for.pt"
84 />
85 <browser:menus
86- module="lp.registry.browser.person"
87+ module="lp.answers.browser.person"
88 classes="PersonAnswersMenu"
89 />
90 <browser:page
91
92=== added file 'lib/lp/answers/browser/person.py'
93--- lib/lp/answers/browser/person.py 1970-01-01 00:00:00 +0000
94+++ lib/lp/answers/browser/person.py 2016-09-24 06:34:14 +0000
95@@ -0,0 +1,278 @@
96+# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
97+# GNU Affero General Public License version 3 (see the file LICENSE).
98+
99+"""Person-related answer listing classes."""
100+
101+__metaclass__ = type
102+__all__ = [
103+ 'PersonAnswerContactForView',
104+ 'PersonAnswersMenu',
105+ 'PersonLatestQuestionsView',
106+ 'PersonSearchQuestionsView',
107+ 'SearchAnsweredQuestionsView',
108+ 'SearchAssignedQuestionsView',
109+ 'SearchCommentedQuestionsView',
110+ 'SearchCreatedQuestionsView',
111+ 'SearchNeedAttentionQuestionsView',
112+ 'SearchSubscribedQuestionsView',
113+ ]
114+
115+
116+from operator import attrgetter
117+
118+from lp import _
119+from lp.answers.browser.questiontarget import SearchQuestionsView
120+from lp.answers.enums import QuestionParticipation
121+from lp.answers.interfaces.questionsperson import IQuestionsPerson
122+from lp.app.browser.launchpadform import LaunchpadFormView
123+from lp.registry.interfaces.person import IPerson
124+from lp.services.propertycache import cachedproperty
125+from lp.services.webapp import (
126+ Link,
127+ NavigationMenu,
128+ )
129+from lp.services.webapp.publisher import LaunchpadView
130+
131+
132+class PersonLatestQuestionsView(LaunchpadFormView):
133+ """View used by the porlet displaying the latest questions made by
134+ a person.
135+ """
136+
137+ @cachedproperty
138+ def getLatestQuestions(self, quantity=5):
139+ """Return <quantity> latest questions created for this target. """
140+ return IQuestionsPerson(self.context).searchQuestions(
141+ participation=QuestionParticipation.OWNER)[:quantity]
142+
143+
144+class PersonSearchQuestionsView(SearchQuestionsView):
145+ """View to search and display questions that involve an `IPerson`."""
146+
147+ display_target_column = True
148+
149+ @property
150+ def template(self):
151+ # Persons always show the default template.
152+ return self.default_template
153+
154+ @property
155+ def pageheading(self):
156+ """See `SearchQuestionsView`."""
157+ return _('Questions involving $name',
158+ mapping=dict(name=self.context.displayname))
159+
160+ @property
161+ def empty_listing_message(self):
162+ """See `SearchQuestionsView`."""
163+ return _('No questions involving $name found with the '
164+ 'requested statuses.',
165+ mapping=dict(name=self.context.displayname))
166+
167+
168+class SearchAnsweredQuestionsView(PersonSearchQuestionsView):
169+ """View used to search and display questions answered by an IPerson."""
170+
171+ def getDefaultFilter(self):
172+ """See `SearchQuestionsView`."""
173+ return dict(participation=QuestionParticipation.ANSWERER)
174+
175+ @property
176+ def pageheading(self):
177+ """See `SearchQuestionsView`."""
178+ return _('Questions answered by $name',
179+ mapping=dict(name=self.context.displayname))
180+
181+ @property
182+ def empty_listing_message(self):
183+ """See `SearchQuestionsView`."""
184+ return _('No questions answered by $name found with the '
185+ 'requested statuses.',
186+ mapping=dict(name=self.context.displayname))
187+
188+
189+class SearchAssignedQuestionsView(PersonSearchQuestionsView):
190+ """View used to search and display questions assigned to an IPerson."""
191+
192+ def getDefaultFilter(self):
193+ """See `SearchQuestionsView`."""
194+ return dict(participation=QuestionParticipation.ASSIGNEE)
195+
196+ @property
197+ def pageheading(self):
198+ """See `SearchQuestionsView`."""
199+ return _('Questions assigned to $name',
200+ mapping=dict(name=self.context.displayname))
201+
202+ @property
203+ def empty_listing_message(self):
204+ """See `SearchQuestionsView`."""
205+ return _('No questions assigned to $name found with the '
206+ 'requested statuses.',
207+ mapping=dict(name=self.context.displayname))
208+
209+
210+class SearchCommentedQuestionsView(PersonSearchQuestionsView):
211+ """View used to search and show questions commented on by an IPerson."""
212+
213+ def getDefaultFilter(self):
214+ """See `SearchQuestionsView`."""
215+ return dict(participation=QuestionParticipation.COMMENTER)
216+
217+ @property
218+ def pageheading(self):
219+ """See `SearchQuestionsView`."""
220+ return _('Questions commented on by $name ',
221+ mapping=dict(name=self.context.displayname))
222+
223+ @property
224+ def empty_listing_message(self):
225+ """See `SearchQuestionsView`."""
226+ return _('No questions commented on by $name found with the '
227+ 'requested statuses.',
228+ mapping=dict(name=self.context.displayname))
229+
230+
231+class SearchCreatedQuestionsView(PersonSearchQuestionsView):
232+ """View used to search and display questions created by an IPerson."""
233+
234+ def getDefaultFilter(self):
235+ """See `SearchQuestionsView`."""
236+ return dict(participation=QuestionParticipation.OWNER)
237+
238+ @property
239+ def pageheading(self):
240+ """See `SearchQuestionsView`."""
241+ return _('Questions asked by $name',
242+ mapping=dict(name=self.context.displayname))
243+
244+ @property
245+ def empty_listing_message(self):
246+ """See `SearchQuestionsView`."""
247+ return _('No questions asked by $name found with the '
248+ 'requested statuses.',
249+ mapping=dict(name=self.context.displayname))
250+
251+
252+class SearchNeedAttentionQuestionsView(PersonSearchQuestionsView):
253+ """View used to search and show questions needing an IPerson attention."""
254+
255+ def getDefaultFilter(self):
256+ """See `SearchQuestionsView`."""
257+ return dict(needs_attention=True)
258+
259+ @property
260+ def pageheading(self):
261+ """See `SearchQuestionsView`."""
262+ return _("Questions needing $name's attention",
263+ mapping=dict(name=self.context.displayname))
264+
265+ @property
266+ def empty_listing_message(self):
267+ """See `SearchQuestionsView`."""
268+ return _("No questions need $name's attention.",
269+ mapping=dict(name=self.context.displayname))
270+
271+
272+class SearchSubscribedQuestionsView(PersonSearchQuestionsView):
273+ """View used to search and show questions subscribed to by an IPerson."""
274+
275+ def getDefaultFilter(self):
276+ """See `SearchQuestionsView`."""
277+ return dict(participation=QuestionParticipation.SUBSCRIBER)
278+
279+ @property
280+ def pageheading(self):
281+ """See `SearchQuestionsView`."""
282+ return _('Questions $name is subscribed to',
283+ mapping=dict(name=self.context.displayname))
284+
285+ @property
286+ def empty_listing_message(self):
287+ """See `SearchQuestionsView`."""
288+ return _('No questions subscribed to by $name found with the '
289+ 'requested statuses.',
290+ mapping=dict(name=self.context.displayname))
291+
292+
293+class PersonAnswerContactForView(LaunchpadView):
294+ """View used to show all the IQuestionTargets that an IPerson is an answer
295+ contact for.
296+ """
297+
298+ @property
299+ def label(self):
300+ return 'Projects for which %s is an answer contact' % (
301+ self.context.displayname)
302+
303+ page_title = label
304+
305+ @cachedproperty
306+ def direct_question_targets(self):
307+ """List of targets that the IPerson is a direct answer contact.
308+
309+ Return a list of IQuestionTargets sorted alphabetically by title.
310+ """
311+ return sorted(
312+ IQuestionsPerson(self.context).getDirectAnswerQuestionTargets(),
313+ key=attrgetter('title'))
314+
315+ @cachedproperty
316+ def team_question_targets(self):
317+ """List of IQuestionTargets for the context's team membership.
318+
319+ Sorted alphabetically by title.
320+ """
321+ return sorted(
322+ IQuestionsPerson(self.context).getTeamAnswerQuestionTargets(),
323+ key=attrgetter('title'))
324+
325+ def showRemoveYourselfLink(self):
326+ """The link is shown when the page is in the user's own profile."""
327+ return self.user == self.context
328+
329+
330+class PersonAnswersMenu(NavigationMenu):
331+
332+ usedfor = IPerson
333+ facet = 'answers'
334+ links = ['answered', 'assigned', 'created', 'commented', 'need_attention',
335+ 'subscribed', 'answer_contact_for']
336+
337+ def answer_contact_for(self):
338+ summary = "Projects for which %s is an answer contact" % (
339+ self.context.displayname)
340+ return Link(
341+ '+answer-contact-for', 'Answer contact for', summary, icon='edit')
342+
343+ def answered(self):
344+ summary = 'Questions answered by %s' % self.context.displayname
345+ return Link(
346+ '+answeredquestions', 'Answered', summary, icon='question')
347+
348+ def assigned(self):
349+ summary = 'Questions assigned to %s' % self.context.displayname
350+ return Link(
351+ '+assignedquestions', 'Assigned', summary, icon='question')
352+
353+ def created(self):
354+ summary = 'Questions asked by %s' % self.context.displayname
355+ return Link('+createdquestions', 'Asked', summary, icon='question')
356+
357+ def commented(self):
358+ summary = 'Questions commented on by %s' % (
359+ self.context.displayname)
360+ return Link(
361+ '+commentedquestions', 'Commented', summary, icon='question')
362+
363+ def need_attention(self):
364+ summary = 'Questions needing %s attention' % (
365+ self.context.displayname)
366+ return Link('+needattentionquestions', 'Need attention', summary,
367+ icon='question')
368+
369+ def subscribed(self):
370+ text = 'Subscribed'
371+ summary = 'Questions subscribed to by %s' % (
372+ self.context.displayname)
373+ return Link('+subscribedquestions', text, summary, icon='question')
374
375=== modified file 'lib/lp/registry/browser/person.py'
376--- lib/lp/registry/browser/person.py 2016-07-28 00:26:13 +0000
377+++ lib/lp/registry/browser/person.py 2016-09-24 06:34:14 +0000
378@@ -11,8 +11,6 @@
379 'PeopleSearchView',
380 'PersonAccountAdministerView',
381 'PersonAdministerView',
382- 'PersonAnswerContactForView',
383- 'PersonAnswersMenu',
384 'PersonBrandingView',
385 'PersonBreadcrumb',
386 'PersonCodeOfConductEditView',
387@@ -29,7 +27,6 @@
388 'PersonIndexView',
389 'PersonKarmaView',
390 'PersonLanguagesView',
391- 'PersonLatestQuestionsView',
392 'PersonNavigation',
393 'PersonOAuthTokensView',
394 'PersonOverviewMenu',
395@@ -38,7 +35,6 @@
396 'PersonRdfView',
397 'PersonRelatedSoftwareView',
398 'PersonRenameFormMixin',
399- 'PersonSearchQuestionsView',
400 'PersonSetActionNavigationMenu',
401 'PersonSetContextMenu',
402 'PersonSetNavigation',
403@@ -47,12 +43,6 @@
404 'PPANavigationMenuMixIn',
405 'RedirectToEditLanguagesView',
406 'RestrictedMembershipsPersonView',
407- 'SearchAnsweredQuestionsView',
408- 'SearchAssignedQuestionsView',
409- 'SearchCommentedQuestionsView',
410- 'SearchCreatedQuestionsView',
411- 'SearchNeedAttentionQuestionsView',
412- 'SearchSubscribedQuestionsView',
413 'archive_to_person',
414 ]
415
416@@ -109,9 +99,6 @@
417 from zope.security.proxy import removeSecurityProxy
418
419 from lp import _
420-from lp.answers.browser.questiontarget import SearchQuestionsView
421-from lp.answers.enums import QuestionParticipation
422-from lp.answers.interfaces.questionsperson import IQuestionsPerson
423 from lp.app.browser.launchpadform import (
424 action,
425 custom_widget,
426@@ -3320,250 +3307,6 @@
427 self.next_url = self.action_url
428
429
430-class PersonLatestQuestionsView(LaunchpadFormView):
431- """View used by the porlet displaying the latest questions made by
432- a person.
433- """
434-
435- @cachedproperty
436- def getLatestQuestions(self, quantity=5):
437- """Return <quantity> latest questions created for this target. """
438- return IQuestionsPerson(self.context).searchQuestions(
439- participation=QuestionParticipation.OWNER)[:quantity]
440-
441-
442-class PersonSearchQuestionsView(SearchQuestionsView):
443- """View to search and display questions that involve an `IPerson`."""
444-
445- display_target_column = True
446-
447- @property
448- def template(self):
449- # Persons always show the default template.
450- return self.default_template
451-
452- @property
453- def pageheading(self):
454- """See `SearchQuestionsView`."""
455- return _('Questions involving $name',
456- mapping=dict(name=self.context.displayname))
457-
458- @property
459- def empty_listing_message(self):
460- """See `SearchQuestionsView`."""
461- return _('No questions involving $name found with the '
462- 'requested statuses.',
463- mapping=dict(name=self.context.displayname))
464-
465-
466-class SearchAnsweredQuestionsView(PersonSearchQuestionsView):
467- """View used to search and display questions answered by an IPerson."""
468-
469- def getDefaultFilter(self):
470- """See `SearchQuestionsView`."""
471- return dict(participation=QuestionParticipation.ANSWERER)
472-
473- @property
474- def pageheading(self):
475- """See `SearchQuestionsView`."""
476- return _('Questions answered by $name',
477- mapping=dict(name=self.context.displayname))
478-
479- @property
480- def empty_listing_message(self):
481- """See `SearchQuestionsView`."""
482- return _('No questions answered by $name found with the '
483- 'requested statuses.',
484- mapping=dict(name=self.context.displayname))
485-
486-
487-class SearchAssignedQuestionsView(PersonSearchQuestionsView):
488- """View used to search and display questions assigned to an IPerson."""
489-
490- def getDefaultFilter(self):
491- """See `SearchQuestionsView`."""
492- return dict(participation=QuestionParticipation.ASSIGNEE)
493-
494- @property
495- def pageheading(self):
496- """See `SearchQuestionsView`."""
497- return _('Questions assigned to $name',
498- mapping=dict(name=self.context.displayname))
499-
500- @property
501- def empty_listing_message(self):
502- """See `SearchQuestionsView`."""
503- return _('No questions assigned to $name found with the '
504- 'requested statuses.',
505- mapping=dict(name=self.context.displayname))
506-
507-
508-class SearchCommentedQuestionsView(PersonSearchQuestionsView):
509- """View used to search and show questions commented on by an IPerson."""
510-
511- def getDefaultFilter(self):
512- """See `SearchQuestionsView`."""
513- return dict(participation=QuestionParticipation.COMMENTER)
514-
515- @property
516- def pageheading(self):
517- """See `SearchQuestionsView`."""
518- return _('Questions commented on by $name ',
519- mapping=dict(name=self.context.displayname))
520-
521- @property
522- def empty_listing_message(self):
523- """See `SearchQuestionsView`."""
524- return _('No questions commented on by $name found with the '
525- 'requested statuses.',
526- mapping=dict(name=self.context.displayname))
527-
528-
529-class SearchCreatedQuestionsView(PersonSearchQuestionsView):
530- """View used to search and display questions created by an IPerson."""
531-
532- def getDefaultFilter(self):
533- """See `SearchQuestionsView`."""
534- return dict(participation=QuestionParticipation.OWNER)
535-
536- @property
537- def pageheading(self):
538- """See `SearchQuestionsView`."""
539- return _('Questions asked by $name',
540- mapping=dict(name=self.context.displayname))
541-
542- @property
543- def empty_listing_message(self):
544- """See `SearchQuestionsView`."""
545- return _('No questions asked by $name found with the '
546- 'requested statuses.',
547- mapping=dict(name=self.context.displayname))
548-
549-
550-class SearchNeedAttentionQuestionsView(PersonSearchQuestionsView):
551- """View used to search and show questions needing an IPerson attention."""
552-
553- def getDefaultFilter(self):
554- """See `SearchQuestionsView`."""
555- return dict(needs_attention=True)
556-
557- @property
558- def pageheading(self):
559- """See `SearchQuestionsView`."""
560- return _("Questions needing $name's attention",
561- mapping=dict(name=self.context.displayname))
562-
563- @property
564- def empty_listing_message(self):
565- """See `SearchQuestionsView`."""
566- return _("No questions need $name's attention.",
567- mapping=dict(name=self.context.displayname))
568-
569-
570-class SearchSubscribedQuestionsView(PersonSearchQuestionsView):
571- """View used to search and show questions subscribed to by an IPerson."""
572-
573- def getDefaultFilter(self):
574- """See `SearchQuestionsView`."""
575- return dict(participation=QuestionParticipation.SUBSCRIBER)
576-
577- @property
578- def pageheading(self):
579- """See `SearchQuestionsView`."""
580- return _('Questions $name is subscribed to',
581- mapping=dict(name=self.context.displayname))
582-
583- @property
584- def empty_listing_message(self):
585- """See `SearchQuestionsView`."""
586- return _('No questions subscribed to by $name found with the '
587- 'requested statuses.',
588- mapping=dict(name=self.context.displayname))
589-
590-
591-class PersonAnswerContactForView(LaunchpadView):
592- """View used to show all the IQuestionTargets that an IPerson is an answer
593- contact for.
594- """
595-
596- @property
597- def label(self):
598- return 'Projects for which %s is an answer contact' % (
599- self.context.displayname)
600-
601- page_title = label
602-
603- @cachedproperty
604- def direct_question_targets(self):
605- """List of targets that the IPerson is a direct answer contact.
606-
607- Return a list of IQuestionTargets sorted alphabetically by title.
608- """
609- return sorted(
610- IQuestionsPerson(self.context).getDirectAnswerQuestionTargets(),
611- key=attrgetter('title'))
612-
613- @cachedproperty
614- def team_question_targets(self):
615- """List of IQuestionTargets for the context's team membership.
616-
617- Sorted alphabetically by title.
618- """
619- return sorted(
620- IQuestionsPerson(self.context).getTeamAnswerQuestionTargets(),
621- key=attrgetter('title'))
622-
623- def showRemoveYourselfLink(self):
624- """The link is shown when the page is in the user's own profile."""
625- return self.user == self.context
626-
627-
628-class PersonAnswersMenu(NavigationMenu):
629-
630- usedfor = IPerson
631- facet = 'answers'
632- links = ['answered', 'assigned', 'created', 'commented', 'need_attention',
633- 'subscribed', 'answer_contact_for']
634-
635- def answer_contact_for(self):
636- summary = "Projects for which %s is an answer contact" % (
637- self.context.displayname)
638- return Link(
639- '+answer-contact-for', 'Answer contact for', summary, icon='edit')
640-
641- def answered(self):
642- summary = 'Questions answered by %s' % self.context.displayname
643- return Link(
644- '+answeredquestions', 'Answered', summary, icon='question')
645-
646- def assigned(self):
647- summary = 'Questions assigned to %s' % self.context.displayname
648- return Link(
649- '+assignedquestions', 'Assigned', summary, icon='question')
650-
651- def created(self):
652- summary = 'Questions asked by %s' % self.context.displayname
653- return Link('+createdquestions', 'Asked', summary, icon='question')
654-
655- def commented(self):
656- summary = 'Questions commented on by %s' % (
657- self.context.displayname)
658- return Link(
659- '+commentedquestions', 'Commented', summary, icon='question')
660-
661- def need_attention(self):
662- summary = 'Questions needing %s attention' % (
663- self.context.displayname)
664- return Link('+needattentionquestions', 'Need attention', summary,
665- icon='question')
666-
667- def subscribed(self):
668- text = 'Subscribed'
669- summary = 'Questions subscribed to by %s' % (
670- self.context.displayname)
671- return Link('+subscribedquestions', text, summary, icon='question')
672-
673-
674 class BaseWithStats:
675 """An ISourcePackageRelease or a ISourcePackagePublishingHistory,
676 with extra stats added.