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