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