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