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 grids of project artifacts. |
| 7 |
| 8 A grid is a two-dimensional display of items where the user can choose |
| 9 the X and Y axes. |
| 10 """ |
| 11 |
| 12 import collections |
| 13 import logging |
| 14 |
| 15 from framework import framework_constants |
| 16 from framework import sorting |
| 17 from framework import template_helpers |
| 18 from proto import tracker_pb2 |
| 19 from tracker import tracker_bizobj |
| 20 |
| 21 |
| 22 # We shorten long attribute values to fit into the table cells. |
| 23 _MAX_CELL_DISPLAY_CHARS = 70 |
| 24 |
| 25 |
| 26 def SortGridHeadings(col_name, heading_value_list, users_by_id, config, |
| 27 asc_accessors): |
| 28 """Sort the grid headings according to well-known status and label order. |
| 29 |
| 30 Args: |
| 31 col_name: String column name that is used on that grid axis. |
| 32 heading_value_list: List of grid row or column heading values. |
| 33 users_by_id: Dict mapping user_ids to UserViews. |
| 34 config: ProjectIssueConfig PB for the current project. |
| 35 asc_accessors: Dict (col_name -> function()) for special columns. |
| 36 |
| 37 Returns: |
| 38 The same heading values, but sorted in a logical order. |
| 39 """ |
| 40 decorated_list = [] |
| 41 fd = tracker_bizobj.FindFieldDef(col_name, config) |
| 42 if fd: # Handle fields. |
| 43 for value in heading_value_list: |
| 44 field_value = tracker_bizobj.GetFieldValueWithRawValue( |
| 45 fd.field_type, None, users_by_id, value) |
| 46 decorated_list.append([field_value, field_value]) |
| 47 elif col_name == 'status': |
| 48 wk_statuses = [wks.status.lower() |
| 49 for wks in config.well_known_statuses] |
| 50 decorated_list = [(_WKSortingValue(value.lower(), wk_statuses), value) |
| 51 for value in heading_value_list] |
| 52 |
| 53 elif col_name in asc_accessors: # Special cols still sort alphabetically. |
| 54 decorated_list = [(value, value) |
| 55 for value in heading_value_list] |
| 56 |
| 57 else: # Anything else is assumed to be a label prefix |
| 58 wk_labels = [wkl.label.lower().split('-', 1)[-1] |
| 59 for wkl in config.well_known_labels] |
| 60 decorated_list = [(_WKSortingValue(value.lower(), wk_labels), value) |
| 61 for value in heading_value_list] |
| 62 |
| 63 decorated_list.sort() |
| 64 result = [decorated_tuple[1] for decorated_tuple in decorated_list] |
| 65 logging.info('Headers for %s are: %r', col_name, result) |
| 66 return result |
| 67 |
| 68 |
| 69 def _WKSortingValue(value, well_known_list): |
| 70 """Return a value used to sort headings so that well-known ones are first.""" |
| 71 if not value: |
| 72 return sorting.MAX_STRING # Undefined values sort last. |
| 73 try: |
| 74 # well-known values sort by index |
| 75 return well_known_list.index(value) |
| 76 except ValueError: |
| 77 return value # odd-ball values lexicographically after all well-known ones |
| 78 |
| 79 |
| 80 def MakeGridData( |
| 81 artifacts, x_attr, x_headings, y_attr, y_headings, users_by_id, |
| 82 artifact_view_factory, all_label_values, config): |
| 83 """Return a list of grid row items for display by EZT. |
| 84 |
| 85 Args: |
| 86 artifacts: a list of issues to consider showing. |
| 87 x_attr: lowercase name of the attribute that defines the x-axis. |
| 88 x_headings: list of values for column headings. |
| 89 y_attr: lowercase name of the attribute that defines the y-axis. |
| 90 y_headings: list of values for row headings. |
| 91 users_by_id: dict {user_id: user_view, ...} for referenced users. |
| 92 artifact_view_factory: constructor for grid tiles. |
| 93 all_label_values: pre-parsed dictionary of values from the key-value |
| 94 labels on each issue: {issue_id: {key: [val,...], ...}, ...} |
| 95 config: ProjectIssueConfig PB for the current project. |
| 96 |
| 97 Returns: |
| 98 A list of EZTItems, each representing one grid row, and each having |
| 99 a nested list of grid cells. |
| 100 |
| 101 Each grid row has a row name, and a list of cells. Each cell has a |
| 102 list of tiles. Each tile represents one artifact. Artifacts are |
| 103 represented once in each cell that they match, so one artifact that |
| 104 has multiple values for a certain attribute can occur in multiple cells. |
| 105 """ |
| 106 x_attr = x_attr.lower() |
| 107 y_attr = y_attr.lower() |
| 108 |
| 109 # A flat dictionary {(x, y): [cell, ...], ...] for the whole grid. |
| 110 x_y_data = collections.defaultdict(list) |
| 111 |
| 112 # Put each issue into the grid cell(s) where it belongs. |
| 113 for art in artifacts: |
| 114 label_value_dict = all_label_values[art.local_id] |
| 115 x_vals = GetArtifactAttr( |
| 116 art, x_attr, users_by_id, label_value_dict, config) |
| 117 y_vals = GetArtifactAttr( |
| 118 art, y_attr, users_by_id, label_value_dict, config) |
| 119 tile = artifact_view_factory(art) |
| 120 |
| 121 # Put the current issue into each cell where it belongs, which will usually |
| 122 # be exactly 1 cell, but it could be a few. |
| 123 if x_attr != '--' and y_attr != '--': # User specified both axes. |
| 124 for x in x_vals: |
| 125 for y in y_vals: |
| 126 x_y_data[x, y].append(tile) |
| 127 elif y_attr != '--': # User only specified Y axis. |
| 128 for y in y_vals: |
| 129 x_y_data['All', y].append(tile) |
| 130 elif x_attr != '--': # User only specified X axis. |
| 131 for x in x_vals: |
| 132 x_y_data[x, 'All'].append(tile) |
| 133 else: # User specified neither axis. |
| 134 x_y_data['All', 'All'].append(tile) |
| 135 |
| 136 # Convert the dictionary to a list-of-lists so that EZT can iterate over it. |
| 137 grid_data = [] |
| 138 for y in y_headings: |
| 139 cells_in_row = [] |
| 140 for x in x_headings: |
| 141 tiles = x_y_data[x, y] |
| 142 |
| 143 drill_down = '' |
| 144 if x_attr != '--': |
| 145 drill_down = MakeDrillDownSearch(x_attr, x) |
| 146 if y_attr != '--': |
| 147 drill_down += MakeDrillDownSearch(y_attr, y) |
| 148 |
| 149 cells_in_row.append(template_helpers.EZTItem( |
| 150 tiles=tiles, count=len(tiles), drill_down=drill_down)) |
| 151 grid_data.append(template_helpers.EZTItem( |
| 152 grid_y_heading=y, cells_in_row=cells_in_row)) |
| 153 |
| 154 return grid_data |
| 155 |
| 156 |
| 157 def MakeDrillDownSearch(attr, value): |
| 158 """Constructs search term for drill-down. |
| 159 |
| 160 Args: |
| 161 attr: lowercase name of the attribute to narrow the search on. |
| 162 value: value to narrow the search to. |
| 163 |
| 164 Returns: |
| 165 String with user-query term to narrow a search to the given attr value. |
| 166 """ |
| 167 if value == framework_constants.NO_VALUES: |
| 168 return '-has:%s ' % attr |
| 169 else: |
| 170 return '%s=%s ' % (attr, value) |
| 171 |
| 172 |
| 173 def MakeLabelValuesDict(art): |
| 174 """Return a dict of label values and a list of one-word labels. |
| 175 |
| 176 Args: |
| 177 art: artifact object, e.g., an issue PB. |
| 178 |
| 179 Returns: |
| 180 A dict {prefix: [suffix,...], ...} for each key-value label. |
| 181 """ |
| 182 label_values = collections.defaultdict(list) |
| 183 for label_name in tracker_bizobj.GetLabels(art): |
| 184 if '-' in label_name: |
| 185 key, value = label_name.split('-', 1) |
| 186 label_values[key.lower()].append(value) |
| 187 |
| 188 return label_values |
| 189 |
| 190 |
| 191 def GetArtifactAttr( |
| 192 art, attribute_name, users_by_id, label_attr_values_dict, config): |
| 193 """Return the requested attribute values of the given artifact. |
| 194 |
| 195 Args: |
| 196 art: a tracked artifact with labels, local_id, summary, stars, and owner. |
| 197 attribute_name: lowercase string name of attribute to get. |
| 198 users_by_id: dictionary of UserViews already created. |
| 199 label_attr_values_dict: dictionary {'key': [value, ...], }. |
| 200 config: ProjectIssueConfig PB for the current project. |
| 201 |
| 202 Returns: |
| 203 A list of string attribute values, or [framework_constants.NO_VALUES] |
| 204 if the artifact has no value for that attribute. |
| 205 """ |
| 206 if attribute_name == '--': |
| 207 return [] |
| 208 if attribute_name == 'id': |
| 209 return [art.local_id] |
| 210 if attribute_name == 'summary': |
| 211 return [art.summary] |
| 212 if attribute_name == 'status': |
| 213 return [tracker_bizobj.GetStatus(art)] |
| 214 if attribute_name == 'stars': |
| 215 return [art.star_count] |
| 216 if attribute_name == 'attachments': |
| 217 return [art.attachment_count] |
| 218 # TODO(jrobbins): support blocked on, blocking, and mergedinto. |
| 219 if attribute_name == 'reporter': |
| 220 return [users_by_id[art.reporter_id].display_name] |
| 221 if attribute_name == 'owner': |
| 222 owner_id = tracker_bizobj.GetOwnerId(art) |
| 223 if not owner_id: |
| 224 return [framework_constants.NO_VALUES] |
| 225 else: |
| 226 return [users_by_id[owner_id].display_name] |
| 227 if attribute_name == 'cc': |
| 228 cc_ids = tracker_bizobj.GetCcIds(art) |
| 229 if not cc_ids: |
| 230 return [framework_constants.NO_VALUES] |
| 231 else: |
| 232 return [users_by_id[cc_id].display_name for cc_id in cc_ids] |
| 233 if attribute_name == 'component': |
| 234 comp_ids = list(art.component_ids) + list(art.derived_component_ids) |
| 235 if not comp_ids: |
| 236 return [framework_constants.NO_VALUES] |
| 237 else: |
| 238 paths = [] |
| 239 for comp_id in comp_ids: |
| 240 cd = tracker_bizobj.FindComponentDefByID(comp_id, config) |
| 241 if cd: |
| 242 paths.append(cd.path) |
| 243 return paths |
| 244 |
| 245 # Check to see if it is a field. Process as field only if it is not an enum |
| 246 # type because enum types are stored as key-value labels. |
| 247 fd = tracker_bizobj.FindFieldDef(attribute_name, config) |
| 248 if fd and fd.field_type != tracker_pb2.FieldTypes.ENUM_TYPE: |
| 249 values = [] |
| 250 for fv in art.field_values: |
| 251 if fv.field_id == fd.field_id: |
| 252 value = tracker_bizobj.GetFieldValueWithRawValue( |
| 253 fd.field_type, fv, users_by_id, None) |
| 254 values.append(value) |
| 255 return values |
| 256 |
| 257 # Since it is not a built-in attribute or a field, it must be a key-value |
| 258 # label. |
| 259 return label_attr_values_dict.get( |
| 260 attribute_name, [framework_constants.NO_VALUES]) |
| 261 |
| 262 |
| 263 def AnyArtifactHasNoAttr( |
| 264 artifacts, attr_name, users_by_id, all_label_values, config): |
| 265 """Return true if any artifact does not have a value for attr_name.""" |
| 266 # TODO(jrobbins): all_label_values needs to be keyed by issue_id to allow |
| 267 # cross-project grid views. |
| 268 for art in artifacts: |
| 269 vals = GetArtifactAttr( |
| 270 art, attr_name.lower(), users_by_id, all_label_values[art.local_id], |
| 271 config) |
| 272 if framework_constants.NO_VALUES in vals: |
| 273 return True |
| 274 |
| 275 return False |
OLD | NEW |