Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame^] | 1 | # 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 | """Helper functions for sorting lists of project artifacts. |
| 7 | |
| 8 | This module exports the SortArtifacts function that does sorting of |
| 9 | Monorail business objects (e.g., an issue). The sorting is done by |
| 10 | extracting relevant values from the PB using a dictionary of |
| 11 | accessor functions. |
| 12 | |
| 13 | The desired sorting directives are specified in part of the user's |
| 14 | HTTP request. This sort spec consists of the names of the columns |
| 15 | with optional minus signs to indicate descending sort order. |
| 16 | |
| 17 | The tool configuration object also affects sorting. When sorting by |
| 18 | key-value labels, the well-known labels are considered to come |
| 19 | before any non-well-known labels, and those well-known labels sort in |
| 20 | the order in which they are defined in the tool config PB. |
| 21 | """ |
| 22 | from __future__ import print_function |
| 23 | from __future__ import division |
| 24 | from __future__ import absolute_import |
| 25 | |
| 26 | from functools import total_ordering |
| 27 | |
| 28 | import settings |
| 29 | from proto import tracker_pb2 |
| 30 | from services import caches |
| 31 | from tracker import tracker_bizobj |
| 32 | from tracker import tracker_constants |
| 33 | |
| 34 | |
| 35 | @total_ordering |
| 36 | class DescendingValue(object): |
| 37 | """A wrapper which reverses the sort order of values.""" |
| 38 | |
| 39 | @classmethod |
| 40 | def MakeDescendingValue(cls, obj): |
| 41 | """Make a value that sorts in the reverse order as obj.""" |
| 42 | if isinstance(obj, int): |
| 43 | return -obj |
| 44 | if obj == MAX_STRING: |
| 45 | return MIN_STRING |
| 46 | if obj == MIN_STRING: |
| 47 | return MAX_STRING |
| 48 | if isinstance(obj, list): |
| 49 | return [cls.MakeDescendingValue(item) for item in reversed(obj)] |
| 50 | return DescendingValue(obj) |
| 51 | |
| 52 | def __init__(self, val): |
| 53 | self.val = val |
| 54 | |
| 55 | def __eq__(self, other): |
| 56 | if isinstance(other, DescendingValue): |
| 57 | return self.val == other.val |
| 58 | return self.val == other |
| 59 | |
| 60 | def __ne__(self, other): |
| 61 | if isinstance(other, DescendingValue): |
| 62 | return self.val != other.val |
| 63 | return self.val != other |
| 64 | |
| 65 | def __lt__(self, other): |
| 66 | if isinstance(other, DescendingValue): |
| 67 | return other.val < self.val |
| 68 | return other < self.val |
| 69 | |
| 70 | def __repr__(self): |
| 71 | return 'DescendingValue(%r)' % self.val |
| 72 | |
| 73 | |
| 74 | # A string that sorts after every other string, and one that sorts before them. |
| 75 | MAX_STRING = '~~~' |
| 76 | MIN_STRING = DescendingValue(MAX_STRING) |
| 77 | |
| 78 | |
| 79 | # RAMCache {issue_id: {column_name: sort_key, ...}, ...} |
| 80 | art_values_cache = None |
| 81 | |
| 82 | |
| 83 | def InitializeArtValues(services): |
| 84 | global art_values_cache |
| 85 | art_values_cache = caches.RamCache( |
| 86 | services.cache_manager, 'issue', max_size=settings.issue_cache_max_size) |
| 87 | |
| 88 | |
| 89 | def InvalidateArtValuesKeys(cnxn, keys): |
| 90 | art_values_cache.InvalidateKeys(cnxn, keys) |
| 91 | |
| 92 | |
| 93 | def SortArtifacts( |
| 94 | artifacts, config, accessors, postprocessors, group_by_spec, sort_spec, |
| 95 | users_by_id=None, tie_breakers=None): |
| 96 | """Return a list of artifacts sorted by the user's sort specification. |
| 97 | |
| 98 | In the following, an "accessor" is a function(art) -> [field_value, ...]. |
| 99 | |
| 100 | Args: |
| 101 | artifacts: an unsorted list of project artifact PBs. |
| 102 | config: Project config PB instance that defines the sort order for |
| 103 | labels and statuses in this project. |
| 104 | accessors: dict {column_name: accessor} to get values from the artifacts. |
| 105 | postprocessors: dict {column_name: postprocessor} to get user emails |
| 106 | and timestamps. |
| 107 | group_by_spec: string that lists the grouping order |
| 108 | sort_spec: string that lists the sort order |
| 109 | users_by_id: optional dictionary {user_id: user_view,...} for all users |
| 110 | who participate in the list of artifacts. |
| 111 | tie_breakers: list of column names to add to the end of the sort |
| 112 | spec if they are not already somewhere in the sort spec. |
| 113 | |
| 114 | Returns: |
| 115 | A sorted list of artifacts. |
| 116 | |
| 117 | Note: if username_cols is supplied, then users_by_id should be too. |
| 118 | |
| 119 | The approach to sorting is to construct a comprehensive sort key for |
| 120 | each artifact. To create the sort key, we (a) build lists with a |
| 121 | variable number of fields to sort on, and (b) allow individual |
| 122 | fields to be sorted in descending order. Even with the time taken |
| 123 | to build the sort keys, calling sorted() with the key seems to be |
| 124 | faster overall than doing multiple stable-sorts or doing one sort |
| 125 | using a multi-field comparison function. |
| 126 | """ |
| 127 | sort_directives = ComputeSortDirectives( |
| 128 | config, group_by_spec, sort_spec, tie_breakers=tie_breakers) |
| 129 | |
| 130 | # Build a list of accessors that will extract sort keys from the issues. |
| 131 | accessor_pairs = [ |
| 132 | (sd, _MakeCombinedSortKeyAccessor( |
| 133 | sd, config, accessors, postprocessors, users_by_id)) |
| 134 | for sd in sort_directives] |
| 135 | |
| 136 | def SortKey(art): |
| 137 | """Make a sort_key for the given artifact, used by sorted() below.""" |
| 138 | if art_values_cache.HasItem(art.issue_id): |
| 139 | art_values = art_values_cache.GetItem(art.issue_id) |
| 140 | else: |
| 141 | art_values = {} |
| 142 | |
| 143 | sort_key = [] |
| 144 | for sd, accessor in accessor_pairs: |
| 145 | if sd not in art_values: |
| 146 | art_values[sd] = accessor(art) |
| 147 | sort_key.append(art_values[sd]) |
| 148 | |
| 149 | art_values_cache.CacheItem(art.issue_id, art_values) |
| 150 | return sort_key |
| 151 | |
| 152 | return sorted(artifacts, key=SortKey) |
| 153 | |
| 154 | |
| 155 | def ComputeSortDirectives(config, group_by_spec, sort_spec, tie_breakers=None): |
| 156 | """Return a list with sort directives to be used in sorting. |
| 157 | |
| 158 | Args: |
| 159 | config: Project config PB instance that defines the sort order for |
| 160 | labels and statuses in this project. |
| 161 | group_by_spec: string that lists the grouping order |
| 162 | sort_spec: string that lists the sort order |
| 163 | tie_breakers: list of column names to add to the end of the sort |
| 164 | spec if they are not already somewhere in the sort spec. |
| 165 | |
| 166 | Returns: |
| 167 | A list of lower-case column names, each one may have a leading |
| 168 | minus-sign. |
| 169 | """ |
| 170 | # Prepend the end-user's sort spec to any project default sort spec. |
| 171 | if tie_breakers is None: |
| 172 | tie_breakers = ['id'] |
| 173 | sort_spec = '%s %s %s' % ( |
| 174 | group_by_spec, sort_spec, config.default_sort_spec) |
| 175 | # Sort specs can have interfering sort orders, so remove any duplicates. |
| 176 | field_names = set() |
| 177 | sort_directives = [] |
| 178 | for sort_directive in sort_spec.lower().split(): |
| 179 | field_name = sort_directive.lstrip('-') |
| 180 | if field_name not in field_names: |
| 181 | sort_directives.append(sort_directive) |
| 182 | field_names.add(field_name) |
| 183 | |
| 184 | # Add in the project name so that the overall ordering is completely |
| 185 | # defined in cross-project search. Otherwise, issues jump up and |
| 186 | # down on each reload of the same query, and prev/next links get |
| 187 | # messed up. It's a no-op in single projects. |
| 188 | if 'project' not in sort_directives: |
| 189 | sort_directives.append('project') |
| 190 | |
| 191 | for tie_breaker in tie_breakers: |
| 192 | if tie_breaker not in sort_directives: |
| 193 | sort_directives.append(tie_breaker) |
| 194 | |
| 195 | return sort_directives |
| 196 | |
| 197 | |
| 198 | def _MakeCombinedSortKeyAccessor( |
| 199 | sort_directive, config, accessors, postprocessors, users_by_id): |
| 200 | """Return an accessor that extracts a sort key for a UI table column. |
| 201 | |
| 202 | Args: |
| 203 | sort_directive: string with column name and optional leading minus sign, |
| 204 | for combined columns, it may have slashes, e.g., "-priority/pri". |
| 205 | config: ProjectIssueConfig instance that defines the sort order for |
| 206 | labels and statuses in this project. |
| 207 | accessors: dictionary of (column_name -> accessor) to get values |
| 208 | from the artifacts. |
| 209 | postprocessors: dict {column_name: postprocessor} to get user emails |
| 210 | and timestamps. |
| 211 | users_by_id: dictionary {user_id: user_view,...} for all users |
| 212 | who participate in the list of artifacts (e.g., owners, reporters, cc). |
| 213 | |
| 214 | Returns: |
| 215 | A list of accessor functions that can be applied to an issue to extract |
| 216 | the relevant sort key value. |
| 217 | |
| 218 | The strings for status and labels are converted to lower case in |
| 219 | this method so that they sort like case-insensitive enumerations. |
| 220 | Any component-specific field of the artifact is sorted according to the |
| 221 | value returned by the accessors defined in that component. Those |
| 222 | accessor functions should lower case string values for fields where |
| 223 | case-insensitive sorting is desired. |
| 224 | """ |
| 225 | if sort_directive.startswith('-'): |
| 226 | combined_col_name = sort_directive[1:] |
| 227 | descending = True |
| 228 | else: |
| 229 | combined_col_name = sort_directive |
| 230 | descending = False |
| 231 | |
| 232 | wk_labels = [wkl.label for wkl in config.well_known_labels] |
| 233 | accessors = [ |
| 234 | _MakeSingleSortKeyAccessor( |
| 235 | col_name, config, accessors, postprocessors, users_by_id, wk_labels) |
| 236 | for col_name in combined_col_name.split('/')] |
| 237 | |
| 238 | # The most common case is that we sort on a single column, like "priority". |
| 239 | if len(accessors) == 1: |
| 240 | return _MaybeMakeDescending(accessors[0], descending) |
| 241 | |
| 242 | # Less commonly, we are sorting on a combined column like "priority/pri". |
| 243 | def CombinedAccessor(art): |
| 244 | """Flatten and sort the values for each column in a combined column.""" |
| 245 | key_part = [] |
| 246 | for single_accessor in accessors: |
| 247 | value = single_accessor(art) |
| 248 | if isinstance(value, list): |
| 249 | key_part.extend(value) |
| 250 | else: |
| 251 | key_part.append(value) |
| 252 | return sorted(key_part) |
| 253 | |
| 254 | return _MaybeMakeDescending(CombinedAccessor, descending) |
| 255 | |
| 256 | |
| 257 | def _MaybeMakeDescending(accessor, descending): |
| 258 | """If descending is True, return a new function that reverses accessor.""" |
| 259 | if not descending: |
| 260 | return accessor |
| 261 | |
| 262 | def DescendingAccessor(art): |
| 263 | asc_value = accessor(art) |
| 264 | return DescendingValue.MakeDescendingValue(asc_value) |
| 265 | |
| 266 | return DescendingAccessor |
| 267 | |
| 268 | |
| 269 | def _MakeSingleSortKeyAccessor( |
| 270 | col_name, config, accessors, postprocessors, users_by_id, wk_labels): |
| 271 | """Return an accessor function for a single simple UI column.""" |
| 272 | # Case 1. Handle built-in fields: status, component. |
| 273 | if col_name == 'status': |
| 274 | wk_statuses = [wks.status for wks in config.well_known_statuses] |
| 275 | return _IndexOrLexical(wk_statuses, accessors[col_name]) |
| 276 | |
| 277 | if col_name == 'component': |
| 278 | comp_defs = sorted(config.component_defs, key=lambda cd: cd.path.lower()) |
| 279 | comp_ids = [cd.component_id for cd in comp_defs] |
| 280 | return _IndexListAccessor(comp_ids, accessors[col_name]) |
| 281 | |
| 282 | # Case 2. Any other defined accessor functions. |
| 283 | if col_name in accessors: |
| 284 | if postprocessors and col_name in postprocessors: |
| 285 | # sort users by email address or timestamp rather than user ids. |
| 286 | return _MakeAccessorWithPostProcessor( |
| 287 | users_by_id, accessors[col_name], postprocessors[col_name]) |
| 288 | else: |
| 289 | return accessors[col_name] |
| 290 | |
| 291 | # Case 3. Anything else is assumed to be a label prefix or custom field. |
| 292 | return _IndexOrLexicalList( |
| 293 | wk_labels, config.field_defs, col_name, users_by_id) |
| 294 | |
| 295 | |
| 296 | IGNORABLE_INDICATOR = -1 |
| 297 | |
| 298 | |
| 299 | def _PrecomputeSortIndexes(values, col_name): |
| 300 | """Precompute indexes of strings in the values list for fast lookup later.""" |
| 301 | # Make a dictionary that immediately gives us the index of any value |
| 302 | # in the list, and also add the same values in all-lower letters. In |
| 303 | # the case where two values differ only by case, the later value wins, |
| 304 | # which is fine. |
| 305 | indexes = {} |
| 306 | if col_name: |
| 307 | prefix = col_name + '-' |
| 308 | else: |
| 309 | prefix = '' |
| 310 | for idx, val in enumerate(values): |
| 311 | if val.lower().startswith(prefix): |
| 312 | indexes[val] = idx |
| 313 | indexes[val.lower()] = idx |
| 314 | else: |
| 315 | indexes[val] = IGNORABLE_INDICATOR |
| 316 | indexes[val.lower()] = IGNORABLE_INDICATOR |
| 317 | |
| 318 | return indexes |
| 319 | |
| 320 | |
| 321 | def _MakeAccessorWithPostProcessor(users_by_id, base_accessor, postprocessor): |
| 322 | """Make an accessor that returns a list of user_view properties for sorting. |
| 323 | |
| 324 | Args: |
| 325 | users_by_id: dictionary {user_id: user_view, ...} for all participants |
| 326 | in the entire list of artifacts. |
| 327 | base_accessor: an accessor function f(artifact) -> user_id. |
| 328 | postprocessor: function f(user_view) -> single sortable value. |
| 329 | |
| 330 | Returns: |
| 331 | An accessor f(artifact) -> value that can be used in sorting |
| 332 | the decorated list. |
| 333 | """ |
| 334 | |
| 335 | def Accessor(art): |
| 336 | """Return a user edit name for the given artifact's base_accessor.""" |
| 337 | id_or_id_list = base_accessor(art) |
| 338 | if isinstance(id_or_id_list, list): |
| 339 | values = [postprocessor(users_by_id[user_id]) |
| 340 | for user_id in id_or_id_list] |
| 341 | else: |
| 342 | values = [postprocessor(users_by_id[id_or_id_list])] |
| 343 | |
| 344 | return sorted(values) or MAX_STRING |
| 345 | |
| 346 | return Accessor |
| 347 | |
| 348 | |
| 349 | def _MakeColumnAccessor(col_name): |
| 350 | """Make an accessor for an issue's labels that have col_name as a prefix. |
| 351 | |
| 352 | Args: |
| 353 | col_name: string column name. |
| 354 | |
| 355 | Returns: |
| 356 | An accessor that can be applied to an artifact to return a list of |
| 357 | labels that have col_name as a prefix. |
| 358 | |
| 359 | For example, _MakeColumnAccessor('priority')(issue) could result in |
| 360 | [], or ['priority-high'], or a longer list for multi-valued labels. |
| 361 | """ |
| 362 | prefix = col_name + '-' |
| 363 | |
| 364 | def Accessor(art): |
| 365 | """Return a list of label values on the given artifact.""" |
| 366 | result = [label.lower() for label in tracker_bizobj.GetLabels(art) |
| 367 | if label.lower().startswith(prefix)] |
| 368 | return result |
| 369 | |
| 370 | return Accessor |
| 371 | |
| 372 | |
| 373 | def _IndexOrLexical(wk_values, base_accessor): |
| 374 | """Return an accessor to score an artifact based on a user-defined ordering. |
| 375 | |
| 376 | Args: |
| 377 | wk_values: a list of well-known status values from the config. |
| 378 | base_accessor: function that gets a field from a given issue. |
| 379 | |
| 380 | Returns: |
| 381 | An accessor that can be applied to an issue to return a suitable |
| 382 | sort key. |
| 383 | |
| 384 | For example, when used to sort issue statuses, these accessors return an |
| 385 | integer for well-known statuses, a string for odd-ball statuses, and an |
| 386 | extreme value key for issues with no status. That causes issues to appear |
| 387 | in the expected order with odd-ball issues sorted lexicographically after |
| 388 | the ones with well-known status values, and issues with no defined status at |
| 389 | the very end. |
| 390 | """ |
| 391 | well_known_value_indexes = _PrecomputeSortIndexes(wk_values, '') |
| 392 | |
| 393 | def Accessor(art): |
| 394 | """Custom-made function to return a specific value of any issue.""" |
| 395 | value = base_accessor(art) |
| 396 | if not value: |
| 397 | # Undefined values sort last. |
| 398 | return MAX_STRING |
| 399 | |
| 400 | try: |
| 401 | # Well-known values sort by index. Ascending sorting has positive ints |
| 402 | # in well_known_value_indexes. |
| 403 | return well_known_value_indexes[value] |
| 404 | except KeyError: |
| 405 | # Odd-ball values after well-known and lexicographically. |
| 406 | return value.lower() |
| 407 | |
| 408 | return Accessor |
| 409 | |
| 410 | |
| 411 | def _IndexListAccessor(wk_values, base_accessor): |
| 412 | """Return an accessor to score an artifact based on a user-defined ordering. |
| 413 | |
| 414 | Args: |
| 415 | wk_values: a list of well-known values from the config. |
| 416 | base_accessor: function that gets a field from a given issue. |
| 417 | |
| 418 | Returns: |
| 419 | An accessor that can be applied to an issue to return a suitable |
| 420 | sort key. |
| 421 | """ |
| 422 | well_known_value_indexes = { |
| 423 | val: idx for idx, val in enumerate(wk_values)} |
| 424 | |
| 425 | def Accessor(art): |
| 426 | """Custom-made function to return a specific value of any issue.""" |
| 427 | values = base_accessor(art) |
| 428 | if not values: |
| 429 | # Undefined values sort last. |
| 430 | return MAX_STRING |
| 431 | |
| 432 | indexes = [well_known_value_indexes.get(val, MAX_STRING) for val in values] |
| 433 | return sorted(indexes) |
| 434 | |
| 435 | return Accessor |
| 436 | |
| 437 | |
| 438 | def _IndexOrLexicalList(wk_values, full_fd_list, col_name, users_by_id): |
| 439 | """Return an accessor to score an artifact based on a user-defined ordering. |
| 440 | |
| 441 | Args: |
| 442 | wk_values: A list of well-known labels from the config. |
| 443 | full_fd_list: list of FieldDef PBs that belong to the config. |
| 444 | col_name: lowercase string name of the column that will be sorted on. |
| 445 | users_by_id: A dictionary {user_id: user_view}. |
| 446 | |
| 447 | Returns: |
| 448 | An accessor that can be applied to an issue to return a suitable |
| 449 | sort key. |
| 450 | """ |
| 451 | well_known_value_indexes = _PrecomputeSortIndexes(wk_values, col_name) |
| 452 | |
| 453 | if col_name.endswith(tracker_constants.APPROVER_COL_SUFFIX): |
| 454 | # Custom field names cannot end with the APPROVER_COL_SUFFIX. So the only |
| 455 | # possible relevant values are approvers for an APPROVAL_TYPE named |
| 456 | # field_name and any values from labels with the key 'field_name-approvers'. |
| 457 | field_name = col_name[:-len(tracker_constants.APPROVER_COL_SUFFIX)] |
| 458 | approval_fds = [fd for fd in full_fd_list |
| 459 | if (fd.field_name.lower() == field_name and |
| 460 | fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE)] |
| 461 | |
| 462 | def ApproverAccessor(art): |
| 463 | """Custom-made function to return a sort value or an issue's approvers.""" |
| 464 | idx_or_lex_list = ( |
| 465 | _SortableApprovalApproverValues(art, approval_fds, users_by_id) + |
| 466 | _SortableLabelValues(art, col_name, well_known_value_indexes)) |
| 467 | if not idx_or_lex_list: |
| 468 | return MAX_STRING # issues with no value sort to the end of the list. |
| 469 | return sorted(idx_or_lex_list) |
| 470 | |
| 471 | return ApproverAccessor |
| 472 | |
| 473 | # Column name does not end with APPROVER_COL_SUFFIX, so relevant values |
| 474 | # are Approval statuses or Field Values for fields named col_name and |
| 475 | # values from labels with the key equal to col_name. |
| 476 | field_name = col_name |
| 477 | phase_name = None |
| 478 | if '.' in col_name: |
| 479 | phase_name, field_name = col_name.split('.', 1) |
| 480 | |
| 481 | fd_list = [fd for fd in full_fd_list |
| 482 | if (fd.field_name.lower() == field_name and |
| 483 | fd.field_type != tracker_pb2.FieldTypes.ENUM_TYPE and |
| 484 | bool(phase_name) == fd.is_phase_field)] |
| 485 | approval_fds = [] |
| 486 | if not phase_name: |
| 487 | approval_fds = [fd for fd in fd_list if |
| 488 | fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE] |
| 489 | |
| 490 | def Accessor(art): |
| 491 | """Custom-made function to return a sort value for any issue.""" |
| 492 | idx_or_lex_list = ( |
| 493 | _SortableApprovalStatusValues(art, approval_fds) + |
| 494 | _SortableFieldValues(art, fd_list, users_by_id, phase_name) + |
| 495 | _SortableLabelValues(art, col_name, well_known_value_indexes)) |
| 496 | if not idx_or_lex_list: |
| 497 | return MAX_STRING # issues with no value sort to the end of the list. |
| 498 | return sorted(idx_or_lex_list) |
| 499 | |
| 500 | return Accessor |
| 501 | |
| 502 | |
| 503 | def _SortableApprovalStatusValues(art, fd_list): |
| 504 | """Return a list of approval statuses relevant to one UI table column.""" |
| 505 | sortable_value_list = [] |
| 506 | for fd in fd_list: |
| 507 | for av in art.approval_values: |
| 508 | if av.approval_id == fd.field_id: |
| 509 | # Order approval statuses by life cycle. |
| 510 | # NOT_SET == 8 but should be before all other statuses. |
| 511 | sortable_value_list.append( |
| 512 | 0 if av.status.number == 8 else av.status.number) |
| 513 | |
| 514 | return sortable_value_list |
| 515 | |
| 516 | |
| 517 | def _SortableApprovalApproverValues(art, fd_list, users_by_id): |
| 518 | """Return a list of approval approvers relevant to one UI table column.""" |
| 519 | sortable_value_list = [] |
| 520 | for fd in fd_list: |
| 521 | for av in art.approval_values: |
| 522 | if av.approval_id == fd.field_id: |
| 523 | sortable_value_list.extend( |
| 524 | [users_by_id.get(approver_id).email |
| 525 | for approver_id in av.approver_ids |
| 526 | if users_by_id.get(approver_id)]) |
| 527 | |
| 528 | return sortable_value_list |
| 529 | |
| 530 | |
| 531 | def _SortableFieldValues(art, fd_list, users_by_id, phase_name): |
| 532 | """Return a list of field values relevant to one UI table column.""" |
| 533 | phase_id = None |
| 534 | if phase_name: |
| 535 | phase_id = next(( |
| 536 | phase.phase_id for phase in art.phases |
| 537 | if phase.name.lower() == phase_name), None) |
| 538 | sortable_value_list = [] |
| 539 | for fd in fd_list: |
| 540 | for fv in art.field_values: |
| 541 | if fv.field_id == fd.field_id and fv.phase_id == phase_id: |
| 542 | sortable_value_list.append( |
| 543 | tracker_bizobj.GetFieldValue(fv, users_by_id)) |
| 544 | |
| 545 | return sortable_value_list |
| 546 | |
| 547 | |
| 548 | def _SortableLabelValues(art, col_name, well_known_value_indexes): |
| 549 | """Return a list of ints and strings for labels relevant to one UI column.""" |
| 550 | col_name_dash = col_name + '-' |
| 551 | sortable_value_list = [] |
| 552 | for label in tracker_bizobj.GetLabels(art): |
| 553 | idx_or_lex = well_known_value_indexes.get(label) |
| 554 | if idx_or_lex == IGNORABLE_INDICATOR: |
| 555 | continue # Label is known to not have the desired prefix. |
| 556 | if idx_or_lex is None: |
| 557 | if '-' not in label: |
| 558 | # Skip an irrelevant OneWord label and remember to ignore it later. |
| 559 | well_known_value_indexes[label] = IGNORABLE_INDICATOR |
| 560 | continue |
| 561 | label_lower = label.lower() |
| 562 | if label_lower.startswith(col_name_dash): |
| 563 | # Label is a key-value label with an odd-ball value, remember it |
| 564 | value = label_lower[len(col_name_dash):] |
| 565 | idx_or_lex = value |
| 566 | well_known_value_indexes[label] = value |
| 567 | else: |
| 568 | # Label was a key-value label that is not relevant to this column. |
| 569 | # Remember to ignore it later. |
| 570 | well_known_value_indexes[label] = IGNORABLE_INDICATOR |
| 571 | continue |
| 572 | |
| 573 | sortable_value_list.append(idx_or_lex) |
| 574 | |
| 575 | return sortable_value_list |