← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Guilherme Salgado has proposed merging lp:~salgado/launchpad/person-upcoming-work-view into lp:launchpad with lp:~linaro-infrastructure/launchpad/upcoming-work-progress-bars as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~salgado/launchpad/person-upcoming-work-view/+merge/100878

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.
-- 
https://code.launchpad.net/~salgado/launchpad/person-upcoming-work-view/+merge/100878
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~salgado/launchpad/person-upcoming-work-view into lp:launchpad.
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2012-04-03 15:19:44 +0000
+++ lib/lp/registry/browser/configure.zcml	2012-04-04 20:45:04 +0000
@@ -1085,11 +1085,11 @@
             template="../templates/team-mugshots.pt"
             class="lp.registry.browser.team.TeamMugshotView"/>
       <browser:page
-            for="lp.registry.interfaces.person.ITeam"
-            class="lp.registry.browser.team.TeamUpcomingWorkView"
+            for="lp.registry.interfaces.person.IPerson"
+            class="lp.registry.browser.person.PersonUpcomingWorkView"
             permission="zope.Public"
             name="+upcomingwork"
-            template="../templates/team-upcomingwork.pt"/>
+            template="../templates/person-upcomingwork.pt"/>
         <browser:page
             for="lp.registry.interfaces.person.ITeam"
             class="lp.registry.browser.team.TeamIndexView"

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2012-03-02 07:53:53 +0000
+++ lib/lp/registry/browser/person.py	2012-04-04 20:45:04 +0000
@@ -47,6 +47,7 @@
     'PersonSpecWorkloadTableView',
     'PersonSpecWorkloadView',
     'PersonSpecsMenu',
+    'PersonUpcomingWorkView',
     'PersonView',
     'PersonVouchersView',
     'PPANavigationMenuMixIn',
@@ -63,7 +64,10 @@
 
 
 import cgi
