blob: afb6468aacbf83aec9d8cdeda4d947cfdca3cd7c [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Classes that generate value cells in the issue list table."""
7
8from __future__ import division
9from __future__ import print_function
10from __future__ import absolute_import
11
12import logging
13import time
14import ezt
15
16from framework import framework_constants
17from framework import table_view_helpers
18from framework import template_helpers
19from framework import urls
20from tracker import tracker_bizobj
21from tracker import tracker_helpers
22
23# pylint: disable=unused-argument
24
25
26class TableCellNote(table_view_helpers.TableCell):
27 """TableCell subclass specifically for showing a hotlist issue's note."""
28
29 def __init__(self, issue, note=None, **_kw):
30 if note:
31 display_note = [note]
32 else:
33 display_note = []
34 table_view_helpers.TableCell.__init__(
35 self, table_view_helpers.CELL_TYPE_NOTE, display_note)
36
37
38class TableCellDateAdded(table_view_helpers.TableCell):
39 """TableCell subclass specifically for showing the date added of an issue."""
40
41 def __init__(self, issue, date_added=None, **_kw):
42 table_view_helpers.TableCell.__init__(
43 self, table_view_helpers.CELL_TYPE_ATTR, [date_added])
44
45
46class TableCellAdderID(table_view_helpers.TableCell):
47 """TableCell subclass specifically for showing an issue's adder_id."""
48
49 def __init__(self, issue, adder_id=None, users_by_id=None, **_kw):
50 if adder_id:
51 display_name = [users_by_id[adder_id].display_name]
52 else:
53 display_name = [None]
54 table_view_helpers.TableCell.__init__(
55 self, table_view_helpers.CELL_TYPE_ATTR,
56 display_name)
57
58
59class TableCellRank(table_view_helpers.TableCell):
60 """TableCell subclass specifically for showing issue rank."""
61
62 def __init__(self, issue, issue_rank=None, **_kw):
63 table_view_helpers.TableCell.__init__(
64 self, table_view_helpers.CELL_TYPE_ATTR, [issue_rank])
65
66
67class TableCellID(table_view_helpers.TableCell):
68 """TableCell subclass specifically for showing issue IDs."""
69
70 def __init__(self, issue, **_kw):
71 table_view_helpers.TableCell.__init__(
72 self, table_view_helpers.CELL_TYPE_ID, [str(issue.local_id)])
73
74
75class TableCellStatus(table_view_helpers.TableCell):
76 """TableCell subclass specifically for showing issue status values."""
77
78 def __init__(self, issue, **_kws):
79 values = []
80 derived_values = []
81 if issue.status:
82 values = [issue.status]
83 if issue.derived_status:
84 derived_values = [issue.derived_status]
85
86 table_view_helpers.TableCell.__init__(
87 self, table_view_helpers.CELL_TYPE_ATTR, values,
88 derived_values=derived_values)
89
90
91class TableCellOwner(table_view_helpers.TableCell):
92 """TableCell subclass specifically for showing issue owner name."""
93
94 def __init__(self, issue, users_by_id=None, **_kw):
95 values = []
96 derived_values = []
97 if issue.owner_id:
98 values = [users_by_id[issue.owner_id].display_name]
99 if issue.derived_owner_id:
100 derived_values = [users_by_id[issue.derived_owner_id].display_name]
101
102 table_view_helpers.TableCell.__init__(
103 self, table_view_helpers.CELL_TYPE_ATTR, values,
104 derived_values=derived_values)
105
106
107class TableCellReporter(table_view_helpers.TableCell):
108 """TableCell subclass specifically for showing issue reporter name."""
109
110 def __init__(self, issue, users_by_id=None, **_kw):
111 try:
112 values = [users_by_id[issue.reporter_id].display_name]
113 except KeyError:
114 logging.info('issue reporter %r not found', issue.reporter_id)
115 values = ['deleted?']
116
117 table_view_helpers.TableCell.__init__(
118 self, table_view_helpers.CELL_TYPE_ATTR, values)
119
120
121class TableCellCc(table_view_helpers.TableCell):
122 """TableCell subclass specifically for showing issue Cc user names."""
123
124 def __init__(self, issue, users_by_id=None, **_kw):
125 values = [users_by_id[cc_id].display_name
126 for cc_id in issue.cc_ids]
127
128 derived_values = [users_by_id[cc_id].display_name
129 for cc_id in issue.derived_cc_ids]
130
131 table_view_helpers.TableCell.__init__(
132 self, table_view_helpers.CELL_TYPE_ATTR, values,
133 derived_values=derived_values)
134
135
136class TableCellAttachments(table_view_helpers.TableCell):
137 """TableCell subclass specifically for showing issue attachment count."""
138
139 def __init__(self, issue, **_kw):
140 table_view_helpers.TableCell.__init__(
141 self, table_view_helpers.CELL_TYPE_ATTR, [issue.attachment_count],
142 align='right')
143
144
145class TableCellOpened(table_view_helpers.TableCellDate):
146 """TableCell subclass specifically for showing issue opened date."""
147
148 def __init__(self, issue, **_kw):
149 table_view_helpers.TableCellDate.__init__(self, issue.opened_timestamp)
150
151
152class TableCellClosed(table_view_helpers.TableCellDate):
153 """TableCell subclass specifically for showing issue closed date."""
154
155 def __init__(self, issue, **_kw):
156 table_view_helpers.TableCellDate.__init__(self, issue.closed_timestamp)
157
158
159class TableCellModified(table_view_helpers.TableCellDate):
160 """TableCell subclass specifically for showing issue modified date."""
161
162 def __init__(self, issue, **_kw):
163 table_view_helpers.TableCellDate.__init__(self, issue.modified_timestamp)
164
165
166class TableCellOwnerModified(table_view_helpers.TableCellDate):
167 """TableCell subclass specifically for showing owner modified age."""
168
169 def __init__(self, issue, **_kw):
170 table_view_helpers.TableCellDate.__init__(
171 self, issue.owner_modified_timestamp, days_only=True)
172
173
174class TableCellStatusModified(table_view_helpers.TableCellDate):
175 """TableCell subclass specifically for showing status modified age."""
176
177 def __init__(self, issue, **_kw):
178 table_view_helpers.TableCellDate.__init__(
179 self, issue.status_modified_timestamp, days_only=True)
180
181
182class TableCellComponentModified(table_view_helpers.TableCellDate):
183 """TableCell subclass specifically for showing component modified age."""
184
185 def __init__(self, issue, **_kw):
186 table_view_helpers.TableCellDate.__init__(
187 self, issue.component_modified_timestamp, days_only=True)
188
189
190class TableCellOwnerLastVisit(table_view_helpers.TableCellDate):
191 """TableCell subclass specifically for showing owner last visit days ago."""
192
193 def __init__(self, issue, users_by_id=None, **_kw):
194 owner_view = users_by_id.get(issue.owner_id or issue.derived_owner_id)
195 last_visit = None
196 if owner_view:
197 last_visit = owner_view.user.last_visit_timestamp
198 table_view_helpers.TableCellDate.__init__(
199 self, last_visit, days_only=True)
200
201def _make_issue_view(default_pn, config, viewable_iids_set, ref_issue):
202 viewable = ref_issue.issue_id in viewable_iids_set
203 return template_helpers.EZTItem(
204 id=tracker_bizobj.FormatIssueRef(
205 (ref_issue.project_name, ref_issue.local_id),
206 default_project_name=default_pn),
207 href=tracker_helpers.FormatRelativeIssueURL(
208 ref_issue.project_name, urls.ISSUE_DETAIL, id=ref_issue.local_id),
209 closed=ezt.boolean(
210 viewable and
211 not tracker_helpers.MeansOpenInProject(ref_issue.status, config)),
212 title=ref_issue.summary if viewable else "")
213
214
215class TableCellBlockedOn(table_view_helpers.TableCell):
216 """TableCell subclass for listing issues the current issue is blocked on."""
217
218 def __init__(self, issue, related_issues=None, **_kw):
219 ref_issues = [related_issues[iid] for iid in issue.blocked_on_iids
220 if iid in related_issues]
221 values = [_make_issue_view(issue.project_name, _kw["config"],
222 _kw["viewable_iids_set"], ref_issue)
223 for ref_issue in ref_issues]
224 values.sort(key=lambda x: (x.closed, x.id))
225 table_view_helpers.TableCell.__init__(
226 self, table_view_helpers.CELL_TYPE_ISSUES, values, sort_values=False)
227
228
229class TableCellBlocking(table_view_helpers.TableCell):
230 """TableCell subclass for listing issues the current issue is blocking."""
231
232 def __init__(self, issue, related_issues=None, **_kw):
233 ref_issues = [related_issues[iid] for iid in issue.blocking_iids
234 if iid in related_issues]
235 values = [_make_issue_view(issue.project_name, _kw["config"],
236 _kw["viewable_iids_set"], ref_issue)
237 for ref_issue in ref_issues]
238 values.sort(key=lambda x: (x.closed, x.id))
239 table_view_helpers.TableCell.__init__(
240 self, table_view_helpers.CELL_TYPE_ISSUES, values, sort_values=False)
241
242
243class TableCellBlocked(table_view_helpers.TableCell):
244 """TableCell subclass for showing whether an issue is blocked."""
245
246 def __init__(self, issue, **_kw):
247 if issue.blocked_on_iids:
248 value = 'Yes'
249 else:
250 value = 'No'
251
252 table_view_helpers.TableCell.__init__(
253 self, table_view_helpers.CELL_TYPE_ATTR, [value])
254
255
256class TableCellMergedInto(table_view_helpers.TableCell):
257 """TableCell subclass for showing whether an issue is blocked."""
258
259 def __init__(self, issue, related_issues=None, **_kw):
260 if issue.merged_into:
261 ref_issue = related_issues[issue.merged_into]
262 values = [_make_issue_view(issue.project_name, _kw["config"],
263 _kw["viewable_iids_set"], ref_issue)]
264 else: # Note: None means not merged into any issue.
265 values = []
266 table_view_helpers.TableCell.__init__(
267 self, table_view_helpers.CELL_TYPE_ISSUES, values)
268
269
270class TableCellComponent(table_view_helpers.TableCell):
271 """TableCell subclass for showing components."""
272
273 def __init__(self, issue, config=None, **_kw):
274 explicit_paths = []
275 for component_id in issue.component_ids:
276 cd = tracker_bizobj.FindComponentDefByID(component_id, config)
277 if cd:
278 explicit_paths.append(cd.path)
279
280 derived_paths = []
281 for component_id in issue.derived_component_ids:
282 cd = tracker_bizobj.FindComponentDefByID(component_id, config)
283 if cd:
284 derived_paths.append(cd.path)
285
286 table_view_helpers.TableCell.__init__(
287 self, table_view_helpers.CELL_TYPE_ATTR, explicit_paths,
288 derived_values=derived_paths)
289
290
291class TableCellAllLabels(table_view_helpers.TableCell):
292 """TableCell subclass specifically for showing all labels on an issue."""
293
294 def __init__(self, issue, **_kw):
295 values = []
296 derived_values = []
297 if issue.labels:
298 values = issue.labels[:]
299 if issue.derived_labels:
300 derived_values = issue.derived_labels[:]
301
302 table_view_helpers.TableCell.__init__(
303 self, table_view_helpers.CELL_TYPE_ATTR, values,
304 derived_values=derived_values)
305
306
307# This maps column names to factories/constructors that make table cells.
308# Subclasses can override this mapping, so any additions to this mapping
309# should also be added to subclasses.
310CELL_FACTORIES = {
311 'id': TableCellID,
312 'project': table_view_helpers.TableCellProject,
313 'component': TableCellComponent,
314 'summary': table_view_helpers.TableCellSummary,
315 'status': TableCellStatus,
316 'owner': TableCellOwner,
317 'reporter': TableCellReporter,
318 'cc': TableCellCc,
319 'stars': table_view_helpers.TableCellStars,
320 'attachments': TableCellAttachments,
321 'opened': TableCellOpened,
322 'closed': TableCellClosed,
323 'modified': TableCellModified,
324 'blockedon': TableCellBlockedOn,
325 'blocking': TableCellBlocking,
326 'blocked': TableCellBlocked,
327 'mergedinto': TableCellMergedInto,
328 'ownermodified': TableCellOwnerModified,
329 'statusmodified': TableCellStatusModified,
330 'componentmodified': TableCellComponentModified,
331 'ownerlastvisit': TableCellOwnerLastVisit,
332 'rank': TableCellRank,
333 'added': TableCellDateAdded,
334 'adder': TableCellAdderID,
335 'note': TableCellNote,
336 'alllabels': TableCellAllLabels,
337 }
338
339
340# Time format that spreadsheets seem to understand.
341# E.g.: "May 19 2008 13:30:23". Tested with MS Excel 2003,
342# OpenOffice.org, NeoOffice, and Google Spreadsheets.
343CSV_DATE_TIME_FMT = '%b %d, %Y %H:%M:%S'
344
345
346def TimeStringForCSV(timestamp):
347 """Return a timestamp in a format that spreadsheets understand."""
348 return time.strftime(CSV_DATE_TIME_FMT, time.gmtime(timestamp))
349
350
351class TableCellOpenedCSV(table_view_helpers.TableCell):
352 """TableCell subclass specifically for showing issue opened date."""
353
354 def __init__(self, issue, **_kw):
355 date_str = TimeStringForCSV(issue.opened_timestamp)
356
357 table_view_helpers.TableCell.__init__(
358 self, table_view_helpers.CELL_TYPE_UNFILTERABLE, [date_str])
359
360
361class TableCellOpenedTimestamp(table_view_helpers.TableCell):
362 """TableCell subclass specifically for showing issue opened timestamp."""
363
364 def __init__(self, issue, **_kw):
365 table_view_helpers.TableCell.__init__(
366 self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
367 [issue.opened_timestamp])
368
369
370class TableCellModifiedCSV(table_view_helpers.TableCell):
371 """TableCell subclass specifically for showing issue modified date."""
372
373 def __init__(self, issue, **_kw):
374 values = []
375 if issue.modified_timestamp:
376 values = [TimeStringForCSV(issue.modified_timestamp)]
377
378 table_view_helpers.TableCell.__init__(
379 self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
380
381
382class TableCellModifiedTimestamp(table_view_helpers.TableCell):
383 """TableCell subclass specifically for showing issue modified timestamp."""
384
385 def __init__(self, issue, **_kw):
386 table_view_helpers.TableCell.__init__(
387 self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
388 [issue.modified_timestamp])
389
390
391class TableCellClosedCSV(table_view_helpers.TableCell):
392 """TableCell subclass specifically for showing issue closed date."""
393
394 def __init__(self, issue, **_kw):
395 values = []
396 if issue.closed_timestamp:
397 values = [TimeStringForCSV(issue.closed_timestamp)]
398
399 table_view_helpers.TableCell.__init__(
400 self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
401
402
403class TableCellClosedTimestamp(table_view_helpers.TableCell):
404 """TableCell subclass specifically for showing issue closed timestamp."""
405
406 def __init__(self, issue, **_kw):
407 table_view_helpers.TableCell.__init__(
408 self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
409 [issue.closed_timestamp])
410
411
412class TableCellOwnerModifiedCSV(table_view_helpers.TableCell):
413 """TableCell subclass specifically for showing owner modified date."""
414
415 def __init__(self, issue, **_kw):
416 values = []
417 if issue.modified_timestamp:
418 values = [TimeStringForCSV(issue.owner_modified_timestamp)]
419
420 table_view_helpers.TableCell.__init__(
421 self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
422
423
424class TableCellOwnerModifiedTimestamp(table_view_helpers.TableCell):
425 """TableCell subclass specifically for showing owner modified timestamp."""
426
427 def __init__(self, issue, **_kw):
428 table_view_helpers.TableCell.__init__(
429 self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
430 [issue.owner_modified_timestamp])
431
432
433class TableCellStatusModifiedCSV(table_view_helpers.TableCell):
434 """TableCell subclass specifically for showing status modified date."""
435
436 def __init__(self, issue, **_kw):
437 values = []
438 if issue.modified_timestamp:
439 values = [TimeStringForCSV(issue.status_modified_timestamp)]
440
441 table_view_helpers.TableCell.__init__(
442 self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
443
444
445class TableCellStatusModifiedTimestamp(table_view_helpers.TableCell):
446 """TableCell subclass specifically for showing status modified timestamp."""
447
448 def __init__(self, issue, **_kw):
449 table_view_helpers.TableCell.__init__(
450 self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
451 [issue.status_modified_timestamp])
452
453
454class TableCellComponentModifiedCSV(table_view_helpers.TableCell):
455 """TableCell subclass specifically for showing component modified date."""
456
457 def __init__(self, issue, **_kw):
458 values = []
459 if issue.modified_timestamp:
460 values = [TimeStringForCSV(issue.component_modified_timestamp)]
461
462 table_view_helpers.TableCell.__init__(
463 self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
464
465
466class TableCellComponentModifiedTimestamp(table_view_helpers.TableCell):
467 """TableCell subclass for showing component modified timestamp."""
468
469 def __init__(self, issue, **_kw):
470 table_view_helpers.TableCell.__init__(
471 self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
472 [issue.component_modified_timestamp])
473
474
475class TableCellOwnerLastVisitDaysAgo(table_view_helpers.TableCell):
476 """TableCell subclass specifically for showing owner last visit days ago."""
477
478 def __init__(self, issue, users_by_id=None, **_kw):
479 owner_view = users_by_id.get(issue.owner_id or issue.derived_owner_id)
480 last_visit_days_ago = None
481 if owner_view and owner_view.user.last_visit_timestamp:
482 secs_ago = int(time.time()) - owner_view.user.last_visit_timestamp
483 last_visit_days_ago = secs_ago // framework_constants.SECS_PER_DAY
484 table_view_helpers.TableCell.__init__(
485 self, table_view_helpers.CELL_TYPE_UNFILTERABLE, [last_visit_days_ago])
486
487
488# Maps column names to factories/constructors that make table cells.
489# Uses the defaults in issuelist.py but changes the factory for the
490# summary cell to properly escape the data for CSV files.
491CSV_CELL_FACTORIES = CELL_FACTORIES.copy()
492CSV_CELL_FACTORIES.update({
493 'opened': TableCellOpenedCSV,
494 'openedtimestamp': TableCellOpenedTimestamp,
495 'closed': TableCellClosedCSV,
496 'closedtimestamp': TableCellClosedTimestamp,
497 'modified': TableCellModifiedCSV,
498 'modifiedtimestamp': TableCellModifiedTimestamp,
499 'ownermodified': TableCellOwnerModifiedCSV,
500 'ownermodifiedtimestamp': TableCellOwnerModifiedTimestamp,
501 'statusmodified': TableCellStatusModifiedCSV,
502 'statusmodifiedtimestamp': TableCellStatusModifiedTimestamp,
503 'componentmodified': TableCellComponentModifiedCSV,
504 'componentmodifiedtimestamp': TableCellComponentModifiedTimestamp,
505 'ownerlastvisitdaysago': TableCellOwnerLastVisitDaysAgo,
506 })