OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered 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 |
| 8 This file exports classes TableRow and TableCell that help |
| 9 represent HTML table rows and cells. These classes make rendering |
| 10 HTML tables that list project artifacts much easier to do with EZT. |
| 11 """ |
| 12 |
| 13 import collections |
| 14 import logging |
| 15 |
| 16 from third_party import ezt |
| 17 |
| 18 from framework import framework_constants |
| 19 from framework import template_helpers |
| 20 from proto import tracker_pb2 |
| 21 from tracker import tracker_bizobj |
| 22 |
| 23 |
| 24 def ComputeUnshownColumns(results, shown_columns, config, built_in_cols): |
| 25 """Return a list of unshown columns that the user could add. |
| 26 |
| 27 Args: |
| 28 results: list of search result PBs. Each must have labels. |
| 29 shown_columns: list of column names to be used in results table. |
| 30 config: harmonized config for the issue search, including all |
| 31 well known labels and custom fields. |
| 32 built_in_cols: list of other column names that are built into the tool. |
| 33 E.g., star count, or creation date. |
| 34 |
| 35 Returns: |
| 36 List of column names to append to the "..." menu. |
| 37 """ |
| 38 unshown_set = set() # lowercases column names |
| 39 unshown_list = [] # original-case column names |
| 40 shown_set = {col.lower() for col in shown_columns} |
| 41 labels_already_seen = set() # whole labels, original case |
| 42 |
| 43 def _MaybeAddLabel(label_name): |
| 44 """Add the key part of the given label if needed.""" |
| 45 if label_name.lower() in labels_already_seen: |
| 46 return |
| 47 labels_already_seen.add(label_name.lower()) |
| 48 if '-' in label_name: |
| 49 col, _value = label_name.split('-', 1) |
| 50 _MaybeAddCol(col) |
| 51 |
| 52 def _MaybeAddCol(col): |
| 53 if col.lower() not in shown_set and col.lower() not in unshown_set: |
| 54 unshown_list.append(col) |
| 55 unshown_set.add(col.lower()) |
| 56 |
| 57 # The user can always add any of the default columns. |
| 58 for col in config.default_col_spec.split(): |
| 59 _MaybeAddCol(col) |
| 60 |
| 61 # The user can always add any of the built-in columns. |
| 62 for col in built_in_cols: |
| 63 _MaybeAddCol(col) |
| 64 |
| 65 # The user can add a column for any well-known labels |
| 66 for wkl in config.well_known_labels: |
| 67 _MaybeAddLabel(wkl.label) |
| 68 |
| 69 # The user can add a column for any custom field |
| 70 field_ids_alread_seen = set() |
| 71 for fd in config.field_defs: |
| 72 field_lower = fd.field_name.lower() |
| 73 field_ids_alread_seen.add(fd.field_id) |
| 74 if field_lower not in shown_set and field_lower not in unshown_set: |
| 75 unshown_list.append(fd.field_name) |
| 76 unshown_set.add(field_lower) |
| 77 |
| 78 # The user can add a column for any key-value label or field in the results. |
| 79 for r in results: |
| 80 for label_name in tracker_bizobj.GetLabels(r): |
| 81 _MaybeAddLabel(label_name) |
| 82 for field_value in r.field_values: |
| 83 if field_value.field_id not in field_ids_alread_seen: |
| 84 field_ids_alread_seen.add(field_value.field_id) |
| 85 fd = tracker_bizobj.FindFieldDefByID(field_value.field_id, config) |
| 86 if fd: # could be None for a foreign field, which we don't display. |
| 87 field_lower = fd.field_name.lower() |
| 88 if field_lower not in shown_set and field_lower not in unshown_set: |
| 89 unshown_list.append(fd.field_name) |
| 90 unshown_set.add(field_lower) |
| 91 |
| 92 return sorted(unshown_list) |
| 93 |
| 94 |
| 95 def ExtractUniqueValues(columns, artifact_list, users_by_id, config): |
| 96 """Build a nested list of unique values so the user can auto-filter. |
| 97 |
| 98 Args: |
| 99 columns: a list of lowercase column name strings, which may contain |
| 100 combined columns like "priority/pri". |
| 101 artifact_list: a list of artifacts in the complete set of search results. |
| 102 users_by_id: dict mapping user_ids to UserViews. |
| 103 config: ProjectIssueConfig PB for the current project. |
| 104 |
| 105 Returns: |
| 106 [EZTItem(col1, colname1, [val11, val12,...]), ...] |
| 107 A list of EZTItems, each of which has a col_index, column_name, |
| 108 and a list of unique values that appear in that column. |
| 109 """ |
| 110 column_values = {col_name: {} for col_name in columns} |
| 111 |
| 112 # For each combined column "a/b/c", add entries that point from "a" back |
| 113 # to "a/b/c", from "b" back to "a/b/c", and from "c" back to "a/b/c". |
| 114 combined_column_parts = collections.defaultdict(list) |
| 115 for col in columns: |
| 116 if '/' in col: |
| 117 for col_part in col.split('/'): |
| 118 combined_column_parts[col_part].append(col) |
| 119 |
| 120 unique_labels = set() |
| 121 for art in artifact_list: |
| 122 unique_labels.update(tracker_bizobj.GetLabels(art)) |
| 123 |
| 124 for label in unique_labels: |
| 125 if '-' in label: |
| 126 col, val = label.split('-', 1) |
| 127 col = col.lower() |
| 128 if col in column_values: |
| 129 column_values[col][val.lower()] = val |
| 130 if col in combined_column_parts: |
| 131 for combined_column in combined_column_parts[col]: |
| 132 column_values[combined_column][val.lower()] = val |
| 133 else: |
| 134 if 'summary' in column_values: |
| 135 column_values['summary'][label.lower()] = label |
| 136 |
| 137 # TODO(jrobbins): Consider refacting some of this to tracker_bizobj |
| 138 # or a new builtins.py to reduce duplication. |
| 139 if 'reporter' in column_values: |
| 140 for art in artifact_list: |
| 141 reporter_id = art.reporter_id |
| 142 if reporter_id and reporter_id in users_by_id: |
| 143 reporter_username = users_by_id[reporter_id].display_name |
| 144 column_values['reporter'][reporter_username] = reporter_username |
| 145 |
| 146 if 'owner' in column_values: |
| 147 for art in artifact_list: |
| 148 owner_id = tracker_bizobj.GetOwnerId(art) |
| 149 if owner_id and owner_id in users_by_id: |
| 150 owner_username = users_by_id[owner_id].display_name |
| 151 column_values['owner'][owner_username] = owner_username |
| 152 |
| 153 if 'cc' in column_values: |
| 154 for art in artifact_list: |
| 155 cc_ids = tracker_bizobj.GetCcIds(art) |
| 156 for cc_id in cc_ids: |
| 157 if cc_id and cc_id in users_by_id: |
| 158 cc_username = users_by_id[cc_id].display_name |
| 159 column_values['cc'][cc_username] = cc_username |
| 160 |
| 161 if 'component' in column_values: |
| 162 for art in artifact_list: |
| 163 all_comp_ids = list(art.component_ids) + list(art.derived_component_ids) |
| 164 for component_id in all_comp_ids: |
| 165 cd = tracker_bizobj.FindComponentDefByID(component_id, config) |
| 166 if cd: |
| 167 column_values['component'][cd.path] = cd.path |
| 168 |
| 169 if 'stars' in column_values: |
| 170 for art in artifact_list: |
| 171 star_count = art.star_count |
| 172 column_values['stars'][star_count] = star_count |
| 173 |
| 174 if 'status' in column_values: |
| 175 for art in artifact_list: |
| 176 status = tracker_bizobj.GetStatus(art) |
| 177 if status: |
| 178 column_values['status'][status.lower()] = status |
| 179 |
| 180 # TODO(jrobbins): merged into, blocked on, and blocking. And, the ability |
| 181 # to parse a user query on those fields and do a SQL search. |
| 182 |
| 183 if 'attachments' in column_values: |
| 184 for art in artifact_list: |
| 185 attachment_count = art.attachment_count |
| 186 column_values['attachments'][attachment_count] = attachment_count |
| 187 |
| 188 # Add all custom field values if the custom field name is a shown column. |
| 189 field_id_to_col = {} |
| 190 for art in artifact_list: |
| 191 for fv in art.field_values: |
| 192 field_col, field_type = field_id_to_col.get(fv.field_id, (None, None)) |
| 193 if field_col == 'NOT_SHOWN': |
| 194 continue |
| 195 if field_col is None: |
| 196 fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config) |
| 197 if not fd: |
| 198 field_id_to_col[fv.field_id] = 'NOT_SHOWN', None |
| 199 continue |
| 200 field_col = fd.field_name.lower() |
| 201 field_type = fd.field_type |
| 202 if field_col not in column_values: |
| 203 field_id_to_col[fv.field_id] = 'NOT_SHOWN', None |
| 204 continue |
| 205 field_id_to_col[fv.field_id] = field_col, field_type |
| 206 |
| 207 if field_type == tracker_pb2.FieldTypes.ENUM_TYPE: |
| 208 continue # Already handled by label parsing |
| 209 elif field_type == tracker_pb2.FieldTypes.INT_TYPE: |
| 210 val = fv.int_value |
| 211 elif field_type == tracker_pb2.FieldTypes.STR_TYPE: |
| 212 val = fv.str_value |
| 213 elif field_type == tracker_pb2.FieldTypes.USER_TYPE: |
| 214 user = users_by_id.get(fv.user_id) |
| 215 val = user.email if user else framework_constants.NO_USER_NAME |
| 216 elif field_type == tracker_pb2.FieldTypes.DATE_TYPE: |
| 217 val = fv.int_value # TODO(jrobbins): convert to date |
| 218 elif field_type == tracker_pb2.FieldTypes.BOOL_TYPE: |
| 219 val = 'Yes' if fv.int_value else 'No' |
| 220 |
| 221 column_values[field_col][val] = val |
| 222 |
| 223 # TODO(jrobbins): make the capitalization of well-known unique label and |
| 224 # status values match the way it is written in the issue config. |
| 225 |
| 226 # Return EZTItems for each column in left-to-right display order. |
| 227 result = [] |
| 228 for i, col_name in enumerate(columns): |
| 229 # TODO(jrobbins): sort each set of column values top-to-bottom, by the |
| 230 # order specified in the project artifact config. For now, just sort |
| 231 # lexicographically to make expected output defined. |
| 232 sorted_col_values = sorted(column_values[col_name].values()) |
| 233 result.append(template_helpers.EZTItem( |
| 234 col_index=i, column_name=col_name, filter_values=sorted_col_values)) |
| 235 |
| 236 return result |
| 237 |
| 238 |
| 239 def MakeTableData( |
| 240 visible_results, logged_in_user_id, starred_items, |
| 241 lower_columns, lower_group_by, users_by_id, cell_factories, |
| 242 id_accessor, related_issues, config): |
| 243 """Return a list of list row objects for display by EZT. |
| 244 |
| 245 Args: |
| 246 visible_results: list of artifacts to display on one pagination page. |
| 247 logged_in_user_id: user ID of the signed in user, or None. |
| 248 starred_items: list of IDs/names of items in the current project |
| 249 that the signed in user has starred. |
| 250 lower_columns: list of column names to display, all lowercase. These can |
| 251 be combined column names, e.g., 'priority/pri'. |
| 252 lower_group_by: list of column names that define row groups, all lowercase. |
| 253 users_by_id: dict mapping user IDs to UserViews. |
| 254 cell_factories: dict of functions that each create TableCell objects. |
| 255 id_accessor: function that maps from an artifact to the ID/name that might |
| 256 be in the starred items list. |
| 257 related_issues: dict {issue_id: issue} of pre-fetched related issues. |
| 258 config: ProjectIssueConfig PB for the current project. |
| 259 |
| 260 Returns: |
| 261 A list of TableRow objects, one for each visible result. |
| 262 """ |
| 263 table_data = [] |
| 264 |
| 265 group_cell_factories = [ |
| 266 ChooseCellFactory(group.strip('-'), cell_factories, config) |
| 267 for group in lower_group_by] |
| 268 |
| 269 # Make a list of cell factories, one for each column. |
| 270 factories_to_use = [ |
| 271 ChooseCellFactory(col, cell_factories, config) for col in lower_columns] |
| 272 |
| 273 current_group = None |
| 274 for idx, art in enumerate(visible_results): |
| 275 owner_is_me = ezt.boolean( |
| 276 logged_in_user_id and |
| 277 tracker_bizobj.GetOwnerId(art) == logged_in_user_id) |
| 278 row = MakeRowData( |
| 279 art, lower_columns, owner_is_me, users_by_id, factories_to_use, |
| 280 related_issues, config) |
| 281 row.starred = ezt.boolean(id_accessor(art) in starred_items) |
| 282 row.idx = idx # EZT does not have loop counters, so add idx. |
| 283 table_data.append(row) |
| 284 row.group = None |
| 285 |
| 286 # Also include group information for the first row in each group. |
| 287 # TODO(jrobbins): This seems like more overhead than we need for the |
| 288 # common case where no new group heading row is to be inserted. |
| 289 group = MakeRowData( |
| 290 art, [group_name.strip('-') for group_name in lower_group_by], |
| 291 owner_is_me, users_by_id, group_cell_factories, related_issues, |
| 292 config) |
| 293 for cell, group_name in zip(group.cells, lower_group_by): |
| 294 cell.group_name = group_name |
| 295 if group == current_group: |
| 296 current_group.rows_in_group += 1 |
| 297 else: |
| 298 row.group = group |
| 299 current_group = group |
| 300 current_group.rows_in_group = 1 |
| 301 |
| 302 return table_data |
| 303 |
| 304 |
| 305 def MakeRowData( |
| 306 art, columns, owner_is_me, users_by_id, cell_factory_list, |
| 307 related_issues, config): |
| 308 """Make a TableRow for use by EZT when rendering HTML table of results. |
| 309 |
| 310 Args: |
| 311 art: a project artifact PB |
| 312 columns: list of lower-case column names |
| 313 owner_is_me: boolean indicating that the logged in user is the owner |
| 314 of the current artifact |
| 315 users_by_id: dictionary {user_id: UserView} with each UserView having |
| 316 a "display_name" member. |
| 317 cell_factory_list: list of functions that each create TableCell |
| 318 objects for a given column. |
| 319 related_issues: dict {issue_id: issue} of pre-fetched related issues. |
| 320 config: ProjectIssueConfig PB for the current project. |
| 321 |
| 322 Returns: |
| 323 A TableRow object for use by EZT to render a table of results. |
| 324 """ |
| 325 ordered_row_data = [] |
| 326 non_col_labels = [] |
| 327 label_values = collections.defaultdict(list) |
| 328 |
| 329 flattened_columns = set() |
| 330 for col in columns: |
| 331 if '/' in col: |
| 332 flattened_columns.update(col.split('/')) |
| 333 else: |
| 334 flattened_columns.add(col) |
| 335 |
| 336 # Group all "Key-Value" labels by key, and separate the "OneWord" labels. |
| 337 _AccumulateLabelValues( |
| 338 art.labels, flattened_columns, label_values, non_col_labels) |
| 339 |
| 340 _AccumulateLabelValues( |
| 341 art.derived_labels, flattened_columns, label_values, |
| 342 non_col_labels, is_derived=True) |
| 343 |
| 344 # Build up a list of TableCell objects for this row. |
| 345 for i, col in enumerate(columns): |
| 346 factory = cell_factory_list[i] |
| 347 new_cell = factory( |
| 348 art, col, users_by_id, non_col_labels, label_values, related_issues, |
| 349 config) |
| 350 new_cell.col_index = i |
| 351 ordered_row_data.append(new_cell) |
| 352 |
| 353 return TableRow(ordered_row_data, owner_is_me) |
| 354 |
| 355 |
| 356 def _AccumulateLabelValues( |
| 357 labels, columns, label_values, non_col_labels, is_derived=False): |
| 358 """Parse OneWord and Key-Value labels for display in a list page. |
| 359 |
| 360 Args: |
| 361 labels: a list of label strings. |
| 362 columns: a list of column names. |
| 363 label_values: mutable dictionary {key: [value, ...]} of label values |
| 364 seen so far. |
| 365 non_col_labels: mutable list of OneWord labels seen so far. |
| 366 is_derived: true if these labels were derived via rules. |
| 367 |
| 368 Returns: |
| 369 Nothing. But, the given label_values dictionary will grow to hold |
| 370 the values of the key-value labels passed in, and the non_col_labels |
| 371 list will grow to hold the OneWord labels passed in. These are shown |
| 372 in label columns, and in the summary column, respectively |
| 373 """ |
| 374 for label_name in labels: |
| 375 if '-' in label_name: |
| 376 parts = label_name.split('-') |
| 377 for pivot in range(1, len(parts)): |
| 378 column_name = '-'.join(parts[:pivot]) |
| 379 value = '-'.join(parts[pivot:]) |
| 380 column_name = column_name.lower() |
| 381 if column_name in columns: |
| 382 label_values[column_name].append((value, is_derived)) |
| 383 else: |
| 384 non_col_labels.append((label_name, is_derived)) |
| 385 |
| 386 |
| 387 class TableRow(object): |
| 388 """A tiny auxiliary class to represent a row in an HTML table.""" |
| 389 |
| 390 def __init__(self, cells, owner_is_me): |
| 391 """Initialize the table row with the given data.""" |
| 392 self.cells = cells |
| 393 self.owner_is_me = ezt.boolean(owner_is_me) # Shows tiny ">" on my issues. |
| 394 # Used by MakeTableData for layout. |
| 395 self.idx = None |
| 396 self.group = None |
| 397 self.rows_in_group = None |
| 398 self.starred = None |
| 399 |
| 400 def __cmp__(self, other): |
| 401 """A row is == if each cell is == to the cells in the other row.""" |
| 402 return cmp(self.cells, other.cells) if other else -1 |
| 403 |
| 404 def DebugString(self): |
| 405 """Return a string that is useful for on-page debugging.""" |
| 406 return 'TR(%s)' % self.cells |
| 407 |
| 408 |
| 409 # TODO(jrobbins): also add unsortable... or change this to a list of operations |
| 410 # that can be done. |
| 411 CELL_TYPE_ID = 'ID' |
| 412 CELL_TYPE_SUMMARY = 'summary' |
| 413 CELL_TYPE_ATTR = 'attr' |
| 414 CELL_TYPE_UNFILTERABLE = 'unfilterable' |
| 415 |
| 416 |
| 417 class TableCell(object): |
| 418 """Helper class to represent a table cell when rendering using EZT.""" |
| 419 |
| 420 # Should instances of this class be rendered with whitespace:nowrap? |
| 421 # Subclasses can override this constant, e.g., issuelist TableCellOwner. |
| 422 NOWRAP = ezt.boolean(False) |
| 423 |
| 424 def __init__(self, cell_type, explicit_values, |
| 425 derived_values=None, non_column_labels=None, align='', |
| 426 sort_values=True): |
| 427 """Store all the given data for later access by EZT.""" |
| 428 self.type = cell_type |
| 429 self.align = align |
| 430 self.col_index = 0 # Is set afterward |
| 431 self.values = [] |
| 432 if non_column_labels: |
| 433 self.non_column_labels = [ |
| 434 template_helpers.EZTItem(value=v, is_derived=ezt.boolean(d)) |
| 435 for v, d in non_column_labels] |
| 436 else: |
| 437 self.non_column_labels = [] |
| 438 |
| 439 for v in (sorted(explicit_values) if sort_values else explicit_values): |
| 440 self.values.append(CellItem(v)) |
| 441 |
| 442 if derived_values: |
| 443 for v in (sorted(derived_values) if sort_values else derived_values): |
| 444 self.values.append(CellItem(v, is_derived=True)) |
| 445 |
| 446 def __cmp__(self, other): |
| 447 """A cell is == if each value is == to the values in the other cells.""" |
| 448 return cmp(self.values, other.values) if other else -1 |
| 449 |
| 450 def DebugString(self): |
| 451 return 'TC(%r, %r, %r)' % ( |
| 452 self.type, |
| 453 [v.DebugString() for v in self.values], |
| 454 self.non_column_labels) |
| 455 |
| 456 |
| 457 def CompositeTableCell(columns_to_combine, cell_factories): |
| 458 """Cell factory that combines multiple cells in a combined column.""" |
| 459 |
| 460 class FactoryClass(TableCell): |
| 461 def __init__(self, art, _col, users_by_id, |
| 462 non_col_labels, label_values, related_issues, config): |
| 463 TableCell.__init__(self, CELL_TYPE_UNFILTERABLE, []) |
| 464 |
| 465 for sub_col in columns_to_combine: |
| 466 sub_factory = ChooseCellFactory(sub_col, cell_factories, config) |
| 467 sub_cell = sub_factory( |
| 468 art, sub_col, users_by_id, non_col_labels, label_values, |
| 469 related_issues, config) |
| 470 self.non_column_labels.extend(sub_cell.non_column_labels) |
| 471 self.values.extend(sub_cell.values) |
| 472 |
| 473 return FactoryClass |
| 474 |
| 475 |
| 476 class CellItem(object): |
| 477 """Simple class to display one part of a table cell's value, with style.""" |
| 478 |
| 479 def __init__(self, item, is_derived=False): |
| 480 self.item = item |
| 481 self.is_derived = ezt.boolean(is_derived) |
| 482 |
| 483 def __cmp__(self, other): |
| 484 return cmp(self.item, other.item) if other else -1 |
| 485 |
| 486 def DebugString(self): |
| 487 if self.is_derived: |
| 488 return 'CI(derived: %r)' % self.item |
| 489 else: |
| 490 return 'CI(%r)' % self.item |
| 491 |
| 492 |
| 493 class TableCellKeyLabels(TableCell): |
| 494 """TableCell subclass specifically for showing user-defined label values.""" |
| 495 |
| 496 def __init__( |
| 497 self, _art, col, _users_by_id, _non_col_labels, |
| 498 label_values, _related_issues, _config): |
| 499 label_value_pairs = label_values.get(col, []) |
| 500 explicit_values = [value for value, is_derived in label_value_pairs |
| 501 if not is_derived] |
| 502 derived_values = [value for value, is_derived in label_value_pairs |
| 503 if is_derived] |
| 504 TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values, |
| 505 derived_values=derived_values) |
| 506 |
| 507 |
| 508 class TableCellProject(TableCell): |
| 509 """TableCell subclass for showing an artifact's project name.""" |
| 510 |
| 511 # pylint: disable=unused-argument |
| 512 def __init__( |
| 513 self, art, col, users_by_id, non_col_labels, label_values, |
| 514 _related_issues, _config): |
| 515 TableCell.__init__( |
| 516 self, CELL_TYPE_ATTR, [art.project_name]) |
| 517 |
| 518 |
| 519 class TableCellStars(TableCell): |
| 520 """TableCell subclass for showing an artifact's star count.""" |
| 521 |
| 522 # pylint: disable=unused-argument |
| 523 def __init__( |
| 524 self, art, col, users_by_id, non_col_labels, label_values, |
| 525 _related_issues, _config): |
| 526 TableCell.__init__( |
| 527 self, CELL_TYPE_ATTR, [art.star_count], align='right') |
| 528 |
| 529 |
| 530 class TableCellSummary(TableCell): |
| 531 """TableCell subclass for showing an artifact's summary.""" |
| 532 |
| 533 # pylint: disable=unused-argument |
| 534 def __init__( |
| 535 self, art, col, users_by_id, non_col_labels, label_values, |
| 536 _related_issues, _config): |
| 537 TableCell.__init__( |
| 538 self, CELL_TYPE_SUMMARY, [art.summary], |
| 539 non_column_labels=non_col_labels) |
| 540 |
| 541 |
| 542 class TableCellCustom(TableCell): |
| 543 """Abstract TableCell subclass specifically for showing custom fields.""" |
| 544 |
| 545 def __init__( |
| 546 self, art, col, users_by_id, _non_col_labels, |
| 547 _label_values, _related_issues, config): |
| 548 explicit_values = [] |
| 549 derived_values = [] |
| 550 for fv in art.field_values: |
| 551 # TODO(jrobbins): for cross-project search this could be a list. |
| 552 fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config) |
| 553 if fd.field_name.lower() == col: |
| 554 val = self.ExtractValue(fv, users_by_id) |
| 555 if fv.derived: |
| 556 derived_values.append(val) |
| 557 else: |
| 558 explicit_values.append(val) |
| 559 |
| 560 TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values, |
| 561 derived_values=derived_values) |
| 562 |
| 563 def ExtractValue(self, fv, _users_by_id): |
| 564 return 'field-id-%d-not-implemented-yet' % fv.field_id |
| 565 |
| 566 |
| 567 class TableCellCustomInt(TableCellCustom): |
| 568 """TableCell subclass specifically for showing custom int fields.""" |
| 569 |
| 570 def ExtractValue(self, fv, _users_by_id): |
| 571 return fv.int_value |
| 572 |
| 573 |
| 574 class TableCellCustomStr(TableCellCustom): |
| 575 """TableCell subclass specifically for showing custom str fields.""" |
| 576 |
| 577 def ExtractValue(self, fv, _users_by_id): |
| 578 return fv.str_value |
| 579 |
| 580 |
| 581 class TableCellCustomUser(TableCellCustom): |
| 582 """TableCell subclass specifically for showing custom user fields.""" |
| 583 |
| 584 def ExtractValue(self, fv, users_by_id): |
| 585 if fv.user_id in users_by_id: |
| 586 return users_by_id[fv.user_id].email |
| 587 return 'USER_%d' % fv.user_id |
| 588 |
| 589 |
| 590 class TableCellCustomDate(TableCellCustom): |
| 591 """TableCell subclass specifically for showing custom date fields.""" |
| 592 |
| 593 def ExtractValue(self, fv, _users_by_id): |
| 594 # TODO(jrobbins): convert timestamp to formatted date and time |
| 595 return fv.int_value |
| 596 |
| 597 |
| 598 class TableCellCustomBool(TableCellCustom): |
| 599 """TableCell subclass specifically for showing custom int fields.""" |
| 600 |
| 601 def ExtractValue(self, fv, _users_by_id): |
| 602 return 'Yes' if fv.int_value else 'No' |
| 603 |
| 604 |
| 605 _CUSTOM_FIELD_CELL_FACTORIES = { |
| 606 tracker_pb2.FieldTypes.ENUM_TYPE: TableCellKeyLabels, |
| 607 tracker_pb2.FieldTypes.INT_TYPE: TableCellCustomInt, |
| 608 tracker_pb2.FieldTypes.STR_TYPE: TableCellCustomStr, |
| 609 tracker_pb2.FieldTypes.USER_TYPE: TableCellCustomUser, |
| 610 tracker_pb2.FieldTypes.DATE_TYPE: TableCellCustomDate, |
| 611 tracker_pb2.FieldTypes.BOOL_TYPE: TableCellCustomBool, |
| 612 } |
| 613 |
| 614 |
| 615 def ChooseCellFactory(col, cell_factories, config): |
| 616 """Return the CellFactory to use for the given column.""" |
| 617 if col in cell_factories: |
| 618 return cell_factories[col] |
| 619 |
| 620 if '/' in col: |
| 621 return CompositeTableCell(col.split('/'), cell_factories) |
| 622 |
| 623 fd = tracker_bizobj.FindFieldDef(col, config) |
| 624 if fd: |
| 625 return _CUSTOM_FIELD_CELL_FACTORIES[fd.field_type] |
| 626 |
| 627 return TableCellKeyLabels |
OLD | NEW |