-from datetime import datetime
+from datetime import (
+    datetime,
+    timedelta,
+    )
 import itertools
 from itertools import chain
 from operator import (
@@ -129,6 +133,7 @@
 from lp.app.browser.stringformatter import FormattersAPI
 from lp.app.browser.tales import (
     DateTimeFormatterAPI,
+    format_link,
     PersonFormatterAPI,
     )
 from lp.app.errors import (
@@ -144,7 +149,10 @@
     LaunchpadRadioWidgetWithDescription,
     )
 from lp.blueprints.browser.specificationtarget import HasSpecificationsView
-from lp.blueprints.enums import SpecificationFilter
+from lp.blueprints.enums import (
+    SpecificationFilter,
+    SpecificationWorkItemStatus,
+    )
 from lp.bugs.interfaces.bugtask import (
     BugTaskSearchParams,
     BugTaskStatus,
@@ -4423,3 +4431,289 @@
     def __call__(self):
         """Render `Person` as XHTML using the webservice."""
         return PersonFormatterAPI(self.person).link(None)
+
+
+class PersonUpcomingWorkView(LaunchpadView):
+    """This view displays work items and bugtasks that are due within 60 days
+    and are assigned to a person (or participants of of a team).
+    """
+
+    # We'll show bugs and work items targeted to milestones with a due date up
+    # to DAYS from now.
+    DAYS = 180
+
+    def initialize(self):
+        super(PersonUpcomingWorkView, self).initialize()
+        self.workitem_counts = {}
+        self.bugtask_counts = {}
+        self.milestones_per_date = {}
+        self.progress_per_date = {}
+        for date, containers in self.work_item_containers:
+            total_items = 0
+            total_done = 0
+            milestones = set()
+            self.bugtask_counts[date] = 0
+            self.workitem_counts[date] = 0
+            for container in containers:
+                total_items += len(container.items)
+                total_done += len(container.done_items)
+                if isinstance(container, AggregatedBugsContainer):
+                    self.bugtask_counts[date] += len(container.items)
+                else:
+                    self.workitem_counts[date] += len(container.items)
+                for item in container.items:
+                    milestones.add(item.milestone)
+            self.milestones_per_date[date] = sorted(
+                milestones, key=attrgetter('displayname'))
+            self.progress_per_date[date] = '{0:.0f}'.format(
+                100.0 * total_done / float(total_items))
+
+    @property
+    def label(self):
+        return self.page_title
+
+    @property
+    def page_title(self):
+        return "Upcoming work for %s" % self.context.displayname
+
+    @cachedproperty
+    def work_item_containers(self):
+        cutoff_date = datetime.today().date() + timedelta(days=self.DAYS)
+        result = getWorkItemsDueBefore(self.context, cutoff_date, self.user)
+        return sorted(result.items(), key=itemgetter(0))
+
+
+class WorkItemContainer:
+    """A container of work items, assigned to a person (or a team's
+    participatns), whose milestone is due on a certain date.
+    """
+
+    def __init__(self):
+        self._items = []
+
+    @property
+    def html_link(self):
+        raise NotImplementedError("Must be implemented in subclasses")
+
+    @property
+    def priority_title(self):
+        raise NotImplementedError("Must be implemented in subclasses")
+
+    @property
+    def target_link(self):
+        raise NotImplementedError("Must be implemented in subclasses")
+
+    @property
+    def assignee_link(self):
+        raise NotImplementedError("Must be implemented in subclasses")
+
+    @property
+    def items(self):
+        raise NotImplementedError("Must be implemented in subclasses")
+
+    @property
+    def done_items(self):
+        return [item for item in self._items if item.is_complete]
+
+    @property
+    def percent_done(self):
+        return '{0:.0f}'.format(
+            100.0 * len(self.done_items) / len(self._items))
+
+    def append(self, item):
+        self._items.append(item)
+
+
+class SpecWorkItemContainer(WorkItemContainer):
+    """A container of SpecificationWorkItems wrapped with GenericWorkItem."""
+
+    def __init__(self, spec):
+        super(SpecWorkItemContainer, self).__init__()
+        self.spec = spec
+        self.priority = spec.priority
+        self.target = spec.target
+        self.assignee = spec.assignee
+
+    @property
+    def html_link(self):
+        return format_link(self.spec)
+
+    @property
+    def priority_title(self):
+        return self.priority.title
+
+    @property
+    def target_link(self):
+        return format_link(self.target)
+
+    @property
+    def assignee_link(self):
+        if self.assignee is None:
+            return 'Nobody'
+        return format_link(self.assignee)
+
+    @property
+    def items(self):
+        # Sort the work items by status only because they all have the same
+        # priority.
+        def sort_key(item):
+            status_order = {
+                SpecificationWorkItemStatus.POSTPONED: 5,
+                SpecificationWorkItemStatus.DONE: 4,
+                SpecificationWorkItemStatus.INPROGRESS: 3,
+                SpecificationWorkItemStatus.TODO: 2,
+                SpecificationWorkItemStatus.BLOCKED: 1,
+                }
+            return status_order[item.status]
+        return sorted(self._items, key=sort_key)
+
+
+class AggregatedBugsContainer(WorkItemContainer):
+    """A container of BugTasks wrapped with GenericWorkItem."""
+
+    @property
+    def html_link(self):
+        return 'Bugs targeted to a milestone on this date'
+
+    @property
+    def assignee_link(self):
+        return 'N/A'
+
+    @property
+    def target_link(self):
+        return 'N/A'
+
+    @property
+    def priority_title(self):
+        return 'N/A'
+
+    @property
+    def items(self):
+        def sort_key(item):
+            return (item.status.value, item.priority.value)
+        # Sort by (status, priority) in reverse order because the biggest the
+        # status/priority the more interesting it is to us.
+        return sorted(self._items, key=sort_key, reverse=True)
+
+
+class GenericWorkItem:
+    """A generic piece of work; either a BugTask or a SpecificationWorkItem.
+
+    This class wraps a BugTask or a SpecificationWorkItem to provide a
+    common API so that the template doesn't have to worry about what kind of
+    work item it's dealing with.
+    """
+
+    def __init__(self, assignee, status, priority, target, title,
+                 bugtask=None, work_item=None):
+        self.assignee = assignee
+        self.status = status
+        self.priority = priority
+        self.target = target
+        self.title = title
+        self._bugtask = bugtask
+        self._work_item = work_item
+
+    @classmethod
+    def from_bugtask(cls, bugtask):
+        return cls(
+            bugtask.assignee, bugtask.status, bugtask.importance,
+            bugtask.target, bugtask.bug.description, bugtask=bugtask)
+
+    @classmethod
+    def from_workitem(cls, work_item):
+        assignee = work_item.assignee
+        if assignee is None:
+            assignee = work_item.specification.assignee
+        return cls(
+            assignee, work_item.status, work_item.specification.priority,
+            work_item.specification.target, work_item.title,
+            work_item=work_item)
+
+    @property
+    def display_title(self):
+        if self._work_item is not None:
+            return FormattersAPI(self.title).shorten(120)
+        else:
+            return format_link(self._bugtask)
+
+    @property
+    def milestone(self):
+        milestone = self.actual_workitem.milestone
+        if milestone is None:
+            assert self._work_item is not None, (
+                "BugTaks without a milestone must not be here.")
+            milestone = self._work_item.specification.milestone
+        return milestone
+
+    @property
+    def actual_workitem(self):
+        """Return the actual work item that we are wrapping.
+
+        This may be either an IBugTask or an ISpecificationWorkItem.
+        """
+        if self._work_item is not None:
+            return self._work_item
+        else:
+            return self._bugtask
+
+    @property
+    def is_complete(self):
+        return self.actual_workitem.is_complete
+
+
+def getWorkItemsDueBefore(person, cutoff_date, user):
+    """Return a dict mapping dates to lists of WorkItemContainers.
+
+    This is a grouping, by milestone due date, of all work items
+    (SpecificationWorkItems/BugTasks) assigned to this person (or any of its
+    participants, in case it's a team).
+
+    Only work items whose milestone have a due date between today and the
+    given cut-off date are included in the results.
+    """
+    workitems = person.getAssignedSpecificationWorkItemsDueBefore(cutoff_date)
+    # For every specification that has work items in the list above, create
+    # one SpecWorkItemContainer holding the work items from that spec that are
+    # targeted to the same milestone and assigned to this person (or its
+    # participants, in case it's a team).
+    containers_by_date = {}
+    containers_by_spec = {}
+    for workitem in workitems:
+        spec = workitem.specification
+        milestone = workitem.milestone
+        if milestone is None:
+            milestone = spec.milestone
+        if milestone.dateexpected not in containers_by_date:
+            containers_by_date[milestone.dateexpected] = []
+        container = containers_by_spec.get(spec)
+        if container is None:
+            container = SpecWorkItemContainer(spec)
+            containers_by_spec[spec] = container
+            containers_by_date[milestone.dateexpected].append(container)
+        container.append(GenericWorkItem.from_workitem(workitem))
+
+    # Sort our containers by priority.
+    for date in containers_by_date:
+        containers_by_date[date].sort(
+            key=attrgetter('priority'), reverse=True)
+
+    bugtasks = person.getAssignedBugTasksDueBefore(cutoff_date, user)
+    bug_containers_by_date = {}
+    # For every milestone due date, create an AggregatedBugsContainer with all
+    # the bugtasks targeted to a milestone on that date and assigned to
+    # this person (or its participants, in case it's a team).
+    for task in bugtasks:
+        dateexpected = task.milestone.dateexpected
+        container = bug_containers_by_date.get(dateexpected)
+        if container is None:
+            container = AggregatedBugsContainer()
+            bug_containers_by_date[dateexpected] = container
+            # Also append our new container to the dictionary we're going
+            # to return.
+            if dateexpected not in containers_by_date:
+                containers_by_date[dateexpected] = []
+            containers_by_date[dateexpected].append(container)
+        container.append(GenericWorkItem.from_bugtask(task))
+
+    return containers_by_date

=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py	2012-04-04 20:45:04 +0000
+++ lib/lp/registry/browser/team.py	2012-04-04 20:45:04 +0000
@@ -28,7 +28,6 @@
     'TeamOverviewNavigationMenu',
     'TeamPrivacyAdapter',
     'TeamReassignmentView',
-    'TeamUpcomingWorkView',
     ]
 
 
@@ -38,10 +37,6 @@
     timedelta,
     )
 import math
