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 custom field sevlets.""" |
| 6 | from __future__ import print_function |
| 7 | from __future__ import division |
| 8 | from __future__ import absolute_import |
| 9 | |
| 10 | import collections |
| 11 | import itertools |
| 12 | import logging |
| 13 | import re |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 14 | import settings |
| 15 | |
| 16 | from google.appengine.api import app_identity |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 17 | |
| 18 | from features import autolink_constants |
| 19 | from framework import authdata |
| 20 | from framework import exceptions |
| 21 | from framework import framework_bizobj |
| 22 | from framework import framework_constants |
| 23 | from framework import permissions |
| 24 | from framework import timestr |
| 25 | from framework import validate |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 26 | from mrproto import tracker_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 27 | from services import config_svc |
| 28 | from tracker import tracker_bizobj |
| 29 | |
| 30 | |
| 31 | INVALID_USER_ID = -1 |
| 32 | |
| 33 | ParsedFieldDef = collections.namedtuple( |
| 34 | 'ParsedFieldDef', |
| 35 | 'field_name, field_type_str, min_value, max_value, regex, ' |
| 36 | 'needs_member, needs_perm, grants_perm, notify_on, is_required, ' |
| 37 | 'is_niche, importance, is_multivalued, field_docstring, choices_text, ' |
| 38 | 'applicable_type, applicable_predicate, revised_labels, date_action_str, ' |
| 39 | 'approvers_str, survey, parent_approval_name, is_phase_field, ' |
| 40 | 'is_restricted_field') |
| 41 | |
| 42 | |
| 43 | def ListApplicableFieldDefs(issues, config): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 44 | # type: (Sequence[mrproto.tracker_pb2.Issue], |
| 45 | # mrproto.tracker_pb2.ProjectIssueConfig) -> |
| 46 | # Sequence[mrproto.tracker_pb2.FieldDef] |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 47 | """Return the applicable FieldDefs for the given issues. """ |
| 48 | issue_labels = [] |
| 49 | issue_approval_ids = [] |
| 50 | for issue in issues: |
| 51 | issue_labels.extend(issue.labels) |
| 52 | issue_approval_ids.extend( |
| 53 | [approval.approval_id for approval in issue.approval_values]) |
| 54 | labels_by_prefix = tracker_bizobj.LabelsByPrefix(list(set(issue_labels)), []) |
| 55 | types = set(labels_by_prefix.get('type', [])) |
| 56 | types_lower = [t.lower() for t in types] |
| 57 | applicable_fds = [] |
| 58 | for fd in config.field_defs: |
| 59 | if fd.is_deleted: |
| 60 | continue |
| 61 | if fd.field_id in issue_approval_ids: |
| 62 | applicable_fds.append(fd) |
| 63 | elif fd.field_type != tracker_pb2.FieldTypes.APPROVAL_TYPE and ( |
| 64 | not fd.applicable_type or fd.applicable_type.lower() in types_lower): |
| 65 | applicable_fds.append(fd) |
| 66 | return applicable_fds |
| 67 | |
| 68 | |
| 69 | def ParseFieldDefRequest(post_data, config): |
| 70 | """Parse the user's HTML form data to update a field definition.""" |
| 71 | field_name = post_data.get('name', '') |
| 72 | field_type_str = post_data.get('field_type') |
| 73 | # TODO(jrobbins): once a min or max is set, it cannot be completely removed. |
| 74 | min_value_str = post_data.get('min_value') |
| 75 | try: |
| 76 | min_value = int(min_value_str) |
| 77 | except (ValueError, TypeError): |
| 78 | min_value = None |
| 79 | max_value_str = post_data.get('max_value') |
| 80 | try: |
| 81 | max_value = int(max_value_str) |
| 82 | except (ValueError, TypeError): |
| 83 | max_value = None |
| 84 | regex = post_data.get('regex') |
| 85 | needs_member = 'needs_member' in post_data |
| 86 | needs_perm = post_data.get('needs_perm', '').strip() |
| 87 | grants_perm = post_data.get('grants_perm', '').strip() |
| 88 | notify_on_str = post_data.get('notify_on') |
| 89 | if notify_on_str in config_svc.NOTIFY_ON_ENUM: |
| 90 | notify_on = config_svc.NOTIFY_ON_ENUM.index(notify_on_str) |
| 91 | else: |
| 92 | notify_on = 0 |
| 93 | importance = post_data.get('importance') |
| 94 | is_required = (importance == 'required') |
| 95 | is_niche = (importance == 'niche') |
| 96 | is_multivalued = 'is_multivalued' in post_data |
| 97 | field_docstring = post_data.get('docstring', '') |
| 98 | choices_text = post_data.get('choices', '') |
| 99 | applicable_type = post_data.get('applicable_type', '') |
| 100 | applicable_predicate = '' # TODO(jrobbins): placeholder for future feature |
| 101 | revised_labels = _ParseChoicesIntoWellKnownLabels( |
| 102 | choices_text, field_name, config, field_type_str) |
| 103 | date_action_str = post_data.get('date_action') |
| 104 | approvers_str = post_data.get('approver_names', '').strip().rstrip(',') |
| 105 | survey = post_data.get('survey', '') |
| 106 | parent_approval_name = post_data.get('parent_approval_name', '') |
| 107 | # TODO(jojwang): monorail:3774, remove enum_type condition when |
| 108 | # phases can have labels. |
| 109 | is_phase_field = ('is_phase_field' in post_data) and ( |
| 110 | field_type_str not in ['approval_type', 'enum_type']) |
| 111 | is_restricted_field = 'is_restricted_field' in post_data |
| 112 | |
| 113 | return ParsedFieldDef( |
| 114 | field_name, field_type_str, min_value, max_value, regex, needs_member, |
| 115 | needs_perm, grants_perm, notify_on, is_required, is_niche, importance, |
| 116 | is_multivalued, field_docstring, choices_text, applicable_type, |
| 117 | applicable_predicate, revised_labels, date_action_str, approvers_str, |
| 118 | survey, parent_approval_name, is_phase_field, is_restricted_field) |
| 119 | |
| 120 | |
| 121 | def _ParseChoicesIntoWellKnownLabels( |
| 122 | choices_text, field_name, config, field_type_str): |
| 123 | """Parse a field's possible choices and integrate them into the config. |
| 124 | |
| 125 | Args: |
| 126 | choices_text: string with one label and optional docstring per line. |
| 127 | field_name: string name of the field definition being edited. |
| 128 | config: ProjectIssueConfig PB of the current project. |
| 129 | field_type_str: string name of the new field's type. None if an existing |
| 130 | field is being updated |
| 131 | |
| 132 | Returns: |
| 133 | A revised list of labels that can be used to update the config. |
| 134 | """ |
| 135 | fd = tracker_bizobj.FindFieldDef(field_name, config) |
| 136 | matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(choices_text) |
| 137 | maskingFieldNames = [] |
| 138 | # wkls should only be masked by the field if it is an enum_type. |
| 139 | if (field_type_str == 'enum_type') or ( |
| 140 | fd and fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE): |
| 141 | maskingFieldNames.append(field_name.lower()) |
| 142 | |
| 143 | new_labels = [ |
| 144 | ('%s-%s' % (field_name, label), choice_docstring.strip(), False) |
| 145 | for label, choice_docstring in matches] |
| 146 | kept_labels = [ |
| 147 | (wkl.label, wkl.label_docstring, wkl.deprecated) |
| 148 | for wkl in config.well_known_labels |
| 149 | if not tracker_bizobj.LabelIsMaskedByField( |
| 150 | wkl.label, maskingFieldNames)] |
| 151 | revised_labels = kept_labels + new_labels |
| 152 | return revised_labels |
| 153 | |
| 154 | |
| 155 | def ShiftEnumFieldsIntoLabels( |
| 156 | labels, labels_remove, field_val_strs, field_val_strs_remove, config): |
| 157 | """Look at the custom field values and treat enum fields as labels. |
| 158 | |
| 159 | Args: |
| 160 | labels: list of labels to add/set on the issue. |
| 161 | labels_remove: list of labels to remove from the issue. |
| 162 | field_val_strs: {field_id: [val_str, ...]} of custom fields to add/set. |
| 163 | field_val_strs_remove: {field_id: [val_str, ...]} of custom fields to |
| 164 | remove. |
| 165 | config: ProjectIssueConfig PB including custom field definitions. |
| 166 | |
| 167 | SIDE-EFFECT: the labels and labels_remove lists will be extended with |
| 168 | key-value labels corresponding to the enum field values. Those field |
| 169 | entries will be removed from field_val_strs and field_val_strs_remove. |
| 170 | """ |
| 171 | for fd in config.field_defs: |
| 172 | if fd.field_type != tracker_pb2.FieldTypes.ENUM_TYPE: |
| 173 | continue |
| 174 | |
| 175 | if fd.field_id in field_val_strs: |
| 176 | labels.extend( |
| 177 | '%s-%s' % (fd.field_name, val) |
| 178 | for val in field_val_strs[fd.field_id] |
| 179 | if val and val != '--') |
| 180 | del field_val_strs[fd.field_id] |
| 181 | |
| 182 | if fd.field_id in field_val_strs_remove: |
| 183 | labels_remove.extend( |
| 184 | '%s-%s' % (fd.field_name, val) |
| 185 | for val in field_val_strs_remove[fd.field_id] |
| 186 | if val and val != '--') |
| 187 | del field_val_strs_remove[fd.field_id] |
| 188 | |
| 189 | |
| 190 | def ReviseApprovals(approval_id, approver_ids, survey, config): |
| 191 | revised_approvals = [( |
| 192 | approval.approval_id, approval.approver_ids, approval.survey) for |
| 193 | approval in config.approval_defs if |
| 194 | approval.approval_id != approval_id] |
| 195 | revised_approvals.append((approval_id, approver_ids, survey)) |
| 196 | return revised_approvals |
| 197 | |
| 198 | |
| 199 | def ParseOneFieldValue(cnxn, user_service, fd, val_str): |
| 200 | """Make one FieldValue PB from the given user-supplied string.""" |
| 201 | if fd.field_type == tracker_pb2.FieldTypes.INT_TYPE: |
| 202 | try: |
| 203 | return tracker_bizobj.MakeFieldValue( |
| 204 | fd.field_id, int(val_str), None, None, None, None, False) |
| 205 | except ValueError: |
| 206 | return None # TODO(jrobbins): should bounce |
| 207 | |
| 208 | elif fd.field_type == tracker_pb2.FieldTypes.STR_TYPE: |
| 209 | return tracker_bizobj.MakeFieldValue( |
| 210 | fd.field_id, None, val_str, None, None, None, False) |
| 211 | |
| 212 | elif fd.field_type == tracker_pb2.FieldTypes.USER_TYPE: |
| 213 | if val_str: |
| 214 | try: |
| 215 | user_id = user_service.LookupUserID(cnxn, val_str, autocreate=False) |
| 216 | except exceptions.NoSuchUserException: |
| 217 | # Set to invalid user ID to display error during the validation step. |
| 218 | user_id = INVALID_USER_ID |
| 219 | return tracker_bizobj.MakeFieldValue( |
| 220 | fd.field_id, None, None, user_id, None, None, False) |
| 221 | else: |
| 222 | return None |
| 223 | |
| 224 | elif fd.field_type == tracker_pb2.FieldTypes.DATE_TYPE: |
| 225 | try: |
| 226 | timestamp = timestr.DateWidgetStrToTimestamp(val_str) |
| 227 | return tracker_bizobj.MakeFieldValue( |
| 228 | fd.field_id, None, None, None, timestamp, None, False) |
| 229 | except ValueError: |
| 230 | return None # TODO(jrobbins): should bounce |
| 231 | |
| 232 | elif fd.field_type == tracker_pb2.FieldTypes.URL_TYPE: |
| 233 | val_str = FormatUrlFieldValue(val_str) |
| 234 | try: |
| 235 | return tracker_bizobj.MakeFieldValue( |
| 236 | fd.field_id, None, None, None, None, val_str, False) |
| 237 | except ValueError: |
| 238 | return None # TODO(jojwang): should bounce |
| 239 | |
| 240 | else: |
| 241 | logging.error('Cant parse field with unexpected type %r', fd.field_type) |
| 242 | return None |
| 243 | |
| 244 | |
| 245 | def ParseOnePhaseFieldValue(cnxn, user_service, fd, val_str, phase_ids): |
| 246 | """Return a list containing a FieldValue PB for each phase.""" |
| 247 | phase_fvs = [] |
| 248 | for phase_id in phase_ids: |
| 249 | # TODO(jojwang): monorail:3970, create the FieldValue once and find some |
| 250 | # proto2 CopyFrom() method to create a new one for each phase. |
| 251 | fv = ParseOneFieldValue(cnxn, user_service, fd, val_str) |
| 252 | if fv: |
| 253 | fv.phase_id = phase_id |
| 254 | phase_fvs.append(fv) |
| 255 | |
| 256 | return phase_fvs |
| 257 | |
| 258 | |
| 259 | def ParseFieldValues(cnxn, user_service, field_val_strs, phase_field_val_strs, |
| 260 | config, phase_ids_by_name=None): |
| 261 | """Return a list of FieldValue PBs based on the given dict of strings.""" |
| 262 | field_values = [] |
| 263 | for fd in config.field_defs: |
| 264 | if fd.is_phase_field and ( |
| 265 | fd.field_id in phase_field_val_strs) and phase_ids_by_name: |
| 266 | fvs_by_phase_name = phase_field_val_strs.get(fd.field_id, {}) |
| 267 | for phase_name, val_strs in fvs_by_phase_name.items(): |
| 268 | phase_ids = phase_ids_by_name.get(phase_name) |
| 269 | if not phase_ids: |
| 270 | continue |
| 271 | for val_str in val_strs: |
| 272 | field_values.extend( |
| 273 | ParseOnePhaseFieldValue( |
| 274 | cnxn, user_service, fd, val_str, phase_ids=phase_ids)) |
| 275 | # We do not save phase fields when there are no phases. |
| 276 | elif not fd.is_phase_field and (fd.field_id in field_val_strs): |
| 277 | for val_str in field_val_strs[fd.field_id]: |
| 278 | fv = ParseOneFieldValue(cnxn, user_service, fd, val_str) |
| 279 | if fv: |
| 280 | field_values.append(fv) |
| 281 | |
| 282 | return field_values |
| 283 | |
| 284 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 285 | def ValidateLabels(cnxn, services, project_id, labels, ezt_errors=None): |
| 286 | """Validate labels to block creation of new labels for the Chromium project in |
| 287 | Monorail and return an error string or None. |
| 288 | |
| 289 | Args: |
| 290 | cnxn: MonorailConnection object. |
| 291 | services: Services object referencing services that can be queried. |
| 292 | project_id: Project ID. |
| 293 | labels: List of labels to be validated. |
| 294 | |
| 295 | Returns: |
| 296 | A string containing an error message if there was one. |
| 297 | """ |
| 298 | if settings.unit_test_mode or project_id in settings.label_freeze_project_ids: |
| 299 | new_labels = [ |
| 300 | l for l in labels if services.config.LookupLabelID( |
| 301 | cnxn, project_id, l, autocreate=False, case_sensitive=False) is None |
| 302 | and not settings.is_label_allowed(project_id, l) |
| 303 | ] |
| 304 | if len(new_labels) > 0: |
| 305 | err_msg = ( |
| 306 | "The creation of new labels is blocked for the Chromium project" |
| 307 | " in Monorail. To continue with editing your issue, please" |
| 308 | " remove: {} label(s).").format(", ".join(new_labels)) |
| 309 | if ezt_errors is not None: |
| 310 | ezt_errors.labels = err_msg |
| 311 | return err_msg |
| 312 | return None |
| 313 | |
| 314 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 315 | def ValidateCustomFieldValue(cnxn, project, services, field_def, field_val): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 316 | # type: (MonorailConnection, mrproto.tracker_pb2.Project, Services, |
| 317 | # mrproto.tracker_pb2.FieldDef, mrproto.tracker_pb2.FieldValue) -> str |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 318 | """Validate one custom field value and return an error string or None. |
| 319 | |
| 320 | Args: |
| 321 | cnxn: MonorailConnection object. |
| 322 | project: Project PB with info on the project the custom field belongs to. |
| 323 | services: Services object referencing services that can be queried. |
| 324 | field_def: FieldDef for the custom field we're validating against. |
| 325 | field_val: The value of the custom field. |
| 326 | |
| 327 | Returns: |
| 328 | A string containing an error message if there was one. |
| 329 | """ |
| 330 | if field_def.field_type == tracker_pb2.FieldTypes.INT_TYPE: |
| 331 | if (field_def.min_value is not None and |
| 332 | field_val.int_value < field_def.min_value): |
| 333 | return 'Value must be >= %d.' % field_def.min_value |
| 334 | if (field_def.max_value is not None and |
| 335 | field_val.int_value > field_def.max_value): |
| 336 | return 'Value must be <= %d.' % field_def.max_value |
| 337 | |
| 338 | elif field_def.field_type == tracker_pb2.FieldTypes.STR_TYPE: |
| 339 | if field_def.regex and field_val.str_value: |
| 340 | try: |
| 341 | regex = re.compile(field_def.regex) |
| 342 | if not regex.match(field_val.str_value): |
| 343 | return 'Value must match regular expression: %s.' % field_def.regex |
| 344 | except re.error: |
| 345 | logging.info('Failed to process regex %r with value %r. Allowing.', |
| 346 | field_def.regex, field_val.str_value) |
| 347 | return None |
| 348 | |
| 349 | elif field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE: |
| 350 | field_val_user = services.user.GetUser(cnxn, field_val.user_id) |
| 351 | auth = authdata.AuthData.FromUser(cnxn, field_val_user, services) |
| 352 | if auth.user_pb.user_id == INVALID_USER_ID: |
| 353 | return 'User not found.' |
| 354 | if field_def.needs_member: |
| 355 | user_value_in_project = framework_bizobj.UserIsInProject( |
| 356 | project, auth.effective_ids) |
| 357 | if not user_value_in_project: |
| 358 | return 'User must be a member of the project.' |
| 359 | if field_def.needs_perm: |
| 360 | user_perms = permissions.GetPermissions( |
| 361 | auth.user_pb, auth.effective_ids, project) |
| 362 | has_perm = user_perms.CanUsePerm( |
| 363 | field_def.needs_perm, auth.effective_ids, project, []) |
| 364 | if not has_perm: |
| 365 | return 'User must have permission "%s".' % field_def.needs_perm |
| 366 | return None |
| 367 | |
| 368 | elif field_def.field_type == tracker_pb2.FieldTypes.DATE_TYPE: |
| 369 | # TODO(jrobbins): date validation |
| 370 | pass |
| 371 | |
| 372 | elif field_def.field_type == tracker_pb2.FieldTypes.URL_TYPE: |
| 373 | if field_val.url_value: |
| 374 | if not (validate.IsValidURL(field_val.url_value) |
| 375 | or autolink_constants.IS_A_SHORT_LINK_RE.match( |
| 376 | field_val.url_value) |
| 377 | or autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE.match( |
| 378 | field_val.url_value) |
| 379 | or autolink_constants.IS_IMPLIED_LINK_RE.match( |
| 380 | field_val.url_value)): |
| 381 | return 'Value must be a valid url.' |
| 382 | |
| 383 | return None |
| 384 | |
| 385 | def ValidateCustomFields( |
| 386 | cnxn, services, field_values, config, project, ezt_errors=None, issue=None): |
| 387 | # type: (MonorailConnection, Services, |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 388 | # Collection[mrproto.tracker_pb2.FieldValue], |
| 389 | # mrproto.tracker_pb2.ProjectConfig, mrproto.tracker_pb2.Project, |
| 390 | # Optional[EZTError], Optional[mrproto.tracker_pb2.Issue]) -> |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 391 | # Sequence[str] |
| 392 | """Validate given fields and report problems in error messages.""" |
| 393 | fds_by_id = {fd.field_id: fd for fd in config.field_defs} |
| 394 | err_msgs = [] |
| 395 | |
| 396 | # Create a set of field_ids that have required values. If this set still |
| 397 | # contains items by the end of the function, there is an error. |
| 398 | required_fds = set() |
| 399 | if issue: |
| 400 | applicable_fds = ListApplicableFieldDefs([issue], config) |
| 401 | |
| 402 | lower_field_names = [fd.field_name.lower() for fd in applicable_fds] |
| 403 | label_prefixes = tracker_bizobj.LabelsByPrefix( |
| 404 | list(set(issue.labels)), lower_field_names) |
| 405 | |
| 406 | # Add applicable required fields to required_fds. |
| 407 | for fd in applicable_fds: |
| 408 | if not fd.is_required: |
| 409 | continue |
| 410 | |
| 411 | if (fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and |
| 412 | fd.field_name.lower() in label_prefixes): |
| 413 | # Handle custom enum fields - they're a special case because their |
| 414 | # values are stored in labels instead of FieldValues. |
| 415 | continue |
| 416 | |
| 417 | required_fds.add(fd.field_id) |
| 418 | # Ensure that every field value entered is valid. ie: That users exist. |
| 419 | for fv in field_values: |
| 420 | # Remove field_ids from the required set when found. |
| 421 | if fv.field_id in required_fds: |
| 422 | required_fds.remove(fv.field_id) |
| 423 | |
| 424 | fd = fds_by_id.get(fv.field_id) |
| 425 | if fd: |
| 426 | err_msg = ValidateCustomFieldValue(cnxn, project, services, fd, fv) |
| 427 | |
| 428 | if err_msg: |
| 429 | err_msgs.append('Error for %r: %s' % (fv, err_msg)) |
| 430 | if ezt_errors: |
| 431 | ezt_errors.SetCustomFieldError(fv.field_id, err_msg) |
| 432 | |
| 433 | # Add errors for any fields still left in the required set. |
| 434 | for field_id in required_fds: |
| 435 | fd = fds_by_id.get(field_id) |
| 436 | err_msg = '%s field is required.' % (fd.field_name) |
| 437 | err_msgs.append(err_msg) |
| 438 | if ezt_errors: |
| 439 | ezt_errors.SetCustomFieldError(field_id, err_msg) |
| 440 | |
| 441 | return err_msgs |
| 442 | |
| 443 | |
| 444 | def AssertCustomFieldsEditPerms( |
| 445 | mr, config, field_vals, field_vals_remove, fields_clear, labels, |
| 446 | labels_remove): |
| 447 | """Check permissions for any kind of custom field edition attempt.""" |
| 448 | # TODO: When clearing phase_fields is possible, include it in this method. |
| 449 | field_ids = set() |
| 450 | |
| 451 | for fv in field_vals: |
| 452 | field_ids.add(fv.field_id) |
| 453 | for fvr in field_vals_remove: |
| 454 | field_ids.add(fvr.field_id) |
| 455 | for fd_id in fields_clear: |
| 456 | field_ids.add(fd_id) |
| 457 | |
| 458 | enum_fds_by_name = { |
| 459 | fd.field_name.lower(): fd.field_id |
| 460 | for fd in config.field_defs |
| 461 | if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and not fd.is_deleted |
| 462 | } |
| 463 | for label in itertools.chain(labels, labels_remove): |
| 464 | enum_field_name = tracker_bizobj.LabelIsMaskedByField( |
| 465 | label, enum_fds_by_name.keys()) |
| 466 | if enum_field_name: |
| 467 | field_ids.add(enum_fds_by_name.get(enum_field_name)) |
| 468 | |
| 469 | fds_by_id = {fd.field_id: fd for fd in config.field_defs} |
| 470 | for field_id in field_ids: |
| 471 | fd = fds_by_id.get(field_id) |
| 472 | if fd: |
| 473 | assert permissions.CanEditValueForFieldDef( |
| 474 | mr.auth.effective_ids, mr.perms, mr.project, |
| 475 | fd), 'No permission to edit certain fields.' |
| 476 | |
| 477 | |
| 478 | def ApplyRestrictedDefaultValues( |
| 479 | mr, config, field_vals, labels, template_field_vals, template_labels): |
| 480 | """Add default values of template fields that the user cannot edit. |
| 481 | |
| 482 | This method can be called by servlets where restricted field values that |
| 483 | a user cannot edit are displayed but do not get returned when the user |
| 484 | submits the form (and also assumes that previous assertions ensure these |
| 485 | conditions). These missing default values still need to be passed to the |
| 486 | services layer when a 'write' is done so that these default values do |
| 487 | not get removed. |
| 488 | |
| 489 | Args: |
| 490 | mr: MonorailRequest Object to hold info about the request and the user. |
| 491 | config: ProjectIssueConfig Object for the project. |
| 492 | field_vals: list of FieldValues that the user wants to save. |
| 493 | labels: list of labels that the user wants to save. |
| 494 | template_field_vals: list of FieldValues belonging to the template. |
| 495 | template_labels: list of labels belonging to the template. |
| 496 | |
| 497 | Side Effect: |
| 498 | The default values of a template that the user cannot edit are added |
| 499 | to 'field_vals' and 'labels'. |
| 500 | """ |
| 501 | |
| 502 | fds_by_id = {fd.field_id: fd for fd in config.field_defs} |
| 503 | for fv in template_field_vals: |
| 504 | fd = fds_by_id.get(fv.field_id) |
| 505 | if fd and not permissions.CanEditValueForFieldDef(mr.auth.effective_ids, |
| 506 | mr.perms, mr.project, fd): |
| 507 | field_vals.append(fv) |
| 508 | |
| 509 | fds_by_name = { |
| 510 | fd.field_name.lower(): fd |
| 511 | for fd in config.field_defs |
| 512 | if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and not fd.is_deleted |
| 513 | } |
| 514 | for label in template_labels: |
| 515 | enum_field_name = tracker_bizobj.LabelIsMaskedByField( |
| 516 | label, fds_by_name.keys()) |
| 517 | if enum_field_name: |
| 518 | fd = fds_by_name.get(enum_field_name) |
| 519 | if fd and not permissions.CanEditValueForFieldDef( |
| 520 | mr.auth.effective_ids, mr.perms, mr.project, fd): |
| 521 | labels.append(label) |
| 522 | |
| 523 | |
| 524 | def FormatUrlFieldValue(url_str): |
| 525 | """Check for and add 'https://' to a url string""" |
| 526 | if not url_str.startswith('http'): |
| 527 | return 'http://' + url_str |
| 528 | return url_str |
| 529 | |
| 530 | |
| 531 | def ReviseFieldDefFromParsed(parsed, old_fd): |
| 532 | """Creates new FieldDef based on an original FieldDef and parsed FieldDef""" |
| 533 | if parsed.date_action_str in config_svc.DATE_ACTION_ENUM: |
| 534 | date_action = config_svc.DATE_ACTION_ENUM.index(parsed.date_action_str) |
| 535 | else: |
| 536 | date_action = 0 |
| 537 | return tracker_bizobj.MakeFieldDef( |
| 538 | old_fd.field_id, old_fd.project_id, old_fd.field_name, old_fd.field_type, |
| 539 | parsed.applicable_type, parsed.applicable_predicate, parsed.is_required, |
| 540 | parsed.is_niche, parsed.is_multivalued, parsed.min_value, |
| 541 | parsed.max_value, parsed.regex, parsed.needs_member, parsed.needs_perm, |
| 542 | parsed.grants_perm, parsed.notify_on, date_action, parsed.field_docstring, |
| 543 | False, approval_id=old_fd.approval_id or None, |
| 544 | is_phase_field=old_fd.is_phase_field) |
| 545 | |
| 546 | |
| 547 | def ParsedFieldDefAssertions(mr, parsed): |
| 548 | """Checks if new/updated FieldDef is not violating basic assertions. |
| 549 | If the assertions are violated, the errors |
| 550 | will be included in the mr.errors. |
| 551 | |
| 552 | Args: |
| 553 | mr: MonorailRequest object used to hold |
| 554 | commonly info parsed from the request. |
| 555 | parsed: ParsedFieldDef object used to contain parsed info, |
| 556 | in this case regarding a custom field definition. |
| 557 | """ |
| 558 | # TODO(crbug/monorail/7275): This method is meant to eventually |
| 559 | # do all assertion checkings (shared by create/update fieldDef) |
| 560 | # and assign all mr.errors values. |
| 561 | if (parsed.is_required and parsed.is_niche): |
| 562 | mr.errors.is_niche = 'A field cannot be both required and niche.' |
| 563 | if parsed.date_action_str is not None and ( |
| 564 | parsed.date_action_str not in config_svc.DATE_ACTION_ENUM): |
| 565 | mr.errors.date_action = 'The date action should be either: ' + ', '.join( |
| 566 | config_svc.DATE_ACTION_ENUM) + '.' |
| 567 | if (parsed.min_value is not None and parsed.max_value is not None and |
| 568 | parsed.min_value > parsed.max_value): |
| 569 | mr.errors.min_value = 'Minimum value must be less than maximum.' |
| 570 | if parsed.regex: |
| 571 | try: |
| 572 | re.compile(parsed.regex) |
| 573 | except re.error: |
| 574 | mr.errors.regex = 'Invalid regular expression.' |