blob: 3fa07c2ba9002f3f1e90ea9491a746152e37fd0a [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 and functions for displaying lists of project artifacts.
7
8This file exports classes TableRow and TableCell that help
9represent HTML table rows and cells. These classes make rendering
10HTML tables that list project artifacts much easier to do with EZT.
11"""
12from __future__ import print_function
13from __future__ import division
14from __future__ import absolute_import
15
16import collections
17import itertools
18import logging
19
20from functools import total_ordering
21
22import ezt
23
24from framework import framework_constants
25from framework import template_helpers
26from framework import timestr
27from proto import tracker_pb2
28from tracker import tracker_bizobj
29from tracker import tracker_constants
30
31
32def ComputeUnshownColumns(results, shown_columns, config, built_in_cols):
33 """Return a list of unshown columns that the user could add.
34
35 Args:
36 results: list of search result PBs. Each must have labels.
37 shown_columns: list of column names to be used in results table.
38 config: harmonized config for the issue search, including all
39 well known labels and custom fields.
40 built_in_cols: list of other column names that are built into the tool.
41 E.g., star count, or creation date.
42
43 Returns:
44 List of column names to append to the "..." menu.
45 """
46 unshown_set = set() # lowercases column names
47 unshown_list = [] # original-case column names
48 shown_set = {col.lower() for col in shown_columns}
49 labels_already_seen = set() # whole labels, original case
50
51 def _MaybeAddLabel(label_name):
52 """Add the key part of the given label if needed."""
53 if label_name.lower() in labels_already_seen:
54 return
55 labels_already_seen.add(label_name.lower())
56 if '-' in label_name:
57 col, _value = label_name.split('-', 1)
58 _MaybeAddCol(col)
59
60 def _MaybeAddCol(col):
61 if col.lower() not in shown_set and col.lower() not in unshown_set:
62 unshown_list.append(col)
63 unshown_set.add(col.lower())
64
65 # The user can always add any of the default columns.
66 for col in config.default_col_spec.split():
67 _MaybeAddCol(col)
68
69 # The user can always add any of the built-in columns.
70 for col in built_in_cols:
71 _MaybeAddCol(col)
72
73 # The user can add a column for any well-known labels
74 for wkl in config.well_known_labels:
75 _MaybeAddLabel(wkl.label)
76
77 phase_names = set(itertools.chain.from_iterable(
78 (phase.name.lower() for phase in result.phases) for result in results))
79 # The user can add a column for any custom field
80 field_ids_alread_seen = set()
81 for fd in config.field_defs:
82 field_lower = fd.field_name.lower()
83 field_ids_alread_seen.add(fd.field_id)
84 if fd.is_phase_field:
85 for name in phase_names:
86 phase_field_col = name + '.' + field_lower
87 if (phase_field_col not in shown_set and
88 phase_field_col not in unshown_set):
89 unshown_list.append(phase_field_col)
90 unshown_set.add(phase_field_col)
91 elif field_lower not in shown_set and field_lower not in unshown_set:
92 unshown_list.append(fd.field_name)
93 unshown_set.add(field_lower)
94
95 if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
96 approval_lower_approver = (
97 field_lower + tracker_constants.APPROVER_COL_SUFFIX)
98 if (approval_lower_approver not in shown_set and
99 approval_lower_approver not in unshown_set):
100 unshown_list.append(
101 fd.field_name + tracker_constants.APPROVER_COL_SUFFIX)
102 unshown_set.add(approval_lower_approver)
103
104 # The user can add a column for any key-value label or field in the results.
105 for r in results:
106 for label_name in tracker_bizobj.GetLabels(r):
107 _MaybeAddLabel(label_name)
108 for field_value in r.field_values:
109 if field_value.field_id not in field_ids_alread_seen:
110 field_ids_alread_seen.add(field_value.field_id)
111 fd = tracker_bizobj.FindFieldDefByID(field_value.field_id, config)
112 if fd: # could be None for a foreign field, which we don't display.
113 field_lower = fd.field_name.lower()
114 if field_lower not in shown_set and field_lower not in unshown_set:
115 unshown_list.append(fd.field_name)
116 unshown_set.add(field_lower)
117
118 return sorted(unshown_list)
119
120
121def ExtractUniqueValues(columns, artifact_list, users_by_id,
122 config, related_issues, hotlist_context_dict=None):
123 """Build a nested list of unique values so the user can auto-filter.
124
125 Args:
126 columns: a list of lowercase column name strings, which may contain
127 combined columns like "priority/pri".
128 artifact_list: a list of artifacts in the complete set of search results.
129 users_by_id: dict mapping user_ids to UserViews.
130 config: ProjectIssueConfig PB for the current project.
131 related_issues: dict {issue_id: issue} of pre-fetched related issues.
132 hotlist_context_dict: dict for building a hotlist grid table
133
134 Returns:
135 [EZTItem(col1, colname1, [val11, val12,...]), ...]
136 A list of EZTItems, each of which has a col_index, column_name,
137 and a list of unique values that appear in that column.
138 """
139 column_values = {col_name: {} for col_name in columns}
140
141 # For each combined column "a/b/c", add entries that point from "a" back
142 # to "a/b/c", from "b" back to "a/b/c", and from "c" back to "a/b/c".
143 combined_column_parts = collections.defaultdict(list)
144 for col in columns:
145 if '/' in col:
146 for col_part in col.split('/'):
147 combined_column_parts[col_part].append(col)
148
149 unique_labels = set()
150 for art in artifact_list:
151 unique_labels.update(tracker_bizobj.GetLabels(art))
152
153 for label in unique_labels:
154 if '-' in label:
155 col, val = label.split('-', 1)
156 col = col.lower()
157 if col in column_values:
158 column_values[col][val.lower()] = val
159 if col in combined_column_parts:
160 for combined_column in combined_column_parts[col]:
161 column_values[combined_column][val.lower()] = val
162 else:
163 if 'summary' in column_values:
164 column_values['summary'][label.lower()] = label
165
166 # TODO(jrobbins): Consider refacting some of this to tracker_bizobj
167 # or a new builtins.py to reduce duplication.
168 if 'reporter' in column_values:
169 for art in artifact_list:
170 reporter_id = art.reporter_id
171 if reporter_id and reporter_id in users_by_id:
172 reporter_username = users_by_id[reporter_id].display_name
173 column_values['reporter'][reporter_username] = reporter_username
174
175 if 'owner' in column_values:
176 for art in artifact_list:
177 owner_id = tracker_bizobj.GetOwnerId(art)
178 if owner_id and owner_id in users_by_id:
179 owner_username = users_by_id[owner_id].display_name
180 column_values['owner'][owner_username] = owner_username
181
182 if 'cc' in column_values:
183 for art in artifact_list:
184 cc_ids = tracker_bizobj.GetCcIds(art)
185 for cc_id in cc_ids:
186 if cc_id and cc_id in users_by_id:
187 cc_username = users_by_id[cc_id].display_name
188 column_values['cc'][cc_username] = cc_username
189
190 if 'component' in column_values:
191 for art in artifact_list:
192 all_comp_ids = list(art.component_ids) + list(art.derived_component_ids)
193 for component_id in all_comp_ids:
194 cd = tracker_bizobj.FindComponentDefByID(component_id, config)
195 if cd:
196 column_values['component'][cd.path] = cd.path
197
198 if 'stars' in column_values:
199 for art in artifact_list:
200 star_count = art.star_count
201 column_values['stars'][star_count] = star_count
202
203 if 'status' in column_values:
204 for art in artifact_list:
205 status = tracker_bizobj.GetStatus(art)
206 if status:
207 column_values['status'][status.lower()] = status
208
209 if 'project' in column_values:
210 for art in artifact_list:
211 project_name = art.project_name
212 column_values['project'][project_name] = project_name
213
214 if 'mergedinto' in column_values:
215 for art in artifact_list:
216 if art.merged_into and art.merged_into != 0:
217 merged_issue = related_issues[art.merged_into]
218 merged_issue_ref = tracker_bizobj.FormatIssueRef((
219 merged_issue.project_name, merged_issue.local_id))
220 column_values['mergedinto'][merged_issue_ref] = merged_issue_ref
221
222 if 'blocked' in column_values:
223 for art in artifact_list:
224 if art.blocked_on_iids:
225 column_values['blocked']['is_blocked'] = 'Yes'
226 else:
227 column_values['blocked']['is_not_blocked'] = 'No'
228
229 if 'blockedon' in column_values:
230 for art in artifact_list:
231 if art.blocked_on_iids:
232 for blocked_on_iid in art.blocked_on_iids:
233 blocked_on_issue = related_issues[blocked_on_iid]
234 blocked_on_ref = tracker_bizobj.FormatIssueRef((
235 blocked_on_issue.project_name, blocked_on_issue.local_id))
236 column_values['blockedon'][blocked_on_ref] = blocked_on_ref
237
238 if 'blocking' in column_values:
239 for art in artifact_list:
240 if art.blocking_iids:
241 for blocking_iid in art.blocking_iids:
242 blocking_issue = related_issues[blocking_iid]
243 blocking_ref = tracker_bizobj.FormatIssueRef((
244 blocking_issue.project_name, blocking_issue.local_id))
245 column_values['blocking'][blocking_ref] = blocking_ref
246
247 if 'added' in column_values:
248 for art in artifact_list:
249 if hotlist_context_dict and hotlist_context_dict[art.issue_id]:
250 issue_dict = hotlist_context_dict[art.issue_id]
251 date_added = issue_dict['date_added']
252 column_values['added'][date_added] = date_added
253
254 if 'adder' in column_values:
255 for art in artifact_list:
256 if hotlist_context_dict and hotlist_context_dict[art.issue_id]:
257 issue_dict = hotlist_context_dict[art.issue_id]
258 adder_id = issue_dict['adder_id']
259 adder = users_by_id[adder_id].display_name
260 column_values['adder'][adder] = adder
261
262 if 'note' in column_values:
263 for art in artifact_list:
264 if hotlist_context_dict and hotlist_context_dict[art.issue_id]:
265 issue_dict = hotlist_context_dict[art.issue_id]
266 note = issue_dict['note']
267 if issue_dict['note']:
268 column_values['note'][note] = note
269
270 if 'attachments' in column_values:
271 for art in artifact_list:
272 attachment_count = art.attachment_count
273 column_values['attachments'][attachment_count] = attachment_count
274
275 # Add all custom field values if the custom field name is a shown column.
276 field_id_to_col = {}
277 for art in artifact_list:
278 for fv in art.field_values:
279 field_col, field_type = field_id_to_col.get(fv.field_id, (None, None))
280 if field_col == 'NOT_SHOWN':
281 continue
282 if field_col is None:
283 fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config)
284 if not fd:
285 field_id_to_col[fv.field_id] = 'NOT_SHOWN', None
286 continue
287 field_col = fd.field_name.lower()
288 field_type = fd.field_type
289 if field_col not in column_values:
290 field_id_to_col[fv.field_id] = 'NOT_SHOWN', None
291 continue
292 field_id_to_col[fv.field_id] = field_col, field_type
293
294 if field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
295 continue # Already handled by label parsing
296 elif field_type == tracker_pb2.FieldTypes.INT_TYPE:
297 val = fv.int_value
298 elif field_type == tracker_pb2.FieldTypes.STR_TYPE:
299 val = fv.str_value
300 elif field_type == tracker_pb2.FieldTypes.USER_TYPE:
301 user = users_by_id.get(fv.user_id)
302 val = user.email if user else framework_constants.NO_USER_NAME
303 elif field_type == tracker_pb2.FieldTypes.DATE_TYPE:
304 val = fv.int_value # TODO(jrobbins): convert to date
305 elif field_type == tracker_pb2.FieldTypes.BOOL_TYPE:
306 val = 'Yes' if fv.int_value else 'No'
307
308 column_values[field_col][val] = val
309
310 # TODO(jrobbins): make the capitalization of well-known unique label and
311 # status values match the way it is written in the issue config.
312
313 # Return EZTItems for each column in left-to-right display order.
314 result = []
315 for i, col_name in enumerate(columns):
316 # TODO(jrobbins): sort each set of column values top-to-bottom, by the
317 # order specified in the project artifact config. For now, just sort
318 # lexicographically to make expected output defined.
319 sorted_col_values = sorted(column_values[col_name].values())
320 result.append(template_helpers.EZTItem(
321 col_index=i, column_name=col_name, filter_values=sorted_col_values))
322
323 return result
324
325
326def MakeTableData(
327 visible_results, starred_items, lower_columns, lower_group_by,
328 users_by_id, cell_factories, id_accessor, related_issues,
329 viewable_iids_set, config, context_for_all_issues=None):
330 """Return a list of list row objects for display by EZT.
331
332 Args:
333 visible_results: list of artifacts to display on one pagination page.
334 starred_items: list of IDs/names of items in the current project
335 that the signed in user has starred.
336 lower_columns: list of column names to display, all lowercase. These can
337 be combined column names, e.g., 'priority/pri'.
338 lower_group_by: list of column names that define row groups, all lowercase.
339 users_by_id: dict mapping user IDs to UserViews.
340 cell_factories: dict of functions that each create TableCell objects.
341 id_accessor: function that maps from an artifact to the ID/name that might
342 be in the starred items list.
343 related_issues: dict {issue_id: issue} of pre-fetched related issues.
344 viewable_iids_set: set of issue ids that can be viewed by the user.
345 config: ProjectIssueConfig PB for the current project.
346 context_for_all_issues: A dictionary of dictionaries containing values
347 passed in to cell factory functions to create TableCells. Dictionary
348 form: {issue_id: {'rank': issue_rank, 'issue_info': info_value, ..},
349 issue_id: {'rank': issue_rank}, ..}
350
351 Returns:
352 A list of TableRow objects, one for each visible result.
353 """
354 table_data = []
355
356 group_cell_factories = [
357 ChooseCellFactory(group.strip('-'), cell_factories, config)
358 for group in lower_group_by]
359
360 # Make a list of cell factories, one for each column.
361 factories_to_use = [
362 ChooseCellFactory(col, cell_factories, config) for col in lower_columns]
363
364 current_group = None
365 for idx, art in enumerate(visible_results):
366 row = MakeRowData(
367 art, lower_columns, users_by_id, factories_to_use, related_issues,
368 viewable_iids_set, config, context_for_all_issues)
369 row.starred = ezt.boolean(id_accessor(art) in starred_items)
370 row.idx = idx # EZT does not have loop counters, so add idx.
371 table_data.append(row)
372 row.group = None
373
374 # Also include group information for the first row in each group.
375 # TODO(jrobbins): This seems like more overhead than we need for the
376 # common case where no new group heading row is to be inserted.
377 group = MakeRowData(
378 art, [group_name.strip('-') for group_name in lower_group_by],
379 users_by_id, group_cell_factories, related_issues, viewable_iids_set,
380 config, context_for_all_issues)
381 for cell, group_name in zip(group.cells, lower_group_by):
382 cell.group_name = group_name
383 if group == current_group:
384 current_group.rows_in_group += 1
385 else:
386 row.group = group
387 current_group = group
388 current_group.rows_in_group = 1
389
390 return table_data
391
392
393def MakeRowData(
394 art, columns, users_by_id, cell_factory_list, related_issues,
395 viewable_iids_set, config, context_for_all_issues):
396 """Make a TableRow for use by EZT when rendering HTML table of results.
397
398 Args:
399 art: a project artifact PB
400 columns: list of lower-case column names
401 users_by_id: dictionary {user_id: UserView} with each UserView having
402 a "display_name" member.
403 cell_factory_list: list of functions that each create TableCell
404 objects for a given column.
405 related_issues: dict {issue_id: issue} of pre-fetched related issues.
406 viewable_iids_set: set of issue ids that can be viewed by the user.
407 config: ProjectIssueConfig PB for the current project.
408 context_for_all_issues: A dictionary of dictionaries containing values
409 passed in to cell factory functions to create TableCells. Dictionary
410 form: {issue_id: {'rank': issue_rank, 'issue_info': info_value, ..},
411 issue_id: {'rank': issue_rank}, ..}
412
413 Returns:
414 A TableRow object for use by EZT to render a table of results.
415 """
416 if context_for_all_issues is None:
417 context_for_all_issues = {}
418 ordered_row_data = []
419 non_col_labels = []
420 label_values = collections.defaultdict(list)
421
422 flattened_columns = set()
423 for col in columns:
424 if '/' in col:
425 flattened_columns.update(col.split('/'))
426 else:
427 flattened_columns.add(col)
428
429 # Group all "Key-Value" labels by key, and separate the "OneWord" labels.
430 _AccumulateLabelValues(
431 art.labels, flattened_columns, label_values, non_col_labels)
432
433 _AccumulateLabelValues(
434 art.derived_labels, flattened_columns, label_values,
435 non_col_labels, is_derived=True)
436
437 # Build up a list of TableCell objects for this row.
438 for i, col in enumerate(columns):
439 factory = cell_factory_list[i]
440 kw = {
441 'col': col,
442 'users_by_id': users_by_id,
443 'non_col_labels': non_col_labels,
444 'label_values': label_values,
445 'related_issues': related_issues,
446 'viewable_iids_set': viewable_iids_set,
447 'config': config,
448 }
449 kw.update(context_for_all_issues.get(art.issue_id, {}))
450 new_cell = factory(art, **kw)
451 new_cell.col_index = i
452 ordered_row_data.append(new_cell)
453
454 return TableRow(ordered_row_data)
455
456
457def _AccumulateLabelValues(
458 labels, columns, label_values, non_col_labels, is_derived=False):
459 """Parse OneWord and Key-Value labels for display in a list page.
460
461 Args:
462 labels: a list of label strings.
463 columns: a list of column names.
464 label_values: mutable dictionary {key: [value, ...]} of label values
465 seen so far.
466 non_col_labels: mutable list of OneWord labels seen so far.
467 is_derived: true if these labels were derived via rules.
468
469 Returns:
470 Nothing. But, the given label_values dictionary will grow to hold
471 the values of the key-value labels passed in, and the non_col_labels
472 list will grow to hold the OneWord labels passed in. These are shown
473 in label columns, and in the summary column, respectively
474 """
475 for label_name in labels:
476 if '-' in label_name:
477 parts = label_name.split('-')
478 for pivot in range(1, len(parts)):
479 column_name = '-'.join(parts[:pivot])
480 value = '-'.join(parts[pivot:])
481 column_name = column_name.lower()
482 if column_name in columns:
483 label_values[column_name].append((value, is_derived))
484 else:
485 non_col_labels.append((label_name, is_derived))
486
487
488@total_ordering
489class TableRow(object):
490 """A tiny auxiliary class to represent a row in an HTML table."""
491
492 def __init__(self, cells):
493 """Initialize the table row with the given data."""
494 self.cells = cells
495 # Used by MakeTableData for layout.
496 self.idx = None
497 self.group = None
498 self.rows_in_group = None
499 self.starred = None
500
501 def __eq__(self, other):
502 """A row is == if each cell is == to the cells in the other row."""
503 return other and self.cells == other.cells
504
505 def __ne__(self, other):
506 return not other and self.cells != other.cells
507
508 def __lt__(self, other):
509 return other and self.cells < other.cells
510
511 def DebugString(self):
512 """Return a string that is useful for on-page debugging."""
513 return 'TR(%s)' % self.cells
514
515
516# TODO(jrobbins): also add unsortable... or change this to a list of operations
517# that can be done.
518CELL_TYPE_ID = 'ID'
519CELL_TYPE_SUMMARY = 'summary'
520CELL_TYPE_ATTR = 'attr'
521CELL_TYPE_UNFILTERABLE = 'unfilterable'
522CELL_TYPE_NOTE = 'note'
523CELL_TYPE_PROJECT = 'project'
524CELL_TYPE_URL = 'url'
525CELL_TYPE_ISSUES = 'issues'
526
527
528@total_ordering
529class TableCell(object):
530 """Helper class to represent a table cell when rendering using EZT."""
531
532 # Should instances of this class be rendered with whitespace:nowrap?
533 # Subclasses can override this constant.
534 NOWRAP = ezt.boolean(True)
535
536 def __init__(self, cell_type, explicit_values,
537 derived_values=None, non_column_labels=None, align='',
538 sort_values=True):
539 """Store all the given data for later access by EZT."""
540 self.type = cell_type
541 self.align = align
542 self.col_index = 0 # Is set afterward
543 self.values = []
544 if non_column_labels:
545 self.non_column_labels = [
546 template_helpers.EZTItem(value=v, is_derived=ezt.boolean(d))
547 for v, d in non_column_labels]
548 else:
549 self.non_column_labels = []
550
551 for v in (sorted(explicit_values) if sort_values else explicit_values):
552 self.values.append(CellItem(v))
553
554 if derived_values:
555 for v in (sorted(derived_values) if sort_values else derived_values):
556 self.values.append(CellItem(v, is_derived=True))
557
558 def __eq__(self, other):
559 """A row is == if each cell is == to the cells in the other row."""
560 return other and self.values == other.values
561
562 def __ne__(self, other):
563 return not other and self.values != other.values
564
565 def __lt__(self, other):
566 return other and self.values < other.values
567
568 def DebugString(self):
569 return 'TC(%r, %r, %r)' % (
570 self.type,
571 [v.DebugString() for v in self.values],
572 self.non_column_labels)
573
574
575def CompositeFactoryTableCell(factory_col_list_arg):
576 """Cell factory that combines multiple cells in a combined column."""
577
578 class FactoryClass(TableCell):
579 factory_col_list = factory_col_list_arg
580
581 def __init__(self, art, **kw):
582 TableCell.__init__(self, CELL_TYPE_UNFILTERABLE, [])
583
584 for sub_factory, sub_col in self.factory_col_list:
585 kw['col'] = sub_col
586 sub_cell = sub_factory(art, **kw)
587 self.non_column_labels.extend(sub_cell.non_column_labels)
588 self.values.extend(sub_cell.values)
589 return FactoryClass
590
591
592def CompositeColTableCell(columns_to_combine, cell_factories, config):
593 """Cell factory that combines multiple cells in a combined column."""
594 factory_col_list = []
595 for sub_col in columns_to_combine:
596 sub_factory = ChooseCellFactory(sub_col, cell_factories, config)
597 factory_col_list.append((sub_factory, sub_col))
598 return CompositeFactoryTableCell(factory_col_list)
599
600
601@total_ordering
602class CellItem(object):
603 """Simple class to display one part of a table cell's value, with style."""
604
605 def __init__(self, item, is_derived=False):
606 self.item = item
607 self.is_derived = ezt.boolean(is_derived)
608
609 def __eq__(self, other):
610 """A row is == if each cell is == to the item in the other row."""
611 return other and self.item == other.item
612
613 def __ne__(self, other):
614 return not other and self.item != other.item
615
616 def __lt__(self, other):
617 return other and self.item < other.item
618
619 def DebugString(self):
620 if self.is_derived:
621 return 'CI(derived: %r)' % self.item
622 else:
623 return 'CI(%r)' % self.item
624
625
626class TableCellKeyLabels(TableCell):
627 """TableCell subclass specifically for showing user-defined label values."""
628
629 def __init__(self, _art, col=None, label_values=None, **_kw):
630 label_value_pairs = label_values.get(col, [])
631 explicit_values = [value for value, is_derived in label_value_pairs
632 if not is_derived]
633 derived_values = [value for value, is_derived in label_value_pairs
634 if is_derived]
635 TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values,
636 derived_values=derived_values)
637
638
639class TableCellProject(TableCell):
640 """TableCell subclass for showing an artifact's project name."""
641
642 def __init__(self, art, **_kw):
643 TableCell.__init__(
644 self, CELL_TYPE_PROJECT, [art.project_name])
645
646
647class TableCellStars(TableCell):
648 """TableCell subclass for showing an artifact's star count."""
649
650 def __init__(self, art, **_kw):
651 TableCell.__init__(
652 self, CELL_TYPE_ATTR, [art.star_count], align='right')
653
654
655class TableCellSummary(TableCell):
656 """TableCell subclass for showing an artifact's summary."""
657
658 def __init__(self, art, non_col_labels=None, **_kw):
659 TableCell.__init__(
660 self, CELL_TYPE_SUMMARY, [art.summary],
661 non_column_labels=non_col_labels)
662
663
664class TableCellDate(TableCell):
665 """TableCell subclass for showing any kind of date timestamp."""
666
667 # Make instances of this class render with whitespace:nowrap.
668 NOWRAP = ezt.boolean(True)
669
670 def __init__(self, timestamp, days_only=False):
671 values = []
672 if timestamp:
673 date_str = timestr.FormatRelativeDate(timestamp, days_only=days_only)
674 if not date_str:
675 date_str = timestr.FormatAbsoluteDate(timestamp)
676 values = [date_str]
677
678 TableCell.__init__(self, CELL_TYPE_UNFILTERABLE, values)
679
680
681class TableCellCustom(TableCell):
682 """Abstract TableCell subclass specifically for showing custom fields."""
683
684 def __init__(self, art, col=None, users_by_id=None, config=None, **_kw):
685 explicit_values = []
686 derived_values = []
687 cell_type = CELL_TYPE_ATTR
688 phase_names_by_id = {
689 phase.phase_id: phase.name.lower() for phase in art.phases}
690 phase_name = None
691 # Check if col represents a phase field value in the form <phase>.<field>
692 if '.' in col:
693 phase_name, col = col.split('.', 1)
694 for fv in art.field_values:
695 # TODO(jrobbins): for cross-project search this could be a list.
696 fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config)
697 if not fd:
698 # TODO(jrobbins): This can happen if an issue with a custom
699 # field value is moved to a different project.
700 logging.warn('Issue ID %r has undefined field value %r',
701 art.issue_id, fv)
702 elif fd.field_name.lower() == col and (
703 phase_names_by_id.get(fv.phase_id) == phase_name):
704 if fd.field_type == tracker_pb2.FieldTypes.URL_TYPE:
705 cell_type = CELL_TYPE_URL
706 if fd.field_type == tracker_pb2.FieldTypes.STR_TYPE:
707 self.NOWRAP = ezt.boolean(False)
708 val = tracker_bizobj.GetFieldValue(fv, users_by_id)
709 if fv.derived:
710 derived_values.append(val)
711 else:
712 explicit_values.append(val)
713
714 TableCell.__init__(self, cell_type, explicit_values,
715 derived_values=derived_values)
716
717 def ExtractValue(self, fv, _users_by_id):
718 return 'field-id-%d-not-implemented-yet' % fv.field_id
719
720class TableCellApprovalStatus(TableCell):
721 """Abstract TableCell subclass specifically for showing approval fields."""
722
723 def __init__(self, art, col=None, config=None, **_kw):
724 explicit_values = []
725 for av in art.approval_values:
726 fd = tracker_bizobj.FindFieldDef(col, config)
727 ad = tracker_bizobj.FindApprovalDef(col, config)
728 if not (ad and fd):
729 logging.warn('Issue ID %r has undefined field value %r',
730 art.issue_id, av)
731 elif av.approval_id == fd.field_id:
732 explicit_values.append(av.status.name)
733 break
734
735 TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values)
736
737
738class TableCellApprovalApprover(TableCell):
739 """TableCell subclass specifically for showing approval approvers."""
740
741 def __init__(self, art, col=None, config=None, users_by_id=None, **_kw):
742 explicit_values = []
743 approval_name = col[:-len(tracker_constants.APPROVER_COL_SUFFIX)]
744 for av in art.approval_values:
745 fd = tracker_bizobj.FindFieldDef(approval_name, config)
746 ad = tracker_bizobj.FindApprovalDef(approval_name, config)
747 if not (ad and fd):
748 logging.warn('Issue ID %r has undefined field value %r',
749 art.issue_id, av)
750 elif av.approval_id == fd.field_id:
751 explicit_values = [users_by_id.get(approver_id).display_name
752 for approver_id in av.approver_ids
753 if users_by_id.get(approver_id)]
754 break
755
756 TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values)
757
758def ChooseCellFactory(col, cell_factories, config):
759 """Return the CellFactory to use for the given column."""
760 if col in cell_factories:
761 return cell_factories[col]
762
763 if '/' in col:
764 return CompositeColTableCell(col.split('/'), cell_factories, config)
765
766 is_approver_col = False
767 possible_field_name = col
768 if col.endswith(tracker_constants.APPROVER_COL_SUFFIX):
769 possible_field_name = col[:-len(tracker_constants.APPROVER_COL_SUFFIX)]
770 is_approver_col = True
771 # Check if col represents a phase field value in the form <phase>.<field>
772 elif '.' in possible_field_name:
773 possible_field_name = possible_field_name.split('.')[-1]
774
775 fd = tracker_bizobj.FindFieldDef(possible_field_name, config)
776 if fd:
777 # We cannot assume that non-enum_type field defs do not share their
778 # names with label prefixes. So we need to group them with
779 # TableCellKeyLabels to make sure we catch appropriate labels values.
780 if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
781 if is_approver_col:
782 # Combined cell for 'FieldName-approver' to hold approvers
783 # belonging to FieldName and values belonging to labels with
784 # 'FieldName-approver' as the key.
785 return CompositeFactoryTableCell(
786 [(TableCellApprovalApprover, col), (TableCellKeyLabels, col)])
787 return CompositeFactoryTableCell(
788 [(TableCellApprovalStatus, col), (TableCellKeyLabels, col)])
789 elif fd.field_type != tracker_pb2.FieldTypes.ENUM_TYPE:
790 return CompositeFactoryTableCell(
791 [(TableCellCustom, col), (TableCellKeyLabels, col)])
792
793 return TableCellKeyLabels