-from operator import (
-    attrgetter,
-    itemgetter,
-    )
 from urllib import unquote
 
 from lazr.restful.interface import copy_field
@@ -83,11 +78,7 @@
     custom_widget,
     LaunchpadFormView,
     )
-from lp.app.browser.stringformatter import FormattersAPI
-from lp.app.browser.tales import (
-    format_link,
-    PersonFormatterAPI,
-    )
+from lp.app.browser.tales import PersonFormatterAPI
 from lp.app.errors import UnexpectedFormData
 from lp.app.validators import LaunchpadValidationError
 from lp.app.validators.validation import validate_new_team_email
@@ -98,7 +89,6 @@
     )
 from lp.app.widgets.owner import HiddenUserWidget
 from lp.app.widgets.popup import PersonPickerWidget
-from lp.blueprints.enums import SpecificationWorkItemStatus
 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
 from lp.registry.browser.branding import BrandingChangeView
 from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
@@ -2157,288 +2147,3 @@
         batch_nav = BatchNavigator(
             self.context.allmembers, self.request, size=self.batch_size)
         return batch_nav
-
-
-class TeamUpcomingWorkView(LaunchpadView):
-    """This view displays work items and bugtasks that are due within 60 days
-    and are assigned to members of a team.
-    """
-
-    # We'll show bugs and work items targeted to milestones with a due date up
-    # to DAYS from now.
-    DAYS = 180
-
-    def initialize(self):
-        super(TeamUpcomingWorkView, self).initialize()
-        self.workitem_counts = {}
-        self.bugtask_counts = {}
-        self.milestones_per_date = {}
-        self.progress_per_date = {}
-        for date, containers in self.work_item_containers:
-            total_items = 0
-            total_done = 0
-            milestones = set()
-            self.bugtask_counts[date] = 0
-            self.workitem_counts[date] = 0
-            for container in containers:
-                total_items += len(container.items)
-                total_done += len(container.done_items)
-                if isinstance(container, AggregatedBugsContainer):
-                    self.bugtask_counts[date] += len(container.items)
-                else:
-                    self.workitem_counts[date] += len(container.items)
-                for item in container.items:
-                    milestones.add(item.milestone)
-            self.milestones_per_date[date] = sorted(
-                milestones, key=attrgetter('displayname'))
-            self.progress_per_date[date] = '{0:.0f}'.format(
-                100.0 * total_done / float(total_items))
-
-    @property
-    def label(self):
-        return self.page_title
-
-    @property
-    def page_title(self):
-        return "Upcoming work for %s" % self.context.displayname
-
-    @cachedproperty
-    def work_item_containers(self):
-        cutoff_date = datetime.today().date() + timedelta(days=self.DAYS)
-        result = getWorkItemsDueBefore(self.context, cutoff_date, self.user)
-        return sorted(result.items(), key=itemgetter(0))
-
-
-class WorkItemContainer:
-    """A container of work items, assigned to members of a team, whose
-    milestone is due on a certain date.
-    """
-
-    def __init__(self):
-        self._items = []
-
-    @property
-    def html_link(self):
-        raise NotImplementedError("Must be implemented in subclasses")
-
-    @property
-    def priority_title(self):
-        raise NotImplementedError("Must be implemented in subclasses")
-
-    @property
-    def target_link(self):
-        raise NotImplementedError("Must be implemented in subclasses")
-
-    @property
-    def assignee_link(self):
-        raise NotImplementedError("Must be implemented in subclasses")
-
-    @property
-    def items(self):
-        raise NotImplementedError("Must be implemented in subclasses")
-
-    @property
-    def done_items(self):
-        return [item for item in self._items if item.is_complete]
-
-    @property
-    def percent_done(self):
-        return '{0:.0f}'.format(
-            100.0 * len(self.done_items) / len(self._items))
-
-    def append(self, item):
-        self._items.append(item)
-
-
-class SpecWorkItemContainer(WorkItemContainer):
-    """A container of SpecificationWorkItems wrapped with GenericWorkItem."""
-
-    def __init__(self, spec):
-        super(SpecWorkItemContainer, self).__init__()
-        self.spec = spec
-        self.priority = spec.priority
-        self.target = spec.target
-        self.assignee = spec.assignee
-
-    @property
-    def html_link(self):
-        return format_link(self.spec)
-
-    @property
-    def priority_title(self):
-        return self.priority.title
-
-    @property
-    def target_link(self):
-        return format_link(self.target)
-
-    @property
-    def assignee_link(self):
-        if self.assignee is None:
-            return 'Nobody'
-        return format_link(self.assignee)
-
-    @property
-    def items(self):
-        # Sort the work items by status only because they all have the same
-        # priority.
-        def sort_key(item):
-            status_order = {
-                SpecificationWorkItemStatus.POSTPONED: 5,
-                SpecificationWorkItemStatus.DONE: 4,
-                SpecificationWorkItemStatus.INPROGRESS: 3,
-                SpecificationWorkItemStatus.TODO: 2,
-                SpecificationWorkItemStatus.BLOCKED: 1,
-                }
-            return status_order[item.status]
-        return sorted(self._items, key=sort_key)
-
-
-class AggregatedBugsContainer(WorkItemContainer):
-    """A container of BugTasks wrapped with GenericWorkItem."""
-
-    @property
-    def html_link(self):
-        return 'Bugs targeted to a milestone on this date'
-
-    @property
-    def assignee_link(self):
-        return 'N/A'
-
-    @property
-    def target_link(self):
-        return 'N/A'
-
-    @property
-    def priority_title(self):
-        return 'N/A'
-
-    @property
-    def items(self):
-        def sort_key(item):
-            return (item.status.value, item.priority.value)
-        # Sort by (status, priority) in reverse order because the biggest the
-        # status/priority the more interesting it is to us.
-        return sorted(self._items, key=sort_key, reverse=True)
-
-
-class GenericWorkItem:
-    """A generic piece of work; either a BugTask or a SpecificationWorkItem.
-
-    This class wraps a BugTask or a SpecificationWorkItem to provide a
-    common API so that the template doesn't have to worry about what kind of
-    work item it's dealing with.
-    """
-
-    def __init__(self, assignee, status, priority, target, title,
-                 bugtask=None, work_item=None):
-        self.assignee = assignee
-        self.status = status
-        self.priority = priority
-        self.target = target
-        self.title = title
-        self._bugtask = bugtask
-        self._work_item = work_item
-
-    @classmethod
-    def from_bugtask(cls, bugtask):
-        return cls(
-            bugtask.assignee, bugtask.status, bugtask.importance,
-            bugtask.target, bugtask.bug.description, bugtask=bugtask)
-
-    @classmethod
-    def from_workitem(cls, work_item):
-        assignee = work_item.assignee
-        if assignee is None:
-            assignee = work_item.specification.assignee
-        return cls(
-            assignee, work_item.status, work_item.specification.priority,
-            work_item.specification.target, work_item.title,
-            work_item=work_item)
-
-    @property
-    def display_title(self):
-        if self._work_item is not None:
-            return FormattersAPI(self.title).shorten(120)
-        else:
-            return format_link(self._bugtask)
-
-    @property
-    def milestone(self):
-        milestone = self.actual_workitem.milestone
-        if milestone is None:
-            assert self._work_item is not None, (
-                "BugTaks without a milestone must not be here.")
-            milestone = self._work_item.specification.milestone
-        return milestone
-
-    @property
-    def actual_workitem(self):
-        """Return the actual work item that we are wrapping.
-
-        This may be either an IBugTask or an ISpecificationWorkItem.
-        """
-        if self._work_item is not None:
-            return self._work_item
-        else:
-            return self._bugtask
-
-    @property
-    def is_complete(self):
-        return self.actual_workitem.is_complete
-
-
-def getWorkItemsDueBefore(team, cutoff_date, user):
-    """Return a dict mapping dates to lists of WorkItemContainers.
-
-    This is a grouping, by milestone due date, of all work items
-    (SpecificationWorkItems/BugTasks) assigned to any member of this
-    team.
-
-    Only work items whose milestone have a due date between today and the
-    given cut-off date are included in the results.
-    """
-    workitems = team.getAssignedSpecificationWorkItemsDueBefore(cutoff_date)
-    # For every specification that has work items in the list above, create
-    # one SpecWorkItemContainer holding the work items from that spec that are
-    # targeted to the same milestone and assigned to members of the given team.
-    containers_by_date = {}
-    containers_by_spec = {}
-    for workitem in workitems:
-        spec = workitem.specification
-        milestone = workitem.milestone
-        if milestone is None:
-            milestone = spec.milestone
-        if milestone.dateexpected not in containers_by_date:
-            containers_by_date[milestone.dateexpected] = []
-        container = containers_by_spec.get(spec)
-        if container is None:
-            container = SpecWorkItemContainer(spec)
-            containers_by_spec[spec] = container
-            containers_by_date[milestone.dateexpected].append(container)
-        container.append(GenericWorkItem.from_workitem(workitem))
-
-    # Sort our containers by priority.
-    for date in containers_by_date:
-        containers_by_date[date].sort(
-            key=attrgetter('priority'), reverse=True)
-
-    bugtasks = team.getAssignedBugTasksDueBefore(cutoff_date, user)
-    bug_containers_by_date = {}
-    # For every milestone due date, create an AggregatedBugsContainer with all
-    # the bugtasks targeted to a milestone on that date and assigned to
-    # members of this team.
-    for task in bugtasks:
-        dateexpected = task.milestone.dateexpected
-        container = bug_containers_by_date.get(dateexpected)
-        if container is None:
-            container = AggregatedBugsContainer()
-            bug_containers_by_date[dateexpected] = container
-            # Also append our new container to the dictionary we're going
-            # to return.
-            if dateexpected not in containers_by_date:
-                containers_by_date[dateexpected] = []
-            containers_by_date[dateexpected].append(container)
-        container.append(GenericWorkItem.from_bugtask(task))
-
-    return containers_by_date

