Merge lp:~linaro-infrastructure/launchpad/team-engineering-view-ui into lp:launchpad

Proposed by Guilherme Salgado
Status: Superseded
Proposed branch: lp:~linaro-infrastructure/launchpad/team-engineering-view-ui
Merge into: lp:launchpad
Diff against target: 890 lines (+729/-6)
10 files modified
lib/lp/blueprints/interfaces/specificationworkitem.py (+6/-0)
lib/lp/blueprints/model/specificationworkitem.py (+5/-0)
lib/lp/registry/browser/configure.zcml (+6/-0)
lib/lp/registry/browser/team.py (+297/-3)
lib/lp/registry/browser/tests/test_team_upcomingwork.py (+288/-0)
lib/lp/registry/model/person.py (+2/-2)
lib/lp/registry/templates/team-index.pt (+7/-0)
lib/lp/registry/templates/team-upcomingwork.pt (+105/-0)
lib/lp/services/features/flags.py (+6/-0)
lib/lp/testing/factory.py (+7/-1)
To merge this branch: bzr merge lp:~linaro-infrastructure/launchpad/team-engineering-view-ui
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+100707@code.launchpad.net

This proposal has been superseded by a proposal from 2012-04-05.

Commit message

View for upcoming blueprints and bugs assigned to a team.

Description of the change

This branch starts the implementation of the new team page showing all
upcoming work (up to 6 months, really) assigned to members of the team.

It is part of https://dev.launchpad.net/Projects/WorkItems and a screenshot of
what it currently looks like is at
http://people.canonical.com/~salgado/upcoming-work.png

It is protected with a feature flag because it is not ready for general
consumption yet. We plan to make it visible only to Linaro until we implement
the remaining bits and polish the UI, but if there's interest it can be made
available to other teams as well.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) wrote :

Broadly this looks good. I think you should run format-imports on the files you've changed or added, some of them are not quite right.

I'm not really happy with "registry.upcoming_work_view.enabled" as a feature flag name -- perhaps it should move to blueprints.

review: Approve (code)
Revision history for this message
Guilherme Salgado (salgado) wrote :

Thanks for the review, Steve.

On 03/04/12 20:59, Steve Kowalik wrote:
> Review: Approve code
>
> Broadly this looks good. I think you should run format-imports on the files you've changed or added, some of them are not quite right.

Cool, I'll do that.

>
> I'm not really happy with "registry.upcoming_work_view.enabled" as a feature flag name -- perhaps it should move to blueprints.

The problem is that the new page actually spans across blueprints and
bugs; that's why it is on the default layer and not blueprints.

Revision history for this message
Mattias Backman (mabac) wrote :

On Wed, Apr 4, 2012 at 3:25 AM, Guilherme Salgado
<email address hidden> wrote:
> Thanks for the review, Steve.
>
> On 03/04/12 20:59, Steve Kowalik wrote:
>> Review: Approve code
>>
>> Broadly this looks good. I think you should run format-imports on the files you've changed or added, some of them are not quite right.
>
> Cool, I'll do that.

That bit has been fixed.

>
>>
>> I'm not really happy with "registry.upcoming_work_view.enabled" as a feature flag name -- perhaps it should move to blueprints.
>
> The problem is that the new page actually spans across blueprints and
> bugs; that's why it is on the default layer and not blueprints.
>
>
> --
> https://code.launchpad.net/~linaro-infrastructure/launchpad/team-engineering-view-ui/+merge/100707
> Your team Linaro Infrastructure is subscribed to branch lp:~linaro-infrastructure/launchpad/team-engineering-view-ui.

Revision history for this message
Guilherme Salgado (salgado) wrote :

