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

Proposed by Guilherme Salgado
Status: Merged
Approved by: Richard Harding
Approved revision: no longer in the source branch.
Merge reported by: Данило Шеган
Merged at revision: not available
Proposed branch: lp:~linaro-infrastructure/launchpad/team-engineering-view-ui
Merge into: lp:launchpad
Diff against target: 905 lines (+744/-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 (+289/-3)
lib/lp/registry/browser/tests/test_team_upcomingwork.py (+304/-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 (+112/-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
j.c.sackett (community) Approve
Richard Harding (community) code* Approve
Steve Kowalik code Pending
Review via email: mp+100956@code.launchpad.net

This proposal supersedes a proposal from 2012-04-03.

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal

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.

Revision history for this message
Richard Harding (rharding) wrote :

Thanks. the XSS fix looks good, appreciate the update.

review: Approve (code*)
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks alright; one quibble, though. On line 783 I see:

+ <!-- TODO: Once this page is done and no longer guarded with a feature
+ flag, move this to the appropriate css files. -->

I would ask that you convert this into an XXX comment per our XXXPolicy (https://dev.launchpad.net/PolicyandProcess/XXXPolicy).

+ <tal:XXX replace="nothing">20120405 salgado: [COMMENT TEXT]</tal:XXX>

As this is a fairly minor change, I'm marking approved with the understanding this will be taken care of.

Thanks!

review: Approve
Revision history for this message
Deepti B. Kalakeri (deeptik) wrote :

The changes in revision 14961 looks good, except am not sure if we should put salgado's name in there as he is no longer with the team and would not be there to address the things in future?

Looks good otherwise +1.

Thanks!!!
Deepti.

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

Well, salgado is back to Canonical, so he'll be closer to this code than before ;-). Actually, I guess we should ping him to merge this code as apart from him and danilo nobody in Linaro Infra team can do 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-04-04 22:30:45 +0000
+++ lib/lp/blueprints/interfaces/specificationworkitem.py 2012-04-05 18:43:23 +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-04-04 22:30:45 +0000
+++ lib/lp/blueprints/model/specificationworkitem.py 2012-04-05 18:43:23 +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-04-04 22:30:45 +0000
+++ lib/lp/registry/browser/configure.zcml 2012-04-05 18:43:23 +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-04-04 22:30:45 +0000
+++ lib/lp/registry/browser/team.py 2012-04-05 18:43:23 +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,10 @@
78 custom_widget,83 custom_widget,
79 LaunchpadFormView,84 LaunchpadFormView,
80 )85 )
81from lp.app.browser.tales import PersonFormatterAPI86from lp.app.browser.tales import (
87 format_link,
88 PersonFormatterAPI,
89 )
82from lp.app.errors import UnexpectedFormData90from lp.app.errors import UnexpectedFormData
83from lp.app.validators import LaunchpadValidationError91from lp.app.validators import LaunchpadValidationError
84from lp.app.validators.validation import validate_new_team_email92from lp.app.validators.validation import validate_new_team_email
@@ -89,6 +97,7 @@
89 )97 )
90from lp.app.widgets.owner import HiddenUserWidget98from lp.app.widgets.owner import HiddenUserWidget
91from lp.app.widgets.popup import PersonPickerWidget99from lp.app.widgets.popup import PersonPickerWidget
100from lp.blueprints.enums import SpecificationWorkItemStatus
92from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin101from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
93from lp.registry.browser.branding import BrandingChangeView102from lp.registry.browser.branding import BrandingChangeView
94from lp.registry.browser.mailinglists import enabled_with_active_mailing_list103from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
@@ -142,6 +151,7 @@
142 )151 )
143from lp.security import ModerateByRegistryExpertsOrAdmins152from lp.security import ModerateByRegistryExpertsOrAdmins
144from lp.services.config import config153from lp.services.config import config
154from lp.services.features import getFeatureFlag
145from lp.services.fields import PublicPersonChoice155from lp.services.fields import PublicPersonChoice
146from lp.services.identity.interfaces.emailaddress import IEmailAddressSet156from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
147from lp.services.privacy.interfaces import IObjectPrivacy157from lp.services.privacy.interfaces import IObjectPrivacy
@@ -1589,6 +1599,14 @@
1589 icon = 'add'1599 icon = 'add'
1590 return Link(target, text, icon=icon, enabled=enabled)1600 return Link(target, text, icon=icon, enabled=enabled)
15911601
1602 def upcomingwork(self):
1603 target = '+upcomingwork'
1604 text = 'Upcoming work for this team'
1605 enabled = False
1606 if getFeatureFlag('registry.upcoming_work_view.enabled'):
1607 enabled = True
1608 return Link(target, text, icon='team', enabled=enabled)
1609
15921610
1593class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):1611class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
15941612
@@ -1622,6 +1640,7 @@
1622 'view_recipes',1640 'view_recipes',
1623 'subscriptions',1641 'subscriptions',
1624 'structural_subscriptions',1642 'structural_subscriptions',
1643 'upcomingwork',
1625 ]1644 ]
16261645
16271646
@@ -2089,6 +2108,10 @@
2089 """A marker interface for the edit navigation menu."""2108 """A marker interface for the edit navigation menu."""
20902109
20912110
2111classImplements(TeamIndexView, ITeamIndexMenu)
2112classImplements(TeamEditView, ITeamEditMenu)
2113
2114
2092class TeamNavigationMenuBase(NavigationMenu, TeamMenuMixin):2115class TeamNavigationMenuBase(NavigationMenu, TeamMenuMixin):
20932116
2094 @property2117 @property
@@ -2135,5 +2158,268 @@
2135 return batch_nav2158 return batch_nav
21362159
21372160
2138classImplements(TeamIndexView, ITeamIndexMenu)2161class TeamUpcomingWorkView(LaunchpadView):
2139classImplements(TeamEditView, ITeamEditMenu)2162 """This view displays work items and bugtasks that are due within 60 days
2163 and are assigned to members of a team.
2164 """
2165
2166 # We'll show bugs and work items targeted to milestones with a due date up
2167 # to DAYS from now.
2168 DAYS = 180
2169
2170 def initialize(self):
2171 super(TeamUpcomingWorkView, self).initialize()
2172 self.workitem_counts = {}
2173 self.bugtask_counts = {}
2174 self.milestones_per_date = {}
2175 for date, containers in self.work_item_containers:
2176 milestones = set()
2177 self.bugtask_counts[date] = 0
2178 self.workitem_counts[date] = 0
2179 for container in containers:
2180 if isinstance(container, AggregatedBugsContainer):
2181 self.bugtask_counts[date] += len(container.items)
2182 else:
2183 self.workitem_counts[date] += len(container.items)
2184 for item in container.items:
2185 milestones.add(item.milestone)
2186 self.milestones_per_date[date] = sorted(
2187 milestones, key=attrgetter('displayname'))
2188
2189 @property
2190 def label(self):
2191 return self.page_title
2192
2193 @property
2194 def page_title(self):
2195 return "Upcoming work for %s" % self.context.displayname
2196
2197 @cachedproperty
2198 def work_item_containers(self):
2199 cutoff_date = datetime.today().date() + timedelta(days=self.DAYS)
2200 result = getWorkItemsDueBefore(self.context, cutoff_date, self.user)
2201 return sorted(result.items(), key=itemgetter(0))
2202
2203
2204class WorkItemContainer:
2205 """A container of work items, assigned to members of a team, whose
2206 milestone is due on a certain date.
2207 """
2208
2209 def __init__(self):
2210 self._items = []
2211
2212 @property
2213 def html_link(self):
2214 raise NotImplementedError("Must be implemented in subclasses")
2215
2216 @property
2217 def priority_title(self):
2218 raise NotImplementedError("Must be implemented in subclasses")
2219
2220 @property
2221 def target_link(self):
2222 raise NotImplementedError("Must be implemented in subclasses")
2223
2224 @property
2225 def assignee_link(self):
2226 raise NotImplementedError("Must be implemented in subclasses")
2227
2228 @property
2229 def items(self):
2230 raise NotImplementedError("Must be implemented in subclasses")
2231
2232 @property
2233 def progress_text(self):
2234 done_items = [item for item in self._items if item.is_complete]
2235 return '{0:.0f}%'.format(100.0 * len(done_items) / len(self._items))
2236
2237 def append(self, item):
2238 self._items.append(item)
2239
2240
2241class SpecWorkItemContainer(WorkItemContainer):
2242 """A container of SpecificationWorkItems wrapped with GenericWorkItem."""
2243
2244 def __init__(self, spec):
2245 super(SpecWorkItemContainer, self).__init__()
2246 self.spec = spec
2247 self.priority = spec.priority
2248 self.target = spec.target
2249 self.assignee = spec.assignee
2250
2251 @property
2252 def html_link(self):
2253 return format_link(self.spec)
2254
2255 @property
2256 def priority_title(self):
2257 return self.priority.title
2258
2259 @property
2260 def target_link(self):
2261 return format_link(self.target)
2262
2263 @property
2264 def assignee_link(self):
2265 if self.assignee is None:
2266 return 'Nobody'
2267 return format_link(self.assignee)
2268
2269 @property
2270 def items(self):
2271 # Sort the work items by status only because they all have the same
2272 # priority.
2273 def sort_key(item):
2274 status_order = {
2275 SpecificationWorkItemStatus.POSTPONED: 5,
2276 SpecificationWorkItemStatus.DONE: 4,
2277 SpecificationWorkItemStatus.INPROGRESS: 3,
2278 SpecificationWorkItemStatus.TODO: 2,
2279 SpecificationWorkItemStatus.BLOCKED: 1,
2280 }
2281 return status_order[item.status]
2282 return sorted(self._items, key=sort_key)
2283
2284
2285class AggregatedBugsContainer(WorkItemContainer):
2286 """A container of BugTasks wrapped with GenericWorkItem."""
2287
2288 @property
2289 def html_link(self):
2290 return 'Bugs targeted to a milestone on this date'
2291
2292 @property
2293 def assignee_link(self):
2294 return 'N/A'
2295
2296 @property
2297 def target_link(self):
2298 return 'N/A'
2299
2300 @property
2301 def priority_title(self):
2302 return 'N/A'
2303
2304 @property
2305 def items(self):
2306 def sort_key(item):
2307 return (item.status.value, item.priority.value)
2308 # Sort by (status, priority) in reverse order because the biggest the
2309 # status/priority the more interesting it is to us.
2310 return sorted(self._items, key=sort_key, reverse=True)
2311
2312
2313class GenericWorkItem:
2314 """A generic piece of work; either a BugTask or a SpecificationWorkItem.
2315
2316 This class wraps a BugTask or a SpecificationWorkItem to provide a
2317 common API so that the template doesn't have to worry about what kind of
2318 work item it's dealing with.
2319 """
2320
2321 def __init__(self, assignee, status, priority, target, title,
2322 bugtask=None, work_item=None):
2323 self.assignee = assignee
2324 self.status = status
2325 self.priority = priority
2326 self.target = target
2327 self.title = title
2328 self._bugtask = bugtask
2329 self._work_item = work_item
2330
2331 @classmethod
2332 def from_bugtask(cls, bugtask):
2333 return cls(
2334 bugtask.assignee, bugtask.status, bugtask.importance,
2335 bugtask.target, bugtask.bug.description, bugtask=bugtask)
2336
2337 @classmethod
2338 def from_workitem(cls, work_item):
2339 assignee = work_item.assignee
2340 if assignee is None:
2341 assignee = work_item.specification.assignee
2342 return cls(
2343 assignee, work_item.status, work_item.specification.priority,
2344 work_item.specification.target, work_item.title,
2345 work_item=work_item)
2346
2347 @property
2348 def milestone(self):
2349 milestone = self.actual_workitem.milestone
2350 if milestone is None:
2351 assert self._work_item is not None, (
2352 "BugTaks without a milestone must not be here.")
2353 milestone = self._work_item.specification.milestone
2354 return milestone
2355
2356 @property
2357 def actual_workitem(self):
2358 """Return the actual work item that we are wrapping.
2359
2360 This may be either an IBugTask or an ISpecificationWorkItem.
2361 """
2362 if self._work_item is not None:
2363 return self._work_item
2364 else:
2365 return self._bugtask
2366
2367 @property
2368 def is_complete(self):
2369 return self.actual_workitem.is_complete
2370
2371
2372def getWorkItemsDueBefore(team, cutoff_date, user):
2373 """Return a dict mapping dates to lists of WorkItemContainers.
2374
2375 This is a grouping, by milestone due date, of all work items
2376 (SpecificationWorkItems/BugTasks) assigned to any member of this
2377 team.
2378
2379 Only work items whose milestone have a due date between today and the
2380 given cut-off date are included in the results.
2381 """
2382 workitems = team.getAssignedSpecificationWorkItemsDueBefore(cutoff_date)
2383 # For every specification that has work items in the list above, create
2384 # one SpecWorkItemContainer holding the work items from that spec that are
2385 # targeted to the same milestone and assigned to members of the given team.
2386 containers_by_date = {}
2387 containers_by_spec = {}
2388 for workitem in workitems:
2389 spec = workitem.specification
2390 milestone = workitem.milestone
2391 if milestone is None:
2392 milestone = spec.milestone
2393 if milestone.dateexpected not in containers_by_date:
2394 containers_by_date[milestone.dateexpected] = []
2395 container = containers_by_spec.get(spec)
2396 if container is None:
2397 container = SpecWorkItemContainer(spec)
2398 containers_by_spec[spec] = container
2399 containers_by_date[milestone.dateexpected].append(container)
2400 container.append(GenericWorkItem.from_workitem(workitem))
2401
2402 # Sort our containers by priority.
2403 for date in containers_by_date:
2404 containers_by_date[date].sort(
2405 key=attrgetter('priority'), reverse=True)
2406
2407 bugtasks = team.getAssignedBugTasksDueBefore(cutoff_date, user)
2408 bug_containers_by_date = {}
2409 # For every milestone due date, create an AggregatedBugsContainer with all
2410 # the bugtasks targeted to a milestone on that date and assigned to
2411 # members of this team.
2412 for task in bugtasks:
2413 dateexpected = task.milestone.dateexpected
2414 container = bug_containers_by_date.get(dateexpected)
2415 if container is None:
2416 container = AggregatedBugsContainer()
2417 bug_containers_by_date[dateexpected] = container
2418 # Also append our new container to the dictionary we're going
2419 # to return.
2420 if dateexpected not in containers_by_date:
2421 containers_by_date[dateexpected] = []
2422 containers_by_date[dateexpected].append(container)
2423 container.append(GenericWorkItem.from_bugtask(task))
2424
2425 return containers_by_date
21402426
=== 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-05 18:43:23 +0000
@@ -0,0 +1,304 @@
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 def test_no_xss_on_workitem_title(self):
218 self.factory.makeSpecificationWorkItem(
219 title=u"<script>window.alert('XSS')</script>",
220 assignee=self.team.teamowner, milestone=self.today_milestone)
221
222 browser = self.getViewBrowser(
223 self.team, view_name='+upcomingwork', no_login=True)
224
225 groups = find_tags_by_class(browser.contents, 'collapsible-body')
226 self.assertEqual(1, len(groups))
227 tbody = groups[0]
228 title_td = tbody.findChildren('td')[0]
229 self.assertEqual(
230 "<td>\n<span>&lt;script&gt;window.alert('XSS')&lt;/script&gt;"
231 "</span>\n</td>", str(title_td))
232
233
234class TestTeamUpcomingWorkView(TestCaseWithFactory):
235
236 layer = DatabaseFunctionalLayer
237
238 def setUp(self):
239 super(TestTeamUpcomingWorkView, self).setUp()
240 self.today = datetime.today().date()
241 self.tomorrow = self.today + timedelta(days=1)
242 self.today_milestone = self.factory.makeMilestone(
243 dateexpected=self.today)
244 self.tomorrow_milestone = self.factory.makeMilestone(
245 dateexpected=self.tomorrow)
246 self.team = self.factory.makeTeam()
247
248 def test_workitem_counts(self):
249 self.factory.makeSpecificationWorkItem(
250 assignee=self.team.teamowner, milestone=self.today_milestone)
251 self.factory.makeSpecificationWorkItem(
252 assignee=self.team.teamowner, milestone=self.today_milestone)
253 self.factory.makeSpecificationWorkItem(
254 assignee=self.team.teamowner, milestone=self.tomorrow_milestone)
255
256 view = create_initialized_view(self.team, '+upcomingwork')
257 self.assertEqual(2, view.workitem_counts[self.today])
258 self.assertEqual(1, view.workitem_counts[self.tomorrow])
259
260 def test_bugtask_counts(self):
261 bugtask1 = self.factory.makeBug(
262 milestone=self.today_milestone).bugtasks[0]
263 bugtask2 = self.factory.makeBug(
264 milestone=self.tomorrow_milestone).bugtasks[0]
265 bugtask3 = self.factory.makeBug(
266 milestone=self.tomorrow_milestone).bugtasks[0]
267 for bugtask in [bugtask1, bugtask2, bugtask3]:
268 removeSecurityProxy(bugtask).assignee = self.team.teamowner
269
270 view = create_initialized_view(self.team, '+upcomingwork')
271 self.assertEqual(1, view.bugtask_counts[self.today])
272 self.assertEqual(2, view.bugtask_counts[self.tomorrow])
273
274 def test_milestones_per_date(self):
275 another_milestone_due_today = self.factory.makeMilestone(
276 dateexpected=self.today)
277 self.factory.makeSpecificationWorkItem(
278 assignee=self.team.teamowner, milestone=self.today_milestone)
279 self.factory.makeSpecificationWorkItem(
280 assignee=self.team.teamowner,
281 milestone=another_milestone_due_today)
282 self.factory.makeSpecificationWorkItem(
283 assignee=self.team.teamowner, milestone=self.tomorrow_milestone)
284
285 view = create_initialized_view(self.team, '+upcomingwork')
286 self.assertEqual(
287 sorted([self.today_milestone, another_milestone_due_today],
288 key=attrgetter('displayname')),
289 view.milestones_per_date[self.today])
290 self.assertEqual(
291 [self.tomorrow_milestone],
292 view.milestones_per_date[self.tomorrow])
293
294 def test_work_item_containers_are_sorted_by_date(self):
295 self.factory.makeSpecificationWorkItem(
296 assignee=self.team.teamowner, milestone=self.today_milestone)
297 self.factory.makeSpecificationWorkItem(
298 assignee=self.team.teamowner, milestone=self.tomorrow_milestone)
299
300 view = create_initialized_view(self.team, '+upcomingwork')
301 self.assertEqual(2, len(view.work_item_containers))
302 self.assertEqual(
303 [self.today, self.tomorrow],
304 [date for date, containers in view.work_item_containers])
0305
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-04-04 22:30:45 +0000
+++ lib/lp/registry/model/person.py 2012-04-05 18:43:23 +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 2012-04-04 22:30:45 +0000
+++ lib/lp/registry/templates/team-index.pt 2012-04-05 18:43:23 +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-05 18:43:23 +0000
@@ -0,0 +1,112 @@
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 <tal:XXX replace="nothing">20120405 salgado: Once this page is done and no
27 longer guarded with a feature flag, move this to the appropriate css
28 files.
29 </tal:XXX>
30 <style type="text/css">
31 .collapsible-body {
32 background-color: #eee;
33 }
34 tr.padded td {
35 padding-left: 2em;
36 }
37 </style>
38 </tal:block>
39</head>
40
41<div metal:fill-slot="main">
42
43 <div tal:repeat="pair view/work_item_containers" class="workitems-group">
44 <div tal:define="date python: pair[0]; containers python: pair[1]">
45 <h2>Work items due in <span tal:replace="date/fmt:date" /></h2>
46 <p>
47 From
48 <tal:milestones repeat="milestone python: view.milestones_per_date[date]">
49 <a tal:replace="structure milestone/fmt:link"
50 /><span tal:condition="not: repeat/milestone/end">,</span>
51 </tal:milestones>
52 </p>
53
54 <p>
55 There are <span tal:replace="python: view.workitem_counts[date]" />
56 Blueprint work items and
57 <span tal:replace="python: view.bugtask_counts[date]" /> Bugs due
58 in <span tal:content="date/fmt:date" /> which are assigned to members
59 of this team.
60 </p>
61
62 <table class="listing">
63 <thead>
64 <tr>
65 <th>Blueprint</th>
66 <th>Target</th>
67 <th>Assignee</th>
68 <th>Priority</th>
69 <th>Progress</th>
70 </tr>
71 </thead>
72 <tal:containers repeat="container containers">
73 <tbody>
74 <tr class="expandable">
75 <td>
76 <a href="#" class="expander">&nbsp;</a>
77 <span tal:replace="structure container/html_link" />
78 </td>
79 <td tal:content="structure container/target_link" />
80 <td tal:content="structure container/assignee_link" />
81 <td tal:content="container/priority_title" />
82 <td><span tal:replace="container/progress_text" /> done</td>
83 </tr>
84 </tbody>
85 <tbody class="collapsible-body">
86 <tr tal:repeat="workitem container/items" class="padded">
87 <td>
88 <span tal:condition="not: container/spec|nothing"
89 tal:content="structure workitem/actual_workitem/fmt:link" />
90 <span tal:condition="container/spec|nothing"
91 tal:content="workitem/title/fmt:shorten/120" />
92 </td>
93 <td>
94 <span tal:condition="not: container/spec|nothing"
95 tal:replace="structure workitem/target/fmt:link" />
96 </td>
97 <td><a tal:replace="structure workitem/assignee/fmt:link" /></td>
98 <td>
99 <span tal:condition="not: container/spec|nothing"
100 tal:replace="workitem/priority/title" />
101 </td>
102 <td><span tal:replace="workitem/status/title" /></td>
103 </tr>
104 </tbody>
105 </tal:containers>
106 </table>
107 </div>
108 </div>
109</div>
110
111</body>
112</html>
0113
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2012-04-04 22:30:45 +0000
+++ lib/lp/services/features/flags.py 2012-04-05 18:43:23 +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-05 13:10:38 +0000
+++ lib/lp/testing/factory.py 2012-04-05 18:43:23 +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(