=== renamed file 'lib/lp/registry/browser/tests/test_team_upcomingwork.py' => 'lib/lp/registry/browser/tests/test_person_upcomingwork.py'
--- lib/lp/registry/browser/tests/test_team_upcomingwork.py	2012-04-04 20:45:04 +0000
+++ lib/lp/registry/browser/tests/test_person_upcomingwork.py	2012-04-04 20:45:04 +0000
@@ -15,7 +15,7 @@
     SpecificationPriority,
     SpecificationWorkItemStatus,
     )
-from lp.registry.browser.team import (
+from lp.registry.browser.person import (
     GenericWorkItem,
     getWorkItemsDueBefore,
     WorkItemContainer,
@@ -174,12 +174,12 @@
         self.assertEqual('67', container.percent_done)
 
 
-class TestTeamUpcomingWork(BrowserTestCase):
+class TestPersonUpcomingWork(BrowserTestCase):
 
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        super(TestTeamUpcomingWork, self).setUp()
+        super(TestPersonUpcomingWork, self).setUp()
         self.today = datetime.today().date()
         self.tomorrow = self.today + timedelta(days=1)
         self.today_milestone = self.factory.makeMilestone(
@@ -188,7 +188,10 @@
             dateexpected=self.tomorrow)
         self.team = self.factory.makeTeam()
 
-    def test_basic(self):
+    def test_basic_for_team(self):
+        """Check that the page shows the bugs/work items assigned to members
+        of a team.
+        """
         workitem1 = self.factory.makeSpecificationWorkItem(
             assignee=self.team.teamowner, milestone=self.today_milestone)
         workitem2 = self.factory.makeSpecificationWorkItem(
@@ -203,6 +206,8 @@
         browser = self.getViewBrowser(
             self.team, view_name='+upcomingwork', no_login=True)
 
+        # Check that the two work items and bugtasks created above are shown
+        # and grouped under the appropriate milestone date.
         groups = find_tags_by_class(browser.contents, 'workitems-group')
         self.assertEqual(2, len(groups))
         todays_group = extract_text(groups[0])
@@ -219,6 +224,34 @@
         with anonymous_logged_in():
             self.assertIn(bugtask2.bug.title, tomorrows_group)
 
+    def test_basic_for_person(self):
+        """Check that the page shows the bugs/work items assigned to a person.
+        """
+        person = self.factory.makePerson()
+        workitem = self.factory.makeSpecificationWorkItem(
+            assignee=person, milestone=self.today_milestone)
+        bugtask = self.factory.makeBug(
+            milestone=self.tomorrow_milestone).bugtasks[0]
+        removeSecurityProxy(bugtask).assignee = person
+
+        browser = self.getViewBrowser(
+            person, view_name='+upcomingwork', no_login=True)
+
+        # Check that the two work items created above are shown and grouped
+        # under the appropriate milestone date.
+        groups = find_tags_by_class(browser.contents, 'workitems-group')
+        self.assertEqual(2, len(groups))
+        todays_group = extract_text(groups[0])
+        tomorrows_group = extract_text(groups[1])
+        self.assertStartsWith(
+            todays_group, 'Work items due in %s' % self.today)
+        self.assertIn(workitem.title, todays_group)
+
+        self.assertStartsWith(
+            tomorrows_group, 'Work items due in %s' % self.tomorrow)
+        with anonymous_logged_in():
+            self.assertIn(bugtask.bug.title, tomorrows_group)
+
     def test_overall_progressbar(self):
         """Check that the per-date progress bar is present."""
         # Create two work items on separate specs. One of them is done and the
@@ -272,12 +305,12 @@
         self.assertEqual('0%', container2_progressbar.get('width'))
 
 
-class TestTeamUpcomingWorkView(TestCaseWithFactory):
+class TestPersonUpcomingWorkView(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
-        super(TestTeamUpcomingWorkView, self).setUp()
+        super(TestPersonUpcomingWorkView, self).setUp()
         self.today = datetime.today().date()
         self.tomorrow = self.today + timedelta(days=1)
         self.today_milestone = self.factory.makeMilestone(

=== renamed file 'lib/lp/registry/templates/team-upcomingwork.pt' => 'lib/lp/registry/templates/person-upcomingwork.pt'
--- lib/lp/registry/templates/team-upcomingwork.pt	2012-04-04 20:45:04 +0000
+++ lib/lp/registry/templates/person-upcomingwork.pt	2012-04-04 20:45:04 +0000
@@ -68,8 +68,11 @@
       There are <span tal:replace="python: view.workitem_counts[date]" />
       Blueprint work items and
       <span tal:replace="python: view.bugtask_counts[date]" /> Bugs due
-      in <span tal:content="date/fmt:date" /> which are assigned to members
-      of this team.
+      in <span tal:content="date/fmt:date" /> which are assigned to
+      <tal:team condition="context/is_team">members of this team.</tal:team>
+      <tal:not-team condition="not: context/is_team">
+        <span tal:replace="context/displayname" />
+      </tal:not-team>
     </p>
 
     <table class="listing">