Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | # Copyright 2018 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 | """FLT task to be manually triggered to convert launch issues.""" |
| 7 | from __future__ import print_function |
| 8 | from __future__ import division |
| 9 | from __future__ import absolute_import |
| 10 | |
| 11 | import collections |
| 12 | import logging |
| 13 | import re |
| 14 | import settings |
| 15 | import time |
| 16 | |
| 17 | from businesslogic import work_env |
| 18 | from framework import permissions |
| 19 | from framework import exceptions |
| 20 | from framework import jsonfeed |
| 21 | from proto import tracker_pb2 |
| 22 | from tracker import template_helpers |
| 23 | from tracker import tracker_bizobj |
| 24 | |
| 25 | PM_PREFIX = 'pm-' |
| 26 | TL_PREFIX = 'tl-' |
| 27 | TEST_PREFIX = 'test-' |
| 28 | UX_PREFIX = 'ux-' |
| 29 | |
| 30 | PM_FIELD = 'pm' |
| 31 | TL_FIELD = 'tl' |
| 32 | TE_FIELD = 'te' |
| 33 | UX_FIELD = 'ux' |
| 34 | MTARGET_FIELD = 'm-target' |
| 35 | MAPPROVED_FIELD = 'm-approved' |
| 36 | |
| 37 | CONVERSION_COMMENT = 'Automatic generating of FLT Launch data.' |
| 38 | |
| 39 | BROWSER_APPROVALS_TO_LABELS = { |
| 40 | 'Chrome-Accessibility': 'Launch-Accessibility-', |
| 41 | 'Chrome-Leadership-Exp': 'Launch-Exp-Leadership-', |
| 42 | 'Chrome-Leadership-Full': 'Launch-Leadership-', |
| 43 | 'Chrome-Legal': 'Launch-Legal-', |
| 44 | 'Chrome-Privacy': 'Launch-Privacy-', |
| 45 | 'Chrome-Security': 'Launch-Security-', |
| 46 | 'Chrome-Test': 'Launch-Test-', |
| 47 | 'Chrome-UX': 'Launch-UI-', |
| 48 | } |
| 49 | |
| 50 | OS_APPROVALS_TO_LABELS = { |
| 51 | 'ChromeOS-Accessibility': 'Launch-Accessibility-', |
| 52 | 'ChromeOS-Leadership-Exp': 'Launch-Exp-Leadership-', |
| 53 | 'ChromeOS-Leadership-Full': 'Launch-Leadership-', |
| 54 | 'ChromeOS-Legal': 'Launch-Legal-', |
| 55 | 'ChromeOS-Privacy': 'Launch-Privacy-', |
| 56 | 'ChromeOS-Security': 'Launch-Security-', |
| 57 | 'ChromeOS-Test': 'Launch-Test-', |
| 58 | 'ChromeOS-UX': 'Launch-UI-', |
| 59 | } |
| 60 | |
| 61 | # 'NotReviewed' not included because this should be converted to |
| 62 | # the template approval's default value, eg NOT_SET OR NEEDS_REVIEW |
| 63 | VALUE_TO_STATUS = { |
| 64 | 'ReviewRequested': tracker_pb2.ApprovalStatus.REVIEW_REQUESTED, |
| 65 | 'NeedInfo': tracker_pb2.ApprovalStatus.NEED_INFO, |
| 66 | 'Yes': tracker_pb2.ApprovalStatus.APPROVED, |
| 67 | 'No': tracker_pb2.ApprovalStatus.NOT_APPROVED, |
| 68 | 'NA': tracker_pb2.ApprovalStatus.NA, |
| 69 | # 'Started' is not a valid label value in the chromium project, |
| 70 | # but for some reason, some labels have this value. |
| 71 | 'Started': tracker_pb2.ApprovalStatus.REVIEW_STARTED, |
| 72 | } |
| 73 | |
| 74 | # This works in the Browser and OS process because |
| 75 | # BROWSER_APPROVALS_TO_LABELS and OS_APPROVALS_TO_LABELS have the same values. |
| 76 | # Adding '^' before each label prefix to ensure Blah-Launch-UI-Yes is ignored |
| 77 | REVIEW_LABELS_RE = re.compile('^' + '|^'.join( |
| 78 | list(OS_APPROVALS_TO_LABELS.values()))) |
| 79 | |
| 80 | # Maps template phases to channel names in 'Launch-M-Target-80-[Channel]' labels |
| 81 | BROWSER_PHASE_MAP = { |
| 82 | 'beta': 'beta', |
| 83 | 'stable': 'stable', |
| 84 | 'stable-full': 'stable', |
| 85 | 'stable-exp': 'stable-exp', |
| 86 | } |
| 87 | |
| 88 | PHASE_PAT = '$|'.join(list(BROWSER_PHASE_MAP.values())) |
| 89 | # Matches launch milestone labels, eg. Launch-M-Target-70-Stable-Exp |
| 90 | BROWSER_M_LABELS_RE = re.compile( |
| 91 | r'^Launch-M-(?P<type>Approved|Target)-(?P<m>\d\d)-' |
| 92 | r'(?P<channel>%s$)' % PHASE_PAT, |
| 93 | re.IGNORECASE) |
| 94 | |
| 95 | OS_PHASE_MAP = {'feature freeze': '', |
| 96 | 'branch': '', |
| 97 | 'stable': 'stable', |
| 98 | 'stable-full': 'stable', |
| 99 | 'stable-exp': 'stable-exp',} |
| 100 | # We only care about Launch-M-<type>-<m>-Stable|Stable-Exp labels for OS. |
| 101 | OS_M_LABELS_RE = re.compile( |
| 102 | r'^Launch-M-(?P<type>Approved|Target)-(?P<m>\d\d)-' |
| 103 | r'(?P<channel>Stable$|Stable-Exp$)', re.IGNORECASE) |
| 104 | |
| 105 | CAN = 2 # Query for open issues only |
| 106 | # Ensure empty group_by_spec and sort_spec so issues are sorted by 'ID'. |
| 107 | GROUP_BY_SPEC = '' |
| 108 | SORT_SPEC = '' |
| 109 | |
| 110 | CONVERT_NUM = 20 |
| 111 | CONVERT_START = 0 |
| 112 | VERIFY_NUM = 400 |
| 113 | |
| 114 | # Queries |
| 115 | QUERY_MAP = { |
| 116 | 'default': |
| 117 | 'Type=Launch Rollout-Type=Default OS=Windows,Mac,Linux,Android,iOS', |
| 118 | 'finch': 'Type=Launch Rollout-Type=Finch OS=Windows,Mac,Linux,Android,iOS', |
| 119 | 'os': 'Type=Launch OS=Chrome -OS=Windows,Mac,Linux,Android,iOS' |
| 120 | ' Rollout-Type=Default', |
| 121 | 'os-finch': 'Type=Launch OS=Chrome -OS=Windows,Mac,Linux,Android,iOS' |
| 122 | ' Rollout-Type=Finch'} |
| 123 | |
| 124 | TEMPLATE_MAP = { |
| 125 | 'default': 'Chrome Launch - Default', |
| 126 | 'finch': 'Chrome Launch - Experimental', |
| 127 | 'os': 'Chrome OS Launch - Default', |
| 128 | 'os-finch': 'Chrome OS Launch - Experimental', |
| 129 | } |
| 130 | |
| 131 | ProjectInfo = collections.namedtuple( |
| 132 | 'ProjectInfo', 'config, q, approval_values, phases, ' |
| 133 | 'pm_fid, tl_fid, te_fid, ux_fid, m_target_id, m_approved_id, ' |
| 134 | 'phase_map, approvals_to_labels, labels_re') |
| 135 | |
| 136 | |
Adrià Vilanova Martínez | 9f9ade5 | 2022-10-10 23:20:11 +0200 | [diff] [blame^] | 137 | class FLTConvertTask(jsonfeed.FlaskInternalTask): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 138 | """FLTConvert converts current Type=Launch issues into Type=FLT-Launch.""" |
| 139 | |
| 140 | def AssertBasePermission(self, mr): |
| 141 | super(FLTConvertTask, self).AssertBasePermission(mr) |
| 142 | if not mr.auth.user_pb.is_site_admin: |
| 143 | raise permissions.PermissionException( |
| 144 | 'Only site admins may trigger conversion job') |
| 145 | |
| 146 | def UndoConversion(self, mr): |
| 147 | with work_env.WorkEnv(mr, self.services) as we: |
| 148 | pipeline = we.ListIssues( |
| 149 | 'Type=FLT-Launch FLT=Conversion', ['chromium'], mr.auth.user_id, |
| 150 | CONVERT_NUM, CONVERT_START, 2, GROUP_BY_SPEC, SORT_SPEC, False) |
| 151 | |
| 152 | project = self.services.project.GetProjectByName(mr.cnxn, 'chromium') |
| 153 | config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id) |
| 154 | pm_id = tracker_bizobj.FindFieldDef('PM', config).field_id |
| 155 | tl_id = tracker_bizobj.FindFieldDef('TL', config).field_id |
| 156 | te_id = tracker_bizobj.FindFieldDef('TE', config).field_id |
| 157 | ux_id = tracker_bizobj.FindFieldDef('UX', config).field_id |
| 158 | for possible_stale_issue in pipeline.visible_results: |
| 159 | issue = self.services.issue.GetIssue( |
| 160 | mr.cnxn, possible_stale_issue.issue_id, use_cache=False) |
| 161 | |
| 162 | issue.approval_values = [] |
| 163 | issue.phases = [] |
| 164 | issue.field_values = [fv for fv in issue.field_values |
| 165 | if fv.phase_id is None] |
| 166 | issue.field_values = [fv for fv in issue.field_values |
| 167 | if fv.field_id not in |
| 168 | [pm_id, tl_id, te_id, ux_id]] |
| 169 | issue.labels.remove('Type-FLT-Launch') |
| 170 | issue.labels.remove('FLT-Conversion') |
| 171 | issue.labels.append('Type-Launch') |
| 172 | |
| 173 | self.services.issue._UpdateIssuesApprovals(mr.cnxn, issue) |
| 174 | self.services.issue.UpdateIssue(mr.cnxn, issue) |
| 175 | return {'deleting': [issue.local_id for issue in pipeline.visible_results], |
| 176 | 'num': len(pipeline.visible_results), |
| 177 | } |
| 178 | |
| 179 | def VerifyConversion(self, mr): |
| 180 | """Verify that all FLT-Conversion issues were converted correctly.""" |
| 181 | with work_env.WorkEnv(mr, self.services) as we: |
| 182 | pipeline = we.ListIssues( |
| 183 | 'FLT=Conversion', ['chromium'], mr.auth.user_id, VERIFY_NUM, |
| 184 | CONVERT_START, 2, GROUP_BY_SPEC, SORT_SPEC, False) |
| 185 | |
| 186 | project = self.services.project.GetProjectByName(mr.cnxn, 'chromium') |
| 187 | config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id) |
| 188 | browser_approval_names = {fd.field_id: fd.field_name for fd |
| 189 | in config.field_defs if fd.field_name in |
| 190 | BROWSER_APPROVALS_TO_LABELS.keys()} |
| 191 | os_approval_names = {fd.field_id: fd.field_name for fd in config.field_defs |
| 192 | if (fd.field_name in OS_APPROVALS_TO_LABELS.keys()) |
| 193 | or fd.field_name == 'ChromeOS-Enterprise'} |
| 194 | pm_id = tracker_bizobj.FindFieldDef('PM', config).field_id |
| 195 | tl_id = tracker_bizobj.FindFieldDef('TL', config).field_id |
| 196 | te_id = tracker_bizobj.FindFieldDef('TE', config).field_id |
| 197 | ux_id = tracker_bizobj.FindFieldDef('UX', config).field_id |
| 198 | mapproved_id = tracker_bizobj.FindFieldDef('M-Approved', config).field_id |
| 199 | mtarget_id = tracker_bizobj.FindFieldDef('M-Target', config).field_id |
| 200 | |
| 201 | problems = [] |
| 202 | for possible_stale_issue in pipeline.allowed_results: |
| 203 | issue = self.services.issue.GetIssue( |
| 204 | mr.cnxn, possible_stale_issue.issue_id, use_cache=False) |
| 205 | # Check correct template used |
| 206 | approval_names = browser_approval_names |
| 207 | approvals_to_labels = BROWSER_APPROVALS_TO_LABELS |
| 208 | m_labels_re = BROWSER_M_LABELS_RE |
| 209 | label_channel_to_phase_id = { |
| 210 | phase.name.lower(): phase.phase_id for phase in issue.phases} |
| 211 | if [l for l in issue.labels if l.startswith('OS-')] == ['OS-Chrome']: |
| 212 | approval_names = os_approval_names |
| 213 | m_labels_re = OS_M_LABELS_RE |
| 214 | approvals_to_labels = OS_APPROVALS_TO_LABELS |
| 215 | # OS default launch |
| 216 | if 'Rollout-Type-Default' in issue.labels: |
| 217 | if not all(phase.name in ['Feature Freeze', 'Branch', 'Stable'] |
| 218 | for phase in issue.phases): |
| 219 | problems.append(( |
| 220 | issue.local_id, 'incorrect phases for OS default launch.')) |
| 221 | # OS finch launch |
| 222 | elif 'Rollout-Type-Finch' in issue.labels: |
| 223 | if not all(phase.name in ( |
| 224 | 'Feature Freeze', 'Branch', 'Stable-Exp', 'Stable-Full') |
| 225 | for phase in issue.phases): |
| 226 | problems.append(( |
| 227 | issue.local_id, 'incorrect phases for OS finch launch.')) |
| 228 | else: |
| 229 | problems.append(( |
| 230 | issue.local_id, |
| 231 | 'no rollout-type; should not have been converted')) |
| 232 | # Browser default launch |
| 233 | elif 'Rollout-Type-Default' in issue.labels: |
| 234 | if not all(phase.name.lower() in ['beta', 'stable'] |
| 235 | for phase in issue.phases): |
| 236 | problems.append(( |
| 237 | issue.local_id, 'incorrect phases for Default rollout')) |
| 238 | # Browser finch launch |
| 239 | elif 'Rollout-Type-Finch' in issue.labels: |
| 240 | if not all(phase.name.lower() in ['beta', 'stable-exp', 'stable-full'] |
| 241 | for phase in issue.phases): |
| 242 | problems.append(( |
| 243 | issue.local_id, 'incorrect phases for Finch rollout')) |
| 244 | else: |
| 245 | problems.append(( |
| 246 | issue.local_id, |
| 247 | 'no rollout-type; should not have been converted')) |
| 248 | |
| 249 | # Check approval_values |
| 250 | for av in issue.approval_values: |
| 251 | name = approval_names.get(av.approval_id) |
| 252 | if name == 'ChromeOS-Enterprise': |
| 253 | if av.status != tracker_pb2.ApprovalStatus.NEEDS_REVIEW: |
| 254 | problems.append((issue.local_id, 'bad ChromeOS-Enterprise status')) |
| 255 | continue |
| 256 | label_pre = approvals_to_labels.get(name) |
| 257 | if not label_pre: |
| 258 | # either name was None or not found in APPROVALS_TO_LABELS |
| 259 | problems.append((issue.local_id, 'approval %s not recognized' % name)) |
| 260 | continue |
| 261 | label_value = next((l[len(label_pre):] for l in issue.labels |
| 262 | if l.startswith(label_pre)), None) |
| 263 | if (not label_value or label_value == 'NotReviewed') and av.status in [ |
| 264 | tracker_pb2.ApprovalStatus.NOT_SET, |
| 265 | tracker_pb2.ApprovalStatus.NEEDS_REVIEW]: |
| 266 | continue |
| 267 | if av.status is VALUE_TO_STATUS.get(label_value): |
| 268 | continue |
| 269 | # neither of the above ifs passed |
| 270 | problems.append((issue.local_id, |
| 271 | 'approval %s has status %r for label value %s' % ( |
| 272 | name, av.status.name, label_value))) |
| 273 | |
| 274 | # Check people field_values |
| 275 | expected_people_fvs = self.ConvertPeopleLabels( |
| 276 | mr, issue.labels, pm_id, tl_id, te_id, ux_id) |
| 277 | for people_fv in expected_people_fvs: |
| 278 | if people_fv not in issue.field_values: |
| 279 | if people_fv.field_id == tl_id: |
| 280 | role = 'TL' |
| 281 | elif people_fv.field_id == pm_id: |
| 282 | role = 'PM' |
| 283 | elif people_fv.field_id == ux_id: |
| 284 | role = 'UX' |
| 285 | else: |
| 286 | role = 'TE' |
| 287 | problems.append((issue.local_id, 'missing a field for %s' % role)) |
| 288 | |
| 289 | # Check M phase field_values |
| 290 | for label in issue.labels: |
| 291 | match = re.match(m_labels_re, label) |
| 292 | if match: |
| 293 | channel = match.group('channel') |
| 294 | if (channel.lower() == 'stable-exp' |
| 295 | and 'Rollout-Type-Default' in issue.labels): |
| 296 | # ignore stable-exp for default rollouts. |
| 297 | continue |
| 298 | milestone = match.group('m') |
| 299 | m_type = match.group('type') |
| 300 | m_id = mapproved_id if m_type == 'Approved' else mtarget_id |
| 301 | phase_id = label_channel_to_phase_id.get( |
| 302 | channel.lower(), label_channel_to_phase_id.get('stable-full')) |
| 303 | if not next(( |
| 304 | fv for fv in issue.field_values |
| 305 | if fv.phase_id == phase_id and fv.field_id == m_id and |
| 306 | fv.int_value == int(milestone)), None): |
| 307 | problems.append(( |
| 308 | issue.local_id, 'no phase field for label %s' % label)) |
| 309 | |
| 310 | return { |
| 311 | 'problems found': ['issue %d: %s' % problem for problem in problems], |
| 312 | 'issues verified': ['issue %d' % issue.local_id for |
| 313 | issue in pipeline.allowed_results], |
| 314 | 'num': len(pipeline.allowed_results), |
| 315 | } |
| 316 | |
| 317 | def HandleRequest(self, mr): |
| 318 | """Convert Type=Launch issues to new Type=FLT-Launch issues.""" |
| 319 | launch = mr.GetParam('launch') |
| 320 | if launch == 'delete': |
| 321 | return self.UndoConversion(mr) |
| 322 | if launch == 'verify': |
| 323 | return self.VerifyConversion(mr) |
| 324 | project_info = self.FetchAndAssertProjectInfo(mr) |
| 325 | |
| 326 | # Search for issues: |
| 327 | with work_env.WorkEnv(mr, self.services) as we: |
| 328 | pipeline = we.ListIssues( |
| 329 | project_info.q, ['chromium'], mr.auth.user_id, CONVERT_NUM, |
| 330 | CONVERT_START, 2, GROUP_BY_SPEC, SORT_SPEC, False) |
| 331 | |
| 332 | # Convert issues: |
| 333 | for possible_stale_issue in pipeline.visible_results: |
| 334 | # Note: These approval values and phases from templates will be used |
| 335 | # and modified to create approval values and phases for each issue. |
| 336 | # We need to create copies for each issue so changes are not carried |
| 337 | # over to the conversion of the next issue in the loop. |
| 338 | template_avs = self.CreateApprovalCopies(project_info.approval_values) |
| 339 | template_phases = self.CreatePhasesCopies(project_info.phases) |
| 340 | issue = self.services.issue.GetIssue( |
| 341 | mr.cnxn, possible_stale_issue.issue_id, use_cache=False) |
| 342 | new_approvals = ConvertLaunchLabels( |
| 343 | issue.labels, template_avs, |
| 344 | project_info.config.field_defs, project_info.approvals_to_labels) |
| 345 | m_fvs = ConvertMLabels( |
| 346 | issue.labels, template_phases, |
| 347 | project_info.m_target_id, project_info.m_approved_id, |
| 348 | project_info.labels_re, project_info.phase_map) |
| 349 | people_fvs = self.ConvertPeopleLabels( |
| 350 | mr, issue.labels, |
| 351 | project_info.pm_fid, project_info.tl_fid, project_info.te_fid, |
| 352 | project_info.ux_fid) |
| 353 | amendments = self.ExecuteIssueChanges( |
| 354 | project_info.config, issue, new_approvals, |
| 355 | template_phases, m_fvs + people_fvs) |
| 356 | logging.info(amendments) |
| 357 | |
| 358 | return { |
| 359 | 'converted_issues': [ |
| 360 | issue.local_id for issue in pipeline.visible_results], |
| 361 | 'num': len(pipeline.visible_results), |
| 362 | } |
| 363 | |
| 364 | def CreateApprovalCopies(self, avs): |
| 365 | return [ |
| 366 | tracker_pb2.ApprovalValue( |
| 367 | approval_id=av.approval_id, |
| 368 | status=av.status, |
| 369 | setter_id=av.setter_id, |
| 370 | set_on=av.set_on, |
| 371 | phase_id=av.phase_id) for av in avs |
| 372 | ] |
| 373 | |
| 374 | def CreatePhasesCopies(self, phases): |
| 375 | return [ |
| 376 | tracker_pb2.Phase( |
| 377 | phase_id=phase.phase_id, |
| 378 | name=phase.name, |
| 379 | rank=phase.rank) for phase in phases |
| 380 | ] |
| 381 | |
| 382 | def FetchAndAssertProjectInfo(self, mr): |
| 383 | # Get request details |
| 384 | launch = mr.GetParam('launch') |
| 385 | logging.info(launch) |
| 386 | q = QUERY_MAP.get(launch) |
| 387 | template_name = TEMPLATE_MAP.get(launch) |
| 388 | assert q and template_name, 'bad launch type: %s' % launch |
| 389 | |
| 390 | phase_map = ( |
| 391 | OS_PHASE_MAP if launch in ['os', 'os-finch'] else BROWSER_PHASE_MAP) |
| 392 | approvals_to_labels = ( |
| 393 | OS_APPROVALS_TO_LABELS if launch in ['os', 'os-finch'] |
| 394 | else BROWSER_APPROVALS_TO_LABELS) |
| 395 | m_labels_re = ( |
| 396 | OS_M_LABELS_RE if launch in ['os', 'os-finch'] else BROWSER_M_LABELS_RE) |
| 397 | |
| 398 | # Get project, config, template, assert template in project |
| 399 | project = self.services.project.GetProjectByName(mr.cnxn, 'chromium') |
| 400 | config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id) |
| 401 | template = self.services.template.GetTemplateByName( |
| 402 | mr.cnxn, template_name, project.project_id) |
| 403 | assert template, 'template %s not found in chromium project' % template_name |
| 404 | |
| 405 | # Get template approval_values/phases and assert they are expected |
| 406 | approval_values, phases = template_helpers.FilterApprovalsAndPhases( |
| 407 | template.approval_values, template.phases, config) |
| 408 | assert approval_values and phases, ( |
| 409 | 'no approvals or phases in %s' % template_name) |
| 410 | assert all(phase.name.lower() in list( |
| 411 | phase_map.keys()) for phase in phases), ( |
| 412 | 'one or more phases not recognized') |
| 413 | if launch in ['finch', 'os', 'os-finch']: |
| 414 | assert all( |
| 415 | av.status is tracker_pb2.ApprovalStatus.NEEDS_REVIEW |
| 416 | for av in approval_values |
| 417 | ), '%s template not set up correctly' % launch |
| 418 | |
| 419 | approval_fds = {fd.field_id: fd.field_name for fd in config.field_defs |
| 420 | if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE} |
| 421 | assert all( |
| 422 | approval_fds.get(av.approval_id) in list(approvals_to_labels.keys()) |
| 423 | for av in approval_values |
| 424 | if approval_fds.get(av.approval_id) != 'ChromeOS-Enterprise'), ( |
| 425 | 'one or more approvals not recognized') |
| 426 | approval_def_ids = [ad.approval_id for ad in config.approval_defs] |
| 427 | assert all(av.approval_id in approval_def_ids for av in approval_values), ( |
| 428 | 'one or more approvals not in config.approval_defs') |
| 429 | |
| 430 | # Get relevant USER_TYPE FieldDef ids and assert they exist |
| 431 | user_fds = {fd.field_name.lower(): fd.field_id for fd in config.field_defs |
| 432 | if fd.field_type is tracker_pb2.FieldTypes.USER_TYPE} |
| 433 | logging.info('project USER_TYPE FieldDefs: %s' % user_fds) |
| 434 | pm_fid = user_fds.get(PM_FIELD) |
| 435 | assert pm_fid, 'project has no FieldDef %s' % PM_FIELD |
| 436 | tl_fid = user_fds.get(TL_FIELD) |
| 437 | assert tl_fid, 'project has no FieldDef %s' % TL_FIELD |
| 438 | te_fid = user_fds.get(TE_FIELD) |
| 439 | assert te_fid, 'project has no FieldDef %s' % TE_FIELD |
| 440 | ux_fid = user_fds.get(UX_FIELD) |
| 441 | assert ux_fid, 'project has no FieldDef %s' % UX_FIELD |
| 442 | |
| 443 | # Get relevant M Phase INT_TYPE FieldDef ids and assert they exist |
| 444 | phase_int_fds = {fd.field_name.lower(): fd.field_id |
| 445 | for fd in config.field_defs |
| 446 | if fd.field_type is tracker_pb2.FieldTypes.INT_TYPE |
| 447 | and fd.is_phase_field and fd.is_multivalued} |
| 448 | logging.info( |
| 449 | 'project Phase INT_TYPE multivalued FieldDefs: %s' % phase_int_fds) |
| 450 | m_target_id = phase_int_fds.get(MTARGET_FIELD) |
| 451 | assert m_target_id, 'project has no FieldDef %s' % MTARGET_FIELD |
| 452 | m_approved_id = phase_int_fds.get(MAPPROVED_FIELD) |
| 453 | assert m_approved_id, 'project has no FieldDef %s' % MAPPROVED_FIELD |
| 454 | |
| 455 | return ProjectInfo(config, q, approval_values, phases, pm_fid, tl_fid, |
| 456 | te_fid, ux_fid, m_target_id, m_approved_id, phase_map, |
| 457 | approvals_to_labels, m_labels_re) |
| 458 | |
| 459 | # TODO(jojwang): mr needs to be passed in as arg and |
| 460 | # all self.mr should be changed to mr |
| 461 | def ExecuteIssueChanges(self, config, issue, new_approvals, phases, new_fvs): |
| 462 | # Apply Approval and phase changes |
| 463 | approval_defs_by_id = {ad.approval_id: ad for ad in config.approval_defs} |
| 464 | for av in new_approvals: |
| 465 | ad = approval_defs_by_id.get(av.approval_id) |
| 466 | if ad: |
| 467 | av.approver_ids = ad.approver_ids |
| 468 | survey = '' |
| 469 | if ad.survey: |
| 470 | questions = ad.survey.split('\n') |
| 471 | survey = '\n'.join(['<b>' + q + '</b>' for q in questions]) |
| 472 | self.services.issue.InsertComment( |
| 473 | self.mr.cnxn, tracker_pb2.IssueComment( |
| 474 | issue_id=issue.issue_id, project_id=issue.project_id, |
| 475 | user_id=self.mr.auth.user_id, content=survey, |
| 476 | is_description=True, approval_id=av.approval_id, |
| 477 | timestamp=int(time.time()))) |
| 478 | else: |
| 479 | logging.info( |
| 480 | 'ERROR: ApprovalDef %r for ApprovalValue %r not valid', ad, av) |
| 481 | issue.approval_values = new_approvals |
| 482 | self.services.issue._UpdateIssuesApprovals(self.mr.cnxn, issue) |
| 483 | |
| 484 | # Apply field value changes |
| 485 | issue.phases = phases |
| 486 | delta = tracker_bizobj.MakeIssueDelta( |
| 487 | None, None, [], [], [], [], ['Type-FLT-Launch', 'FLT-Conversion'], |
| 488 | ['Type-Launch'], new_fvs, [], [], [], [], [], [], None, None) |
| 489 | amendments, _ = self.services.issue.DeltaUpdateIssue( |
| 490 | self.mr.cnxn, self.services, self.mr.auth.user_id, issue.project_id, |
| 491 | config, issue, delta, comment=CONVERSION_COMMENT) |
| 492 | |
| 493 | return amendments |
| 494 | |
| 495 | def ConvertPeopleLabels( |
| 496 | self, mr, labels, pm_field_id, tl_field_id, te_field_id, ux_field_id): |
| 497 | field_values = [] |
| 498 | pm_ldap, tl_ldap, test_ldaps, ux_ldaps = ExtractLabelLDAPs(labels) |
| 499 | |
| 500 | pm_fv = self.CreateUserFieldValue(mr, pm_ldap, pm_field_id) |
| 501 | if pm_fv: |
| 502 | field_values.append(pm_fv) |
| 503 | |
| 504 | tl_fv = self.CreateUserFieldValue(mr, tl_ldap, tl_field_id) |
| 505 | if tl_fv: |
| 506 | field_values.append(tl_fv) |
| 507 | |
| 508 | for test_ldap in test_ldaps: |
| 509 | te_fv = self.CreateUserFieldValue(mr, test_ldap, te_field_id) |
| 510 | if te_fv: |
| 511 | field_values.append(te_fv) |
| 512 | |
| 513 | for ux_ldap in ux_ldaps: |
| 514 | ux_fv = self.CreateUserFieldValue(mr, ux_ldap, ux_field_id) |
| 515 | if ux_fv: |
| 516 | field_values.append(ux_fv) |
| 517 | return field_values |
| 518 | |
| 519 | def CreateUserFieldValue(self, mr, ldap, field_id): |
| 520 | if ldap is None: |
| 521 | return None |
| 522 | try: |
| 523 | user_id = self.services.user.LookupUserID(mr.cnxn, ldap+'@chromium.org') |
| 524 | except exceptions.NoSuchUserException: |
| 525 | try: |
| 526 | user_id = self.services.user.LookupUserID(mr.cnxn, ldap+'@google.com') |
| 527 | except exceptions.NoSuchUserException: |
| 528 | logging.info('No chromium.org or google.com accound found for %s', ldap) |
| 529 | return None |
| 530 | return tracker_bizobj.MakeFieldValue( |
| 531 | field_id, None, None, user_id, None, None, False) |
| 532 | |
Adrià Vilanova Martínez | 9f9ade5 | 2022-10-10 23:20:11 +0200 | [diff] [blame^] | 533 | def PostFLTConvertTask(self, **kwargs): |
| 534 | return self.handler(**kwargs) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 535 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 536 | |
| 537 | def ConvertMLabels( |
| 538 | labels, phases, m_target_id, m_approved_id, labels_re, phase_map): |
| 539 | field_values = [] |
| 540 | for label in labels: |
| 541 | match = re.match(labels_re, label) |
| 542 | if match: |
| 543 | milestone = match.group('m') |
| 544 | m_type = match.group('type') |
| 545 | channel = match.group('channel') |
| 546 | for phase in phases: |
| 547 | # We know get(phase) will return something because |
| 548 | # we're checking before ConvertMLabels, that all phases |
| 549 | # exist in BROWSER_PHASE_MAP or OS_PHASE_MAP |
| 550 | if phase_map.get(phase.name.lower()) == channel.lower(): |
| 551 | field_id = m_target_id if ( |
| 552 | m_type.lower() == 'target') else m_approved_id |
| 553 | field_values.append(tracker_bizobj.MakeFieldValue( |
| 554 | field_id, int(milestone), None, None, None, None, False, |
| 555 | phase_id=phase.phase_id)) |
| 556 | break # exit phase loop if match is found. |
| 557 | return field_values |
| 558 | |
| 559 | |
| 560 | def ConvertLaunchLabels(labels, approvals, project_fds, approvals_to_labels): |
| 561 | """Converts 'Launch-[Review]' values into statuses for given approvals.""" |
| 562 | label_values = {} |
| 563 | for label in labels: |
| 564 | launch_match = REVIEW_LABELS_RE.match(label) |
| 565 | if launch_match: |
| 566 | prefix = launch_match.group() |
| 567 | value = label[len(prefix):] # returns 'Yes' from 'Launch-UI-Yes' |
| 568 | label_values[prefix] = value |
| 569 | |
| 570 | field_names_dict = {fd.field_id: fd.field_name for fd in project_fds} |
| 571 | for approval in approvals: |
| 572 | approval_name = field_names_dict.get(approval.approval_id, '') |
| 573 | old_prefix = approvals_to_labels.get(approval_name) |
| 574 | label_value = label_values.get(old_prefix, '') |
| 575 | # if label_value not found in VALUE_TO_STATUS, use current status. |
| 576 | approval.status = VALUE_TO_STATUS.get(label_value, approval.status) |
| 577 | |
| 578 | return approvals |
| 579 | |
| 580 | |
| 581 | def ExtractLabelLDAPs(labels): |
| 582 | """Extracts LDAPs from labels 'PM-', 'TL-', 'UX-', and 'test-'""" |
| 583 | |
| 584 | pm_ldap = None |
| 585 | tl_ldap = None |
| 586 | test_ldaps = [] |
| 587 | ux_ldaps = [] |
| 588 | for label in labels: |
| 589 | label = label.lower() |
| 590 | if label.startswith(PM_PREFIX): |
| 591 | pm_ldap = label[len(PM_PREFIX):] |
| 592 | elif label.startswith(TL_PREFIX): |
| 593 | tl_ldap = label[len(TL_PREFIX):] |
| 594 | elif label.startswith(TEST_PREFIX): |
| 595 | ldap = label[len(TEST_PREFIX):] |
| 596 | if ldap: |
| 597 | test_ldaps.append(ldap) |
| 598 | elif label.startswith(UX_PREFIX): |
| 599 | ldap = label[len(UX_PREFIX):] |
| 600 | if ldap: |
| 601 | ux_ldaps.append(ldap) |
| 602 | return pm_ldap, tl_ldap, test_ldaps, ux_ldaps |