I've changed it back to needs-review as these changes have been reverted because there was a XSS hole in the new page. My last commit here (r14958) fixes that.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/blueprints/interfaces/specificationworkitem.py'
--- lib/lp/blueprints/interfaces/specificationworkitem.py 2012-02-07 01:53:23 +0000
+++ lib/lp/blueprints/interfaces/specificationworkitem.py 2012-04-04 08:07:25 +0000
@@ -74,3 +74,9 @@
74 required=True, description=_(74 required=True, description=_(
75 "The sequence in which the work items are to be displayed in the "75 "The sequence in which the work items are to be displayed in the "
76 "UI."))76 "UI."))
77
78 is_complete = Bool(
79 readonly=True,
80 description=_(
81 "True or False depending on whether or not there is more "
82 "work required on this work item."))
7783
=== modified file 'lib/lp/blueprints/model/specificationworkitem.py'
--- lib/lp/blueprints/model/specificationworkitem.py 2012-03-09 19:27:35 +0000
+++ lib/lp/blueprints/model/specificationworkitem.py 2012-04-04 08:07:25 +0000
@@ -59,3 +59,8 @@
59 self.assignee=assignee59 self.assignee=assignee
60 self.milestone=milestone60 self.milestone=milestone
61 self.sequence=sequence61 self.sequence=sequence
62
63 @property
64 def is_complete(self):
65 """See `ISpecificationWorkItem`."""
66 return self.status == SpecificationWorkItemStatus.DONE
6267
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2012-03-31 11:32:15 +0000
+++ lib/lp/registry/browser/configure.zcml 2012-04-04 08:07:25 +0000
@@ -1084,6 +1084,12 @@
1084 name="+mugshots"1084 name="+mugshots"
1085 template="../templates/team-mugshots.pt"1085 template="../templates/team-mugshots.pt"
1086 class="lp.registry.browser.team.TeamMugshotView"/>1086 class="lp.registry.browser.team.TeamMugshotView"/>
1087 <browser:page
1088 for="lp.registry.interfaces.person.ITeam"
1089 class="lp.registry.browser.team.TeamUpcomingWorkView"
1090 permission="zope.Public"
1091 name="+upcomingwork"
1092 template="../templates/team-upcomingwork.pt"/>
1087 <browser:page1093 <browser:page
1088 for="lp.registry.interfaces.person.ITeam"1094 for="lp.registry.interfaces.person.ITeam"
1089 class="lp.registry.browser.team.TeamIndexView"1095 class="lp.registry.browser.team.TeamIndexView"
10901096
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2012-03-14 21:05:57 +0000
+++ lib/lp/registry/browser/team.py 2012-04-04 08:07:25 +0000
@@ -28,6 +28,7 @@
28 'TeamOverviewNavigationMenu',28 'TeamOverviewNavigationMenu',
29 'TeamPrivacyAdapter',29 'TeamPrivacyAdapter',
30 'TeamReassignmentView',30 'TeamReassignmentView',
31 'TeamUpcomingWorkView',
31 ]32 ]
3233
3334
@@ -37,6 +38,10 @@
37 timedelta,38 timedelta,
38 )39 )
39import math40import math
41from operator import (
42 attrgetter,
43 itemgetter,
44 )
40from urllib import unquote45from urllib import unquote
4146
42from lazr.restful.interface import copy_field47from lazr.restful.interface import copy_field
@@ -78,7 +83,11 @@
78 custom_widget,83 custom_widget,
79 LaunchpadFormView,84 LaunchpadFormView,
80 )85 )
81from lp.app.browser.tales import PersonFormatterAPI86from lp.app.browser.stringformatter import FormattersAPI
87from lp.app.browser.tales import (
88 format_link,
89 PersonFormatterAPI,
90 )
82from lp.app.errors import UnexpectedFormData91from lp.app.errors import UnexpectedFormData
83from lp.app.validators import LaunchpadValidationError92from lp.app.validators import LaunchpadValidationError
84from lp.app.validators.validation import validate_new_team_email93from lp.app.validators.validation import validate_new_team_email
@@ -89,6 +98,7 @@
89 )98 )
90from lp.app.widgets.owner import HiddenUserWidget99from lp.app.widgets.owner import HiddenUserWidget
91from lp.app.widgets.popup import PersonPickerWidget100from lp.app.widgets.popup import PersonPickerWidget
101from lp.blueprints.enums import SpecificationWorkItemStatus
92from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin102from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
93from lp.registry.browser.branding import BrandingChangeView103from lp.registry.browser.branding import BrandingChangeView
94from lp.registry.browser.mailinglists import enabled_with_active_mailing_list104from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
@@ -142,6 +152,7 @@
142 )152 )
143from lp.security import ModerateByRegistryExpertsOrAdmins153from lp.security import ModerateByRegistryExpertsOrAdmins
144from lp.services.config import config154from lp.services.config import config
155from lp.services.features import getFeatureFlag
145from lp.services.fields import PublicPersonChoice156from lp.services.fields import PublicPersonChoice
146from lp.services.identity.interfaces.emailaddress import IEmailAddressSet157from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
147from lp.services.privacy.interfaces import IObjectPrivacy158from lp.services.privacy.interfaces import IObjectPrivacy
@@ -1589,6 +1600,14 @@
1589 icon = 'add'1600 icon = 'add'
1590 return Link(target, text, icon=icon, enabled=enabled)1601 return Link(target, text, icon=icon, enabled=enabled)
15911602
1603 def upcomingwork(self):
1604 target = '+upcomingwork'
1605 text = 'Upcoming work for this team'
1606 enabled = False
1607 if getFeatureFlag('registry.upcoming_work_view.enabled'):
1608 enabled = True
1609 return Link(target, text, icon='team', enabled=enabled)
1610
15921611
1593class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):1612class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
15941613
@@ -1622,6 +1641,7 @@
1622 'view_recipes',1641 'view_recipes',
1623 'subscriptions',1642 'subscriptions',
1624 'structural_subscriptions',1643 'structural_subscriptions',
1644 'upcomingwork',
1625 ]1645 ]
16261646
16271647
@@ -2089,6 +2109,10 @@
2089 """A marker interface for the edit navigation menu."""2109 """A marker interface for the edit navigation menu."""
20902110
20912111
2112classImplements(TeamIndexView, ITeamIndexMenu)
2113classImplements(TeamEditView, ITeamEditMenu)
2114
2115
2092class TeamNavigationMenuBase(NavigationMenu, TeamMenuMixin):2116class TeamNavigationMenuBase(NavigationMenu, TeamMenuMixin):
20932117
2094 @property2118 @property
@@ -2135,5 +2159,275 @@
2135 return batch_nav2159 return batch_nav
21362160
21372161
2138classImplements(TeamIndexView, ITeamIndexMenu)2162class TeamUpcomingWorkView(LaunchpadView):
2139classImplements(TeamEditView, ITeamEditMenu)2163 """This view displays work items and bugtasks that are due within 60 days
2164 and are assigned to members of a team.
2165 """
2166
2167 # We'll show bugs and work items targeted to milestones with a due date up
2168 # to DAYS from now.
2169 DAYS = 180
2170
2171 def initialize(self):
2172 super(TeamUpcomingWorkView, self).initialize()
2173 self.workitem_counts = {}
2174 self.bugtask_counts = {}
2175 self.milestones_per_date = {}
2176 for date, containers in self.work_item_containers:
2177 milestones = set()
2178 self.bugtask_counts[date] = 0
2179 self.workitem_counts[date] = 0
2180 for container in containers:
2181 if isinstance(container, AggregatedBugsContainer):
2182 self.bugtask_counts[date] += len(container.items)
2183 else:
2184 self.workitem_counts[date] += len(container.items)
2185 for item in container.items:
2186 milestones.add(item.milestone)
2187 self.milestones_per_date[date] = sorted(
2188 milestones, key=attrgetter('displayname'))
2189
2190 @property
2191 def label(self):
2192 return self.page_title
2193
2194 @property
2195 def page_title(self):
2196 return "Upcoming work for %s" % self.context.displayname
2197
2198 @cachedproperty
2199 def work_item_containers(self):
2200 cutoff_date = datetime.today().date() + timedelta(days=self.DAYS)
2201 result = getWorkItemsDueBefore(self.context, cutoff_date, self.user)
2202 return sorted(result.items(), key=itemgetter(0))
2203
2204
2205class WorkItemContainer:
2206 """A container of work items, assigned to members of a team, whose
2207 milestone is due on a certain date.
2208 """
2209
2210 def __init__(self):
2211 self._items = []
2212
2213 @property
2214 def html_link(self):
2215 raise NotImplementedError("Must be implemented in subclasses")
2216
2217 @property
2218 def priority_title(self):
2219 raise NotImplementedError("Must be implemented in subclasses")
2220
2221 @property
2222 def target_link(self):
2223 raise NotImplementedError("Must be implemented in subclasses")
2224
2225 @property
2226 def assignee_link(self):
2227 raise NotImplementedError("Must be implemented in subclasses")
2228
2229 @property
2230 def items(self):
2231 raise NotImplementedError("Must be implemented in subclasses")
2232
2233 @property
2234 def progress_text(self):
2235 done_items = [item for item in self._items if item.is_complete]
2236 return '{0:.0f}%'.format(100.0 * len(done_items) / len(self._items))
2237
2238 def append(self, item):
2239 self._items.append(item)
2240
2241
2242class SpecWorkItemContainer(WorkItemContainer):
2243 """A container of SpecificationWorkItems wrapped with GenericWorkItem."""
2244
2245 def __init__(self, spec):
2246 super(SpecWorkItemContainer, self).__init__()
2247 self.spec = spec
2248 self.priority = spec.priority
2249 self.target = spec.target
2250 self.assignee = spec.assignee
2251
2252 @property
2253 def html_link(self):
2254 return format_link(self.spec)
2255
2256 @property
2257 def priority_title(self):
2258 return self.priority.title
2259
2260 @property
2261 def target_link(self):
2262 return format_link(self.target)
2263
2264 @property
2265 def assignee_link(self):
2266 if self.assignee is None:
2267 return 'Nobody'
2268 return format_link(self.assignee)
2269
2270 @property
2271 def items(self):
2272 # Sort the work items by status only because they all have the same
2273 # priority.
2274 def sort_key(item):
2275 status_order = {
2276 SpecificationWorkItemStatus.POSTPONED: 5,
2277 SpecificationWorkItemStatus.DONE: 4,
2278 SpecificationWorkItemStatus.INPROGRESS: 3,
2279 SpecificationWorkItemStatus.TODO: 2,
2280 SpecificationWorkItemStatus.BLOCKED: 1,
2281 }
2282 return status_order[item.status]
2283 return sorted(self._items, key=sort_key)
2284
2285
2286class AggregatedBugsContainer(WorkItemContainer):
2287 """A container of BugTasks wrapped with GenericWorkItem."""
2288
2289 @property
2290 def html_link(self):
2291 return 'Bugs targeted to a milestone on this date'
2292
2293 @property
2294 def assignee_link(self):
2295 return 'N/A'
2296
2297 @property
2298 def target_link(self):
2299 return 'N/A'
2300
2301 @property
2302 def priority_title(self):
2303 return 'N/A'
2304
2305 @property
2306 def items(self):
2307 def sort_key(item):
2308 return (item.status.value, item.priority.value)
2309 # Sort by (status, priority) in reverse order because the biggest the
2310 # status/priority the more interesting it is to us.
2311 return sorted(self._items, key=sort_key, reverse=True)
2312
2313
2314class GenericWorkItem:
2315 """A generic piece of work; either a BugTask or a SpecificationWorkItem.
2316
2317 This class wraps a BugTask or a SpecificationWorkItem to provide a
2318 common API so that the template doesn't have to worry about what kind of
2319 work item it's dealing with.
2320 """
2321
2322 def __init__(self, assignee, status, priority, target, title,
2323 bugtask=None, work_item=None):
2324 self.assignee = assignee
2325 self.status = status
2326 self.priority = priority
2327 self.target = target
2328 self.title = title
2329 self._bugtask = bugtask
2330 self._work_item = work_item
2331
2332 @classmethod
2333 def from_bugtask(cls, bugtask):
2334 return cls(
2335 bugtask.assignee, bugtask.status, bugtask.importance,
2336 bugtask.target, bugtask.bug.description, bugtask=bugtask)
2337
2338 @classmethod
2339 def from_workitem(cls, work_item):
2340 assignee = work_item.assignee
2341 if assignee is None:
2342 assignee = work_item.specification.assignee
2343 return cls(
2344 assignee, work_item.status, work_item.specification.priority,
2345 work_item.specification.target, work_item.title,
2346 work_item=work_item)
2347
2348 @property
2349 def display_title(self):
2350 if self._work_item is not None:
2351 return FormattersAPI(self.title).shorten(120)
2352 else:
2353 return format_link(self._bugtask)
2354
2355 @property
2356 def milestone(self):
2357 milestone = self.actual_workitem.milestone
2358 if milestone is None:
2359 assert self._work_item is not None, (
2360 "BugTaks without a milestone must not be here.")
2361 milestone = self._work_item.specification.milestone
2362 return milestone
2363
2364 @property
2365 def actual_workitem(self):
2366 """Return the actual work item that we are wrapping.
2367
2368 This may be either an IBugTask or an ISpecificationWorkItem.
2369 """
2370 if self._work_item is not None:
2371 return self._work_item
2372 else:
2373 return self._bugtask
2374
2375 @property
2376 def is_complete(self):
2377 return self.actual_workitem.is_complete
2378
2379
2380def getWorkItemsDueBefore(team, cutoff_date, user):
2381 """Return a dict mapping dates to lists of WorkItemContainers.
2382
2383 This is a grouping, by milestone due date, of all work items
2384 (SpecificationWorkItems/BugTasks) assigned to any member of this
2385 team.
2386
2387 Only work items whose milestone have a due date between today and the
2388 given cut-off date are included in the results.
2389 """
2390 workitems = team.getAssignedSpecificationWorkItemsDueBefore(cutoff_date)
2391 # For every specification that has work items in the list above, create
2392 # one SpecWorkItemContainer holding the work items from that spec that are
2393 # targeted to the same milestone and assigned to members of the given team.
2394 containers_by_date = {}
2395 containers_by_spec = {}
2396 for workitem in workitems:
2397 spec = workitem.specification
2398 milestone = workitem.milestone
2399 if milestone is None:
2400 milestone = spec.milestone
2401 if milestone.dateexpected not in containers_by_date:
2402 containers_by_date[milestone.dateexpected] = []
2403 container = containers_by_spec.get(spec)
2404 if container is None:
2405 container = SpecWorkItemContainer(spec)
2406 containers_by_spec[spec] = container
2407 containers_by_date[milestone.dateexpected].append(container)
2408 container.append(GenericWorkItem.from_workitem(workitem))
2409
2410 # Sort our containers by priority.
2411 for date in containers_by_date:
2412 containers_by_date[date].sort(
2413 key=attrgetter('priority'), reverse=True)
2414
2415 bugtasks = team.getAssignedBugTasksDueBefore(cutoff_date, user)
2416 bug_containers_by_date = {}
2417 # For every milestone due date, create an AggregatedBugsContainer with all
2418 # the bugtasks targeted to a milestone on that date and assigned to
2419 # members of this team.
2420 for task in bugtasks:
2421 dateexpected = task.milestone.dateexpected
2422 container = bug_containers_by_date.get(dateexpected)
2423 if container is None:
2424 container = AggregatedBugsContainer()
2425 bug_containers_by_date[dateexpected] = container
2426 # Also append our new container to the dictionary we're going
2427 # to return.
2428 if dateexpected not in containers_by_date:
2429 containers_by_date[dateexpected] = []
2430 containers_by_date[dateexpected].append(container)
2431 container.append(GenericWorkItem.from_bugtask(task))
2432
2433 return containers_by_date
21402434
=== added file 'lib/lp/registry/browser/tests/test_team_upcomingwork.py'
--- lib/lp/registry/browser/tests/test_team_upcomingwork.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_team_upcomingwork.py 2012-04-04 08:07:25 +0000
@@ -0,0 +1,288 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6from datetime import (
7 datetime,
8 timedelta,
9 )
10from operator import attrgetter
11
12from zope.security.proxy import removeSecurityProxy
13
14from lp.registry.browser.team import (
15 GenericWorkItem,
16 getWorkItemsDueBefore,
17 WorkItemContainer,
18 )
19from lp.testing import (
20 anonymous_logged_in,
21 BrowserTestCase,
22 TestCase,
23 TestCaseWithFactory,
24 )
25from lp.testing.layers import DatabaseFunctionalLayer
26from lp.testing.pages import (
27 extract_text,
28 find_tags_by_class,
29 )
30from lp.testing.views import create_initialized_view
31
32
33class Test_getWorkItemsDueBefore(TestCaseWithFactory):
34
35 layer = DatabaseFunctionalLayer
36
37 def setUp(self):
38 super(Test_getWorkItemsDueBefore, self).setUp()
39 self.today = datetime.today().date()
40 current_milestone = self.factory.makeMilestone(
41 dateexpected=self.today)
42 self.current_milestone = current_milestone
43 self.future_milestone = self.factory.makeMilestone(
44 product=current_milestone.product,
45 dateexpected=datetime(2060, 1, 1))
46 self.team = self.factory.makeTeam()
47
48 def test_basic(self):
49 spec = self.factory.makeSpecification(
50 product=self.current_milestone.product,
51 assignee=self.team.teamowner, milestone=self.current_milestone)
52 workitem = self.factory.makeSpecificationWorkItem(
53 title=u'workitem 1', specification=spec)
54 bugtask = self.factory.makeBug(
55 milestone=self.current_milestone).bugtasks[0]
56 removeSecurityProxy(bugtask).assignee = self.team.teamowner
57
58 workitems = getWorkItemsDueBefore(
59 self.team, self.current_milestone.dateexpected, user=None)
60
61 self.assertEqual(
62 [self.current_milestone.dateexpected], workitems.keys())
63 containers = workitems[self.current_milestone.dateexpected]
64 # We have one container for the work item from the spec and another
65 # one for the bugtask.
66 self.assertEqual(2, len(containers))
67 [workitem_container, bugtask_container] = containers
68
69 self.assertEqual(1, len(bugtask_container.items))
70 self.assertEqual(bugtask, bugtask_container.items[0].actual_workitem)
71
72 self.assertEqual(1, len(workitem_container.items))
73 self.assertEqual(
74 workitem, workitem_container.items[0].actual_workitem)
75
76 def test_foreign_container(self):
77 # This spec is targeted to a person who's not a member of our team, so
78 # only those workitems that are explicitly assigned to a member of our
79 # team will be returned.
80 spec = self.factory.makeSpecification(
81 product=self.current_milestone.product,
82 milestone=self.current_milestone,
83 assignee=self.factory.makePerson())
84 self.factory.makeSpecificationWorkItem(
85 title=u'workitem 1', specification=spec)
86 workitem = self.factory.makeSpecificationWorkItem(
87 title=u'workitem 2', specification=spec,
88 assignee=self.team.teamowner)
89
90 workitems = getWorkItemsDueBefore(
91 self.team, self.current_milestone.dateexpected, user=None)
92
93 self.assertEqual(
94 [self.current_milestone.dateexpected], workitems.keys())
95 containers = workitems[self.current_milestone.dateexpected]
96 self.assertEqual(1, len(containers))
97 [container] = containers
98 self.assertEqual(1, len(container.items))
99 self.assertEqual(workitem, container.items[0].actual_workitem)
100
101 def test_future_container(self):
102 spec = self.factory.makeSpecification(
103 product=self.current_milestone.product,
104 assignee=self.team.teamowner)
105 # This workitem is targeted to a future milestone so it won't be in
106 # our results below.
107 self.factory.makeSpecificationWorkItem(
108 title=u'workitem 1', specification=spec,
109 milestone=self.future_milestone)
110 current_wi = self.factory.makeSpecificationWorkItem(
111 title=u'workitem 2', specification=spec,
112 milestone=self.current_milestone)
113
114 workitems = getWorkItemsDueBefore(
115 self.team, self.current_milestone.dateexpected, user=None)
116
117 self.assertEqual(
118 [self.current_milestone.dateexpected], workitems.keys())
119 containers = workitems[self.current_milestone.dateexpected]
120 self.assertEqual(1, len(containers))
121 [container] = containers
122 self.assertEqual(1, len(container.items))
123 self.assertEqual(current_wi, container.items[0].actual_workitem)
124
125
126class TestGenericWorkItem(TestCaseWithFactory):
127
128 layer = DatabaseFunctionalLayer
129
130 def setUp(self):
131 super(TestGenericWorkItem, self).setUp()
132 today = datetime.today().date()
133 self.milestone = self.factory.makeMilestone(dateexpected=today)
134
135 def test_from_bugtask(self):
136 bugtask = self.factory.makeBug(milestone=self.milestone).bugtasks[0]
137 workitem = GenericWorkItem.from_bugtask(bugtask)
138 self.assertEqual(workitem.assignee, bugtask.assignee)
139 self.assertEqual(workitem.status, bugtask.status)
140 self.assertEqual(workitem.priority, bugtask.importance)
141 self.assertEqual(workitem.target, bugtask.target)
142 self.assertEqual(workitem.title, bugtask.bug.description)
143 self.assertEqual(workitem.actual_workitem, bugtask)
144
145 def test_from_workitem(self):
146 workitem = self.factory.makeSpecificationWorkItem(
147 milestone=self.milestone)
148 generic_wi = GenericWorkItem.from_workitem(workitem)
149 self.assertEqual(generic_wi.assignee, workitem.assignee)
150 self.assertEqual(generic_wi.status, workitem.status)
151 self.assertEqual(generic_wi.priority, workitem.specification.priority)
152 self.assertEqual(generic_wi.target, workitem.specification.target)
153 self.assertEqual(generic_wi.title, workitem.title)
154 self.assertEqual(generic_wi.actual_workitem, workitem)
155
156
157class TestWorkItemContainer(TestCase):
158
159 class MockWorkItem:
160
161 def __init__(self, is_complete):
162 self.is_complete = is_complete
163
164 def test_progress_text(self):
165 container = WorkItemContainer()
166 container.append(self.MockWorkItem(True))
167 container.append(self.MockWorkItem(False))
168 container.append(self.MockWorkItem(True))
169 self.assertEqual('67%', container.progress_text)
170
171
172class TestTeamUpcomingWork(BrowserTestCase):
173
174 layer = DatabaseFunctionalLayer
175
176 def setUp(self):
177 super(TestTeamUpcomingWork, self).setUp()
178 self.today = datetime.today().date()
179 self.tomorrow = self.today + timedelta(days=1)
180 self.today_milestone = self.factory.makeMilestone(
181 dateexpected=self.today)
182 self.tomorrow_milestone = self.factory.makeMilestone(
183 dateexpected=self.tomorrow)
184 self.team = self.factory.makeTeam()
185
186 def test_basic(self):
187 workitem1 = self.factory.makeSpecificationWorkItem(
188 assignee=self.team.teamowner, milestone=self.today_milestone)
189 workitem2 = self.factory.makeSpecificationWorkItem(
190 assignee=self.team.teamowner, milestone=self.tomorrow_milestone)
191 bugtask1 = self.factory.makeBug(
192 milestone=self.today_milestone).bugtasks[0]
193 bugtask2 = self.factory.makeBug(
194 milestone=self.tomorrow_milestone).bugtasks[0]
195 for bugtask in [bugtask1, bugtask2]:
196 removeSecurityProxy(bugtask).assignee = self.team.teamowner
197
198 browser = self.getViewBrowser(
199 self.team, view_name='+upcomingwork', no_login=True)
200
201 groups = find_tags_by_class(browser.contents, 'workitems-group')
202 self.assertEqual(2, len(groups))
203 todays_group = extract_text(groups[0])
204 tomorrows_group = extract_text(groups[1])
205 self.assertStartsWith(
206 todays_group, 'Work items due in %s' % self.today)
207 self.assertIn(workitem1.title, todays_group)
208 with anonymous_logged_in():
209 self.assertIn(bugtask1.bug.title, todays_group)
210
211 self.assertStartsWith(
212 tomorrows_group, 'Work items due in %s' % self.tomorrow)
213 self.assertIn(workitem2.title, tomorrows_group)
214 with anonymous_logged_in():
215 self.assertIn(bugtask2.bug.title, tomorrows_group)
216
217
218class TestTeamUpcomingWorkView(TestCaseWithFactory):
219
220 layer = DatabaseFunctionalLayer
221
222 def setUp(self):
223 super(TestTeamUpcomingWorkView, self).setUp()
224 self.today = datetime.today().date()
225 self.tomorrow = self.today + timedelta(days=1)
226 self.today_milestone = self.factory.makeMilestone(
227 dateexpected=self.today)
228 self.tomorrow_milestone = self.factory.makeMilestone(
229 dateexpected=self.tomorrow)
230 self.team = self.factory.makeTeam()
231
232 def test_workitem_counts(self):
233 self.factory.makeSpecificationWorkItem(
234 assignee=self.team.teamowner, milestone=self.today_milestone)
235 self.factory.makeSpecificationWorkItem(
236 assignee=self.team.teamowner, milestone=self.today_milestone)
237 self.factory.makeSpecificationWorkItem(
238 assignee=self.team.teamowner, milestone=self.tomorrow_milestone)
239
240 view = create_initialized_view(self.team, '+upcomingwork')
241 self.assertEqual(2, view.workitem_counts[self.today])
242 self.assertEqual(1, view.workitem_counts[self.tomorrow])
243
244 def test_bugtask_counts(self):
245 bugtask1 = self.factory.makeBug(
246 milestone=self.today_milestone).bugtasks[0]
247 bugtask2 = self.factory.makeBug(
248 milestone=self.tomorrow_milestone).bugtasks[0]
249 bugtask3 = self.factory.makeBug(
250 milestone=self.tomorrow_milestone).bugtasks[0]
251 for bugtask in [bugtask1, bugtask2, bugtask3]:
252 removeSecurityProxy(bugtask).assignee = self.team.teamowner
253
254 view = create_initialized_view(self.team, '+upcomingwork')
255 self.assertEqual(1, view.bugtask_counts[self.today])
256 self.assertEqual(2, view.bugtask_counts[self.tomorrow])
257
258 def test_milestones_per_date(self):
259 another_milestone_due_today = self.factory.makeMilestone(
260 dateexpected=self.today)
261 self.factory.makeSpecificationWorkItem(
262 assignee=self.team.teamowner, milestone=self.today_milestone)
263 self.factory.makeSpecificationWorkItem(
264 assignee=self.team.teamowner,
265 milestone=another_milestone_due_today)
266 self.factory.makeSpecificationWorkItem(
267 assignee=self.team.teamowner, milestone=self.tomorrow_milestone)
268
269 view = create_initialized_view(self.team, '+upcomingwork')
270 self.assertEqual(
271 sorted([self.today_milestone, another_milestone_due_today],
272 key=attrgetter('displayname')),
273 view.milestones_per_date[self.today])
274 self.assertEqual(
275 [self.tomorrow_milestone],
276 view.milestones_per_date[self.tomorrow])
277
278 def test_work_item_containers_are_sorted_by_date(self):
279 self.factory.makeSpecificationWorkItem(
280 assignee=self.team.teamowner, milestone=self.today_milestone)
281 self.factory.makeSpecificationWorkItem(
282 assignee=self.team.teamowner, milestone=self.tomorrow_milestone)
283
284 view = create_initialized_view(self.team, '+upcomingwork')
285 self.assertEqual(2, len(view.work_item_containers))
286 self.assertEqual(
287 [self.today, self.tomorrow],
288 [date for date, containers in view.work_item_containers])
0289
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-03-29 06:02:46 +0000
+++ lib/lp/registry/model/person.py 2012-04-04 08:07:25 +0000
@@ -128,11 +128,11 @@
128 SpecificationImplementationStatus,128 SpecificationImplementationStatus,
129 SpecificationSort,129 SpecificationSort,
130 )130 )
131from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
132from lp.blueprints.model.specification import (131from lp.blueprints.model.specification import (
133 HasSpecificationsMixin,132 HasSpecificationsMixin,
134 Specification,133 Specification,
135 )134 )
135from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
136from lp.bugs.interfaces.bugtarget import IBugTarget136from lp.bugs.interfaces.bugtarget import IBugTarget
137from lp.bugs.interfaces.bugtask import (137from lp.bugs.interfaces.bugtask import (
138 BugTaskSearchParams,138 BugTaskSearchParams,
@@ -1535,7 +1535,7 @@
1535 milestone_dateexpected_after=today)1535 milestone_dateexpected_after=today)
15361536
1537 # Cast to a list to avoid DecoratedResultSet running pre_iter_hook1537 # Cast to a list to avoid DecoratedResultSet running pre_iter_hook
1538 # multiple times when load_related() iterates over through the tasks.1538 # multiple times when load_related() iterates over the tasks.
1539 tasks = list(getUtility(IBugTaskSet).search(search_params))1539 tasks = list(getUtility(IBugTaskSet).search(search_params))
1540 # Eager load the things we need that are not already eager loaded by1540 # Eager load the things we need that are not already eager loaded by
1541 # BugTaskSet.search().1541 # BugTaskSet.search().
15421542
=== modified file 'lib/lp/registry/templates/team-index.pt'
--- lib/lp/registry/templates/team-index.pt 2011-01-04 16:08:57 +0000
+++ lib/lp/registry/templates/team-index.pt 2012-04-04 08:07:25 +0000
@@ -64,6 +64,13 @@
64 Related software and packages64 Related software and packages
65 </a>65 </a>
66 </li>66 </li>
67 <li
68 tal:define="link context/menu:overview/upcomingwork"
69 tal:condition="link/enabled">
70 <a class="sprite info" tal:attributes="href link/fmt:url">
71 Upcoming work assigned to members of this team
72 </a>
73 </li>
67 </ul>74 </ul>
6875
69 <div class="yui-g">76 <div class="yui-g">
7077
=== added file 'lib/lp/registry/templates/team-upcomingwork.pt'
--- lib/lp/registry/templates/team-upcomingwork.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/team-upcomingwork.pt 2012-04-04 08:07:25 +0000
@@ -0,0 +1,105 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8<body>
9
10<head>
11 <tal:block metal:fill-slot="head_epilogue">
12 <script type="text/javascript">
13 LPJS.use('node', 'event', 'lp.app.widgets.expander', function(Y) {
14 Y.on('domready', function() {
15 Y.all('[class=expandable]').each(function(e) {
16 var expander_icon = e.one('[class=expander]');
17 // Our parent's first sibling is the tbody we want to collapse.
18 var widget_body = e.ancestor().next();
19 var expander = new Y.lp.app.widgets.expander.Expander(
20 expander_icon, widget_body);
21 expander.setUp(true);
22 })
23 })
24 });
25 </script>
26 <!-- TODO: Once this page is done and no longer guarded with a feature
27 flag, move this to the appropriate css files. -->
28 <style type="text/css">
29 .collapsible-body {
30 background-color: #eee;
31 }
32 tr.padded td {
33 padding-left: 2em;
34 }
35 </style>
36 </tal:block>
37</head>
38
39<div metal:fill-slot="main">
40
41 <div tal:repeat="pair view/work_item_containers" class="workitems-group">
42 <div tal:define="date python: pair[0]; containers python: pair[1]">
43 <h2>Work items due in <span tal:replace="date/fmt:date" /></h2>
44 <p>
45 From
46 <tal:milestones repeat="milestone python: view.milestones_per_date[date]">
47 <a tal:replace="structure milestone/fmt:link"
48 /><span tal:condition="not: repeat/milestone/end">,</span>
49 </tal:milestones>
50 </p>
51
52 <p>
53 There are <span tal:replace="python: view.workitem_counts[date]" />
54 Blueprint work items and
55 <span tal:replace="python: view.bugtask_counts[date]" /> Bugs due
56 in <span tal:content="date/fmt:date" /> which are assigned to members
57 of this team.
58 </p>
59
60 <table class="listing">
61 <thead>
62 <tr>
63 <th>Blueprint</th>
64 <th>Target</th>
65 <th>Assignee</th>
66 <th>Priority</th>
67 <th>Progress</th>
68 </tr>
69 </thead>
70 <tal:containers repeat="container containers">
71 <tbody>
72 <tr class="expandable">
73 <td>
74 <a href="#" class="expander">&nbsp;</a>
75 <span tal:replace="structure container/html_link" />
76 </td>
77 <td tal:content="structure container/target_link" />
78 <td tal:content="structure container/assignee_link" />
79 <td tal:content="container/priority_title" />
80 <td><span tal:replace="container/progress_text" /> done</td>
81 </tr>
82 </tbody>
83 <tbody class="collapsible-body">
84 <tr tal:repeat="workitem container/items" class="padded">
85 <td tal:content="structure workitem/display_title" />
86 <td>
87 <span tal:condition="not: container/spec|nothing"
88 tal:replace="structure workitem/target/fmt:link" />
89 </td>
90 <td><a tal:replace="structure workitem/assignee/fmt:link" /></td>
91 <td>
92 <span tal:condition="not: container/spec|nothing"
93 tal:replace="workitem/priority/title" />
94 </td>
95 <td><span tal:replace="workitem/status/title" /></td>
96 </tr>
97 </tbody>
98 </tal:containers>
99 </table>
100 </div>
101 </div>
102</div>
103
104</body>
105</html>
0106
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2012-04-04 05:49:36 +0000
+++ lib/lp/services/features/flags.py 2012-04-04 08:07:25 +0000
@@ -315,6 +315,12 @@
315 '',315 '',
316 '',316 '',
317 ''),317 ''),
318 ('registry.upcoming_work_view.enabled',
319 'boolean',
320 ('If true, the new upcoming work view of teams is available.'),
321 '',
322 '',
323 ''),
318 ])324 ])
319325
320# The set of all flag names that are documented.326# The set of all flag names that are documented.
321327
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-04-04 05:46:26 +0000
+++ lib/lp/testing/factory.py 2012-04-04 08:07:25 +0000
@@ -2134,7 +2134,13 @@
2134 if title is None:2134 if title is None:
2135 title = self.getUniqueString(u'title')2135 title = self.getUniqueString(u'title')
2136 if specification is None:2136 if specification is None:
2137 specification = self.makeSpecification()2137 product = None
2138 distribution = None
2139 if milestone is not None:
2140 product = milestone.product
2141 distribution = milestone.distribution
2142 specification = self.makeSpecification(
2143 product=product, distribution=distribution)
2138 if sequence is None:2144 if sequence is None:
2139 sequence = self.getUniqueInteger()2145 sequence = self.getUniqueInteger()
2140 work_item = removeSecurityProxy(specification).newWorkItem(2146 work_item = removeSecurityProxy(specification).newWorkItem(