Merge lp:~salgado/launchpad/person-upcoming-work-view into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 15070
Proposed branch: lp:~salgado/launchpad/person-upcoming-work-view
Merge into: lp:launchpad
Prerequisite: lp:~linaro-infrastructure/launchpad/upcoming-work-progress-bars
Diff against target: 782 lines (+338/-302)
5 files modified
lib/lp/registry/browser/configure.zcml (+3/-3)
lib/lp/registry/browser/person.py (+289/-2)
lib/lp/registry/browser/team.py (+1/-288)
lib/lp/registry/browser/tests/test_person_upcomingwork.py (+40/-7)
lib/lp/registry/templates/person-upcomingwork.pt (+5/-2)
To merge this branch: bzr merge lp:~salgado/launchpad/person-upcoming-work-view
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+100878@code.launchpad.net

Commit message

Make the new +upcomingwork page work for people too

Description of the change

This branch just shuffles code around so that the new +upcomingwork view works for people as well. There's no link to it anywhere, though, as we need to check with Dan/Huw where it should go. Even when we add a link to it, it will be behind a feature flag just like the one for teams is.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

This code has already landed, so I won't recommend it here, but in the future it might be nice to use adaptation to provide generic workitems. SpecificationWorkItem could implement IGenericWorkItem directly, and you could provide an adapter from BugTask to IGenericWorkItem.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/configure.zcml'
2--- lib/lp/registry/browser/configure.zcml 2012-04-05 13:56:24 +0000
3+++ lib/lp/registry/browser/configure.zcml 2012-04-05 13:56:25 +0000
4@@ -1085,11 +1085,11 @@
5 template="../templates/team-mugshots.pt"
6 class="lp.registry.browser.team.TeamMugshotView"/>
7 <browser:page
8- for="lp.registry.interfaces.person.ITeam"
9- class="lp.registry.browser.team.TeamUpcomingWorkView"
10+ for="lp.registry.interfaces.person.IPerson"
11+ class="lp.registry.browser.person.PersonUpcomingWorkView"
12 permission="zope.Public"
13 name="+upcomingwork"
14- template="../templates/team-upcomingwork.pt"/>
15+ template="../templates/person-upcomingwork.pt"/>
16 <browser:page
17 for="lp.registry.interfaces.person.ITeam"
18 class="lp.registry.browser.team.TeamIndexView"
19
20=== modified file 'lib/lp/registry/browser/person.py'
21--- lib/lp/registry/browser/person.py 2012-03-02 07:53:53 +0000
22+++ lib/lp/registry/browser/person.py 2012-04-05 13:56:25 +0000
23@@ -47,6 +47,7 @@
24 'PersonSpecWorkloadTableView',
25 'PersonSpecWorkloadView',
26 'PersonSpecsMenu',
27+ 'PersonUpcomingWorkView',
28 'PersonView',
29 'PersonVouchersView',
30 'PPANavigationMenuMixIn',
31@@ -63,7 +64,10 @@
32
33
34 import cgi
35-from datetime import datetime
36+from datetime import (
37+ datetime,
38+ timedelta,
39+ )
40 import itertools
41 from itertools import chain
42 from operator import (
43@@ -129,6 +133,7 @@
44 from lp.app.browser.stringformatter import FormattersAPI
45 from lp.app.browser.tales import (
46 DateTimeFormatterAPI,
47+ format_link,
48 PersonFormatterAPI,
49 )
50 from lp.app.errors import (
51@@ -144,7 +149,10 @@
52 LaunchpadRadioWidgetWithDescription,
53 )
54 from lp.blueprints.browser.specificationtarget import HasSpecificationsView
55-from lp.blueprints.enums import SpecificationFilter
56+from lp.blueprints.enums import (
57+ SpecificationFilter,
58+ SpecificationWorkItemStatus,
59+ )
60 from lp.bugs.interfaces.bugtask import (
61 BugTaskSearchParams,
62 BugTaskStatus,
63@@ -4423,3 +4431,282 @@
64 def __call__(self):
65 """Render `Person` as XHTML using the webservice."""
66 return PersonFormatterAPI(self.person).link(None)
67+
68+
69+class PersonUpcomingWorkView(LaunchpadView):
70+ """This view displays work items and bugtasks that are due within 60 days
71+ and are assigned to a person (or participants of of a team).
72+ """
73+
74+ # We'll show bugs and work items targeted to milestones with a due date up
75+ # to DAYS from now.
76+ DAYS = 180
77+
78+ def initialize(self):
79+ super(PersonUpcomingWorkView, self).initialize()
80+ self.workitem_counts = {}
81+ self.bugtask_counts = {}
82+ self.milestones_per_date = {}
83+ self.progress_per_date = {}
84+ for date, containers in self.work_item_containers:
85+ total_items = 0
86+ total_done = 0
87+ milestones = set()
88+ self.bugtask_counts[date] = 0
89+ self.workitem_counts[date] = 0
90+ for container in containers:
91+ total_items += len(container.items)
92+ total_done += len(container.done_items)
93+ if isinstance(container, AggregatedBugsContainer):
94+ self.bugtask_counts[date] += len(container.items)
95+ else:
96+ self.workitem_counts[date] += len(container.items)
97+ for item in container.items:
98+ milestones.add(item.milestone)
99+ self.milestones_per_date[date] = sorted(
100+ milestones, key=attrgetter('displayname'))
101+ self.progress_per_date[date] = '{0:.0f}'.format(
102+ 100.0 * total_done / float(total_items))
103+
104+ @property
105+ def label(self):
106+ return self.page_title
107+
108+ @property
109+ def page_title(self):
110+ return "Upcoming work for %s" % self.context.displayname
111+
112+ @cachedproperty
113+ def work_item_containers(self):
114+ cutoff_date = datetime.today().date() + timedelta(days=self.DAYS)
115+ result = getWorkItemsDueBefore(self.context, cutoff_date, self.user)
116+ return sorted(result.items(), key=itemgetter(0))
117+
118+
119+class WorkItemContainer:
120+ """A container of work items, assigned to a person (or a team's
121+ participatns), whose milestone is due on a certain date.
122+ """
123+
124+ def __init__(self):
125+ self._items = []
126+
127+ @property
128+ def html_link(self):
129+ raise NotImplementedError("Must be implemented in subclasses")
130+
131+ @property
132+ def priority_title(self):
133+ raise NotImplementedError("Must be implemented in subclasses")
134+
135+ @property
136+ def target_link(self):
137+ raise NotImplementedError("Must be implemented in subclasses")
138+
139+ @property
140+ def assignee_link(self):
141+ raise NotImplementedError("Must be implemented in subclasses")
142+
143+ @property
144+ def items(self):
145+ raise NotImplementedError("Must be implemented in subclasses")
146+
147+ @property
148+ def done_items(self):
149+ return [item for item in self._items if item.is_complete]
150+
151+ @property
152+ def percent_done(self):
153+ return '{0:.0f}'.format(
154+ 100.0 * len(self.done_items) / len(self._items))
155+
156+ def append(self, item):
157+ self._items.append(item)
158+
159+
160+class SpecWorkItemContainer(WorkItemContainer):
161+ """A container of SpecificationWorkItems wrapped with GenericWorkItem."""
162+
163+ def __init__(self, spec):
164+ super(SpecWorkItemContainer, self).__init__()
165+ self.spec = spec
166+ self.priority = spec.priority
167+ self.target = spec.target
168+ self.assignee = spec.assignee
169+
170+ @property
171+ def html_link(self):
172+ return format_link(self.spec)
173+
174+ @property
175+ def priority_title(self):
176+ return self.priority.title
177+
178+ @property
179+ def target_link(self):
180+ return format_link(self.target)
181+
182+ @property
183+ def assignee_link(self):
184+ if self.assignee is None:
185+ return 'Nobody'
186+ return format_link(self.assignee)
187+
188+ @property
189+ def items(self):
190+ # Sort the work items by status only because they all have the same
191+ # priority.
192+ def sort_key(item):
193+ status_order = {
194+ SpecificationWorkItemStatus.POSTPONED: 5,
195+ SpecificationWorkItemStatus.DONE: 4,
196+ SpecificationWorkItemStatus.INPROGRESS: 3,
197+ SpecificationWorkItemStatus.TODO: 2,
198+ SpecificationWorkItemStatus.BLOCKED: 1,
199+ }
200+ return status_order[item.status]
201+ return sorted(self._items, key=sort_key)
202+
203+
204+class AggregatedBugsContainer(WorkItemContainer):
205+ """A container of BugTasks wrapped with GenericWorkItem."""
206+
207+ @property
208+ def html_link(self):
209+ return 'Bugs targeted to a milestone on this date'
210+
211+ @property
212+ def assignee_link(self):
213+ return 'N/A'
214+
215+ @property
216+ def target_link(self):
217+ return 'N/A'
218+
219+ @property
220+ def priority_title(self):
221+ return 'N/A'
222+
223+ @property
224+ def items(self):
225+ def sort_key(item):
226+ return (item.status.value, item.priority.value)
227+ # Sort by (status, priority) in reverse order because the biggest the
228+ # status/priority the more interesting it is to us.
229+ return sorted(self._items, key=sort_key, reverse=True)
230+
231+
232+class GenericWorkItem:
233+ """A generic piece of work; either a BugTask or a SpecificationWorkItem.
234+
235+ This class wraps a BugTask or a SpecificationWorkItem to provide a
236+ common API so that the template doesn't have to worry about what kind of
237+ work item it's dealing with.
238+ """
239+
240+ def __init__(self, assignee, status, priority, target, title,
241+ bugtask=None, work_item=None):
242+ self.assignee = assignee
243+ self.status = status
244+ self.priority = priority
245+ self.target = target
246+ self.title = title
247+ self._bugtask = bugtask
248+ self._work_item = work_item
249+
250+ @classmethod
251+ def from_bugtask(cls, bugtask):
252+ return cls(
253+ bugtask.assignee, bugtask.status, bugtask.importance,
254+ bugtask.target, bugtask.bug.description, bugtask=bugtask)
255+
256+ @classmethod
257+ def from_workitem(cls, work_item):
258+ assignee = work_item.assignee
259+ if assignee is None:
260+ assignee = work_item.specification.assignee
261+ return cls(
262+ assignee, work_item.status, work_item.specification.priority,
263+ work_item.specification.target, work_item.title,
264+ work_item=work_item)
265+
266+ @property
267+ def milestone(self):
268+ milestone = self.actual_workitem.milestone
269+ if milestone is None:
270+ assert self._work_item is not None, (
271+ "BugTaks without a milestone must not be here.")
272+ milestone = self._work_item.specification.milestone
273+ return milestone
274+
275+ @property
276+ def actual_workitem(self):
277+ """Return the actual work item that we are wrapping.
278+
279+ This may be either an IBugTask or an ISpecificationWorkItem.
280+ """
281+ if self._work_item is not None:
282+ return self._work_item
283+ else:
284+ return self._bugtask
285+
286+ @property
287+ def is_complete(self):
288+ return self.actual_workitem.is_complete
289+
290+
291+def getWorkItemsDueBefore(person, cutoff_date, user):
292+ """Return a dict mapping dates to lists of WorkItemContainers.
293+
294+ This is a grouping, by milestone due date, of all work items
295+ (SpecificationWorkItems/BugTasks) assigned to this person (or any of its
296+ participants, in case it's a team).
297+
298+ Only work items whose milestone have a due date between today and the
299+ given cut-off date are included in the results.
300+ """
301+ workitems = person.getAssignedSpecificationWorkItemsDueBefore(cutoff_date)
302+ # For every specification that has work items in the list above, create
303+ # one SpecWorkItemContainer holding the work items from that spec that are
304+ # targeted to the same milestone and assigned to this person (or its
305+ # participants, in case it's a team).
306+ containers_by_date = {}
307+ containers_by_spec = {}
308+ for workitem in workitems:
309+ spec = workitem.specification
310+ milestone = workitem.milestone
311+ if milestone is None:
312+ milestone = spec.milestone
313+ if milestone.dateexpected not in containers_by_date:
314+ containers_by_date[milestone.dateexpected] = []
315+ container = containers_by_spec.get(spec)
316+ if container is None:
317+ container = SpecWorkItemContainer(spec)
318+ containers_by_spec[spec] = container
319+ containers_by_date[milestone.dateexpected].append(container)
320+ container.append(GenericWorkItem.from_workitem(workitem))
321+
322+ # Sort our containers by priority.
323+ for date in containers_by_date:
324+ containers_by_date[date].sort(
325+ key=attrgetter('priority'), reverse=True)
326+
327+ bugtasks = person.getAssignedBugTasksDueBefore(cutoff_date, user)
328+ bug_containers_by_date = {}
329+ # For every milestone due date, create an AggregatedBugsContainer with all
330+ # the bugtasks targeted to a milestone on that date and assigned to
331+ # this person (or its participants, in case it's a team).
332+ for task in bugtasks:
333+ dateexpected = task.milestone.dateexpected
334+ container = bug_containers_by_date.get(dateexpected)
335+ if container is None:
336+ container = AggregatedBugsContainer()
337+ bug_containers_by_date[dateexpected] = container
338+ # Also append our new container to the dictionary we're going
339+ # to return.
340+ if dateexpected not in containers_by_date:
341+ containers_by_date[dateexpected] = []
342+ containers_by_date[dateexpected].append(container)
343+ container.append(GenericWorkItem.from_bugtask(task))
344+
345+ return containers_by_date
346
347=== modified file 'lib/lp/registry/browser/team.py'
348--- lib/lp/registry/browser/team.py 2012-04-05 13:56:24 +0000
349+++ lib/lp/registry/browser/team.py 2012-04-05 13:56:25 +0000
350@@ -28,7 +28,6 @@
351 'TeamOverviewNavigationMenu',
352 'TeamPrivacyAdapter',
353 'TeamReassignmentView',
354- 'TeamUpcomingWorkView',
355 ]
356
357
358@@ -38,10 +37,6 @@
359 timedelta,
360 )
361 import math
362-from operator import (
363- attrgetter,
364- itemgetter,
365- )
366 from urllib import unquote
367
368 from lazr.restful.interface import copy_field
369@@ -83,10 +78,7 @@
370 custom_widget,
371 LaunchpadFormView,
372 )
373-from lp.app.browser.tales import (
374- format_link,
375- PersonFormatterAPI,
376- )
377+from lp.app.browser.tales import PersonFormatterAPI
378 from lp.app.errors import UnexpectedFormData
379 from lp.app.validators import LaunchpadValidationError
380 from lp.app.validators.validation import validate_new_team_email
381@@ -97,7 +89,6 @@
382 )
383 from lp.app.widgets.owner import HiddenUserWidget
384 from lp.app.widgets.popup import PersonPickerWidget
385-from lp.blueprints.enums import SpecificationWorkItemStatus
386 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
387 from lp.registry.browser.branding import BrandingChangeView
388 from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
389@@ -2156,281 +2147,3 @@
390 batch_nav = BatchNavigator(
391 self.context.allmembers, self.request, size=self.batch_size)
392 return batch_nav
393-
394-
395-class TeamUpcomingWorkView(LaunchpadView):
396- """This view displays work items and bugtasks that are due within 60 days
397- and are assigned to members of a team.
398- """
399-
400- # We'll show bugs and work items targeted to milestones with a due date up
401- # to DAYS from now.
402- DAYS = 180
403-
404- def initialize(self):
405- super(TeamUpcomingWorkView, self).initialize()
406- self.workitem_counts = {}
407- self.bugtask_counts = {}
408- self.milestones_per_date = {}
409- self.progress_per_date = {}
410- for date, containers in self.work_item_containers:
411- total_items = 0
412- total_done = 0
413- milestones = set()
414- self.bugtask_counts[date] = 0
415- self.workitem_counts[date] = 0
416- for container in containers:
417- total_items += len(container.items)
418- total_done += len(container.done_items)
419- if isinstance(container, AggregatedBugsContainer):
420- self.bugtask_counts[date] += len(container.items)
421- else:
422- self.workitem_counts[date] += len(container.items)
423- for item in container.items:
424- milestones.add(item.milestone)
425- self.milestones_per_date[date] = sorted(
426- milestones, key=attrgetter('displayname'))
427- self.progress_per_date[date] = '{0:.0f}'.format(
428- 100.0 * total_done / float(total_items))
429-
430- @property
431- def label(self):
432- return self.page_title
433-
434- @property
435- def page_title(self):
436- return "Upcoming work for %s" % self.context.displayname
437-
438- @cachedproperty
439- def work_item_containers(self):
440- cutoff_date = datetime.today().date() + timedelta(days=self.DAYS)
441- result = getWorkItemsDueBefore(self.context, cutoff_date, self.user)
442- return sorted(result.items(), key=itemgetter(0))
443-
444-
445-class WorkItemContainer:
446- """A container of work items, assigned to members of a team, whose
447- milestone is due on a certain date.
448- """
449-
450- def __init__(self):
451- self._items = []
452-
453- @property
454- def html_link(self):
455- raise NotImplementedError("Must be implemented in subclasses")
456-
457- @property
458- def priority_title(self):
459- raise NotImplementedError("Must be implemented in subclasses")
460-
461- @property
462- def target_link(self):
463- raise NotImplementedError("Must be implemented in subclasses")
464-
465- @property
466- def assignee_link(self):
467- raise NotImplementedError("Must be implemented in subclasses")
468-
469- @property
470- def items(self):
471- raise NotImplementedError("Must be implemented in subclasses")
472-
473- @property
474- def done_items(self):
475- return [item for item in self._items if item.is_complete]
476-
477- @property
478- def percent_done(self):
479- return '{0:.0f}'.format(
480- 100.0 * len(self.done_items) / len(self._items))
481-
482- def append(self, item):
483- self._items.append(item)
484-
485-
486-class SpecWorkItemContainer(WorkItemContainer):
487- """A container of SpecificationWorkItems wrapped with GenericWorkItem."""
488-
489- def __init__(self, spec):
490- super(SpecWorkItemContainer, self).__init__()
491- self.spec = spec
492- self.priority = spec.priority
493- self.target = spec.target
494- self.assignee = spec.assignee
495-
496- @property
497- def html_link(self):
498- return format_link(self.spec)
499-
500- @property
501- def priority_title(self):
502- return self.priority.title
503-
504- @property
505- def target_link(self):
506- return format_link(self.target)
507-
508- @property
509- def assignee_link(self):
510- if self.assignee is None:
511- return 'Nobody'
512- return format_link(self.assignee)
513-
514- @property
515- def items(self):
516- # Sort the work items by status only because they all have the same
517- # priority.
518- def sort_key(item):
519- status_order = {
520- SpecificationWorkItemStatus.POSTPONED: 5,
521- SpecificationWorkItemStatus.DONE: 4,
522- SpecificationWorkItemStatus.INPROGRESS: 3,
523- SpecificationWorkItemStatus.TODO: 2,
524- SpecificationWorkItemStatus.BLOCKED: 1,
525- }
526- return status_order[item.status]
527- return sorted(self._items, key=sort_key)
528-
529-
530-class AggregatedBugsContainer(WorkItemContainer):
531- """A container of BugTasks wrapped with GenericWorkItem."""
532-
533- @property
534- def html_link(self):
535- return 'Bugs targeted to a milestone on this date'
536-
537- @property
538- def assignee_link(self):
539- return 'N/A'
540-
541- @property
542- def target_link(self):
543- return 'N/A'
544-
545- @property
546- def priority_title(self):
547- return 'N/A'
548-
549- @property
550- def items(self):
551- def sort_key(item):
552- return (item.status.value, item.priority.value)
553- # Sort by (status, priority) in reverse order because the biggest the
554- # status/priority the more interesting it is to us.
555- return sorted(self._items, key=sort_key, reverse=True)
556-
557-
558-class GenericWorkItem:
559- """A generic piece of work; either a BugTask or a SpecificationWorkItem.
560-
561- This class wraps a BugTask or a SpecificationWorkItem to provide a
562- common API so that the template doesn't have to worry about what kind of
563- work item it's dealing with.
564- """
565-
566- def __init__(self, assignee, status, priority, target, title,
567- bugtask=None, work_item=None):
568- self.assignee = assignee
569- self.status = status
570- self.priority = priority
571- self.target = target
572- self.title = title
573- self._bugtask = bugtask
574- self._work_item = work_item
575-
576- @classmethod
577- def from_bugtask(cls, bugtask):
578- return cls(
579- bugtask.assignee, bugtask.status, bugtask.importance,
580- bugtask.target, bugtask.bug.description, bugtask=bugtask)
581-
582- @classmethod
583- def from_workitem(cls, work_item):
584- assignee = work_item.assignee
585- if assignee is None:
586- assignee = work_item.specification.assignee
587- return cls(
588- assignee, work_item.status, work_item.specification.priority,
589- work_item.specification.target, work_item.title,
590- work_item=work_item)
591-
592- @property
593- def milestone(self):
594- milestone = self.actual_workitem.milestone
595- if milestone is None:
596- assert self._work_item is not None, (
597- "BugTaks without a milestone must not be here.")
598- milestone = self._work_item.specification.milestone
599- return milestone
600-
601- @property
602- def actual_workitem(self):
603- """Return the actual work item that we are wrapping.
604-
605- This may be either an IBugTask or an ISpecificationWorkItem.
606- """
607- if self._work_item is not None:
608- return self._work_item
609- else:
610- return self._bugtask
611-
612- @property
613- def is_complete(self):
614- return self.actual_workitem.is_complete
615-
616-
617-def getWorkItemsDueBefore(team, cutoff_date, user):
618- """Return a dict mapping dates to lists of WorkItemContainers.
619-
620- This is a grouping, by milestone due date, of all work items
621- (SpecificationWorkItems/BugTasks) assigned to any member of this
622- team.
623-
624- Only work items whose milestone have a due date between today and the
625- given cut-off date are included in the results.
626- """
627- workitems = team.getAssignedSpecificationWorkItemsDueBefore(cutoff_date)
628- # For every specification that has work items in the list above, create
629- # one SpecWorkItemContainer holding the work items from that spec that are
630- # targeted to the same milestone and assigned to members of the given team.
631- containers_by_date = {}
632- containers_by_spec = {}
633- for workitem in workitems:
634- spec = workitem.specification
635- milestone = workitem.milestone
636- if milestone is None:
637- milestone = spec.milestone
638- if milestone.dateexpected not in containers_by_date:
639- containers_by_date[milestone.dateexpected] = []
640- container = containers_by_spec.get(spec)
641- if container is None:
642- container = SpecWorkItemContainer(spec)
643- containers_by_spec[spec] = container
644- containers_by_date[milestone.dateexpected].append(container)
645- container.append(GenericWorkItem.from_workitem(workitem))
646-
647- # Sort our containers by priority.
648- for date in containers_by_date:
649- containers_by_date[date].sort(
650- key=attrgetter('priority'), reverse=True)
651-
652- bugtasks = team.getAssignedBugTasksDueBefore(cutoff_date, user)
653- bug_containers_by_date = {}
654- # For every milestone due date, create an AggregatedBugsContainer with all
655- # the bugtasks targeted to a milestone on that date and assigned to
656- # members of this team.
657- for task in bugtasks:
658- dateexpected = task.milestone.dateexpected
659- container = bug_containers_by_date.get(dateexpected)
660- if container is None:
661- container = AggregatedBugsContainer()
662- bug_containers_by_date[dateexpected] = container
663- # Also append our new container to the dictionary we're going
664- # to return.
665- if dateexpected not in containers_by_date:
666- containers_by_date[dateexpected] = []
667- containers_by_date[dateexpected].append(container)
668- container.append(GenericWorkItem.from_bugtask(task))
669-
670- return containers_by_date
671
672=== renamed file 'lib/lp/registry/browser/tests/test_team_upcomingwork.py' => 'lib/lp/registry/browser/tests/test_person_upcomingwork.py'
673--- lib/lp/registry/browser/tests/test_team_upcomingwork.py 2012-04-05 13:56:24 +0000
674+++ lib/lp/registry/browser/tests/test_person_upcomingwork.py 2012-04-05 13:56:25 +0000
675@@ -15,7 +15,7 @@
676 SpecificationPriority,
677 SpecificationWorkItemStatus,
678 )
679-from lp.registry.browser.team import (
680+from lp.registry.browser.person import (
681 GenericWorkItem,
682 getWorkItemsDueBefore,
683 WorkItemContainer,
684@@ -174,12 +174,12 @@
685 self.assertEqual('67', container.percent_done)
686
687
688-class TestTeamUpcomingWork(BrowserTestCase):
689+class TestPersonUpcomingWork(BrowserTestCase):
690
691 layer = DatabaseFunctionalLayer
692
693 def setUp(self):
694- super(TestTeamUpcomingWork, self).setUp()
695+ super(TestPersonUpcomingWork, self).setUp()
696 self.today = datetime.today().date()
697 self.tomorrow = self.today + timedelta(days=1)
698 self.today_milestone = self.factory.makeMilestone(
699@@ -188,7 +188,10 @@
700 dateexpected=self.tomorrow)
701 self.team = self.factory.makeTeam()
702
703- def test_basic(self):
704+ def test_basic_for_team(self):
705+ """Check that the page shows the bugs/work items assigned to members
706+ of a team.
707+ """
708 workitem1 = self.factory.makeSpecificationWorkItem(
709 assignee=self.team.teamowner, milestone=self.today_milestone)
710 workitem2 = self.factory.makeSpecificationWorkItem(
711@@ -203,6 +206,8 @@
712 browser = self.getViewBrowser(
713 self.team, view_name='+upcomingwork', no_login=True)
714
715+ # Check that the two work items and bugtasks created above are shown
716+ # and grouped under the appropriate milestone date.
717 groups = find_tags_by_class(browser.contents, 'workitems-group')
718 self.assertEqual(2, len(groups))
719 todays_group = extract_text(groups[0])
720@@ -287,13 +292,41 @@
721 self.assertEqual('100%', container1_progressbar.get('width'))
722 self.assertEqual('0%', container2_progressbar.get('width'))
723
724-
725-class TestTeamUpcomingWorkView(TestCaseWithFactory):
726+ def test_basic_for_person(self):
727+ """Check that the page shows the bugs/work items assigned to a person.
728+ """
729+ person = self.factory.makePerson()
730+ workitem = self.factory.makeSpecificationWorkItem(
731+ assignee=person, milestone=self.today_milestone)
732+ bugtask = self.factory.makeBug(
733+ milestone=self.tomorrow_milestone).bugtasks[0]
734+ removeSecurityProxy(bugtask).assignee = person
735+
736+ browser = self.getViewBrowser(
737+ person, view_name='+upcomingwork', no_login=True)
738+
739+ # Check that the two work items created above are shown and grouped
740+ # under the appropriate milestone date.
741+ groups = find_tags_by_class(browser.contents, 'workitems-group')
742+ self.assertEqual(2, len(groups))
743+ todays_group = extract_text(groups[0])
744+ tomorrows_group = extract_text(groups[1])
745+ self.assertStartsWith(
746+ todays_group, 'Work items due in %s' % self.today)
747+ self.assertIn(workitem.title, todays_group)
748+
749+ self.assertStartsWith(
750+ tomorrows_group, 'Work items due in %s' % self.tomorrow)
751+ with anonymous_logged_in():
752+ self.assertIn(bugtask.bug.title, tomorrows_group)
753+
754+
755+class TestPersonUpcomingWorkView(TestCaseWithFactory):
756
757 layer = DatabaseFunctionalLayer
758
759 def setUp(self):
760- super(TestTeamUpcomingWorkView, self).setUp()
761+ super(TestPersonUpcomingWorkView, self).setUp()
762 self.today = datetime.today().date()
763 self.tomorrow = self.today + timedelta(days=1)
764 self.today_milestone = self.factory.makeMilestone(
765
766=== renamed file 'lib/lp/registry/templates/team-upcomingwork.pt' => 'lib/lp/registry/templates/person-upcomingwork.pt'
767--- lib/lp/registry/templates/team-upcomingwork.pt 2012-04-05 13:56:24 +0000
768+++ lib/lp/registry/templates/person-upcomingwork.pt 2012-04-05 13:56:25 +0000
769@@ -68,8 +68,11 @@
770 There are <span tal:replace="python: view.workitem_counts[date]" />
771 Blueprint work items and
772 <span tal:replace="python: view.bugtask_counts[date]" /> Bugs due
773- in <span tal:content="date/fmt:date" /> which are assigned to members
774- of this team.
775+ in <span tal:content="date/fmt:date" /> which are assigned to
776+ <tal:team condition="context/is_team">members of this team.</tal:team>
777+ <tal:not-team condition="not: context/is_team">
778+ <span tal:replace="context/displayname" />
779+ </tal:not-team>
780 </p>
781
782 <table class="listing">