OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered by a BSD-style |
| 3 # license that can be found in the LICENSE file or at |
| 4 # https://developers.google.com/open-source/licenses/bsd |
| 5 |
| 6 """Helper functions for custom field sevlets.""" |
| 7 |
| 8 import collections |
| 9 import logging |
| 10 import re |
| 11 |
| 12 from framework import framework_bizobj |
| 13 from framework import framework_constants |
| 14 from framework import monorailrequest |
| 15 from framework import permissions |
| 16 from proto import tracker_pb2 |
| 17 from services import config_svc |
| 18 from services import user_svc |
| 19 from tracker import tracker_bizobj |
| 20 |
| 21 |
| 22 INVALID_USER_ID = -1 |
| 23 |
| 24 ParsedFieldDef = collections.namedtuple( |
| 25 'ParsedFieldDef', |
| 26 'field_name, field_type_str, min_value, max_value, regex, ' |
| 27 'needs_member, needs_perm, grants_perm, notify_on, is_required, ' |
| 28 'is_multivalued, field_docstring, choices_text, applicable_type, ' |
| 29 'applicable_predicate, revised_labels') |
| 30 |
| 31 |
| 32 def ParseFieldDefRequest(post_data, config): |
| 33 """Parse the user's HTML form data to update a field definition.""" |
| 34 field_name = post_data.get('name', '') |
| 35 field_type_str = post_data.get('field_type') |
| 36 # TODO(jrobbins): once a min or max is set, it cannot be completely removed. |
| 37 min_value_str = post_data.get('min_value') |
| 38 try: |
| 39 min_value = int(min_value_str) |
| 40 except (ValueError, TypeError): |
| 41 min_value = None |
| 42 max_value_str = post_data.get('max_value') |
| 43 try: |
| 44 max_value = int(max_value_str) |
| 45 except (ValueError, TypeError): |
| 46 max_value = None |
| 47 regex = post_data.get('regex') |
| 48 needs_member = 'needs_member' in post_data |
| 49 needs_perm = post_data.get('needs_perm', '').strip() |
| 50 grants_perm = post_data.get('grants_perm', '').strip() |
| 51 notify_on_str = post_data.get('notify_on') |
| 52 if notify_on_str in config_svc.NOTIFY_ON_ENUM: |
| 53 notify_on = config_svc.NOTIFY_ON_ENUM.index(notify_on_str) |
| 54 else: |
| 55 notify_on = 0 |
| 56 is_required = 'is_required' in post_data |
| 57 is_multivalued = 'is_multivalued' in post_data |
| 58 field_docstring = post_data.get('docstring', '') |
| 59 choices_text = post_data.get('choices', '') |
| 60 applicable_type = post_data.get('applicable_type', '') |
| 61 applicable_predicate = '' # TODO(jrobbins): placeholder for future feature |
| 62 revised_labels = _ParseChoicesIntoWellKnownLabels( |
| 63 choices_text, field_name, config) |
| 64 |
| 65 return ParsedFieldDef( |
| 66 field_name, field_type_str, min_value, max_value, regex, |
| 67 needs_member, needs_perm, grants_perm, notify_on, is_required, |
| 68 is_multivalued, field_docstring, choices_text, applicable_type, |
| 69 applicable_predicate, revised_labels) |
| 70 |
| 71 |
| 72 def _ParseChoicesIntoWellKnownLabels(choices_text, field_name, config): |
| 73 """Parse a field's possible choices and integrate them into the config. |
| 74 |
| 75 Args: |
| 76 choices_text: string with one label and optional docstring per line. |
| 77 field_name: string name of the field definition being edited. |
| 78 config: ProjectIssueConfig PB of the current project. |
| 79 |
| 80 Returns: |
| 81 A revised list of labels that can be used to update the config. |
| 82 """ |
| 83 matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(choices_text) |
| 84 new_labels = [ |
| 85 ('%s-%s' % (field_name, label), choice_docstring.strip(), False) |
| 86 for label, choice_docstring in matches] |
| 87 kept_labels = [ |
| 88 (wkl.label, wkl.label_docstring, False) |
| 89 for wkl in config.well_known_labels |
| 90 if not tracker_bizobj.LabelIsMaskedByField( |
| 91 wkl.label, [field_name.lower()])] |
| 92 revised_labels = kept_labels + new_labels |
| 93 return revised_labels |
| 94 |
| 95 |
| 96 def ShiftEnumFieldsIntoLabels( |
| 97 labels, labels_remove, field_val_strs, field_val_strs_remove, config): |
| 98 """Look at the custom field values and treat enum fields as labels. |
| 99 |
| 100 Args: |
| 101 labels: list of labels to add/set on the issue. |
| 102 labels_remove: list of labels to remove from the issue. |
| 103 field_val_strs: {field_id: [val_str, ...]} of custom fields to add/set. |
| 104 field_val_strs_remove: {field_id: [val_str, ...]} of custom fields to |
| 105 remove. |
| 106 config: ProjectIssueConfig PB including custom field definitions. |
| 107 |
| 108 SIDE-EFFECT: the labels and labels_remove lists will be extended with |
| 109 key-value labels corresponding to the enum field values. Those field |
| 110 entries will be removed from field_vals and field_vals_remove. |
| 111 """ |
| 112 for fd in config.field_defs: |
| 113 if fd.field_type != tracker_pb2.FieldTypes.ENUM_TYPE: |
| 114 continue |
| 115 |
| 116 if fd.field_id in field_val_strs: |
| 117 labels.extend( |
| 118 '%s-%s' % (fd.field_name, val) |
| 119 for val in field_val_strs[fd.field_id] |
| 120 if val and val != '--') |
| 121 del field_val_strs[fd.field_id] |
| 122 |
| 123 if fd.field_id in field_val_strs_remove: |
| 124 labels_remove.extend( |
| 125 '%s-%s' % (fd.field_name, val) |
| 126 for val in field_val_strs_remove[fd.field_id] |
| 127 if val and val != '--') |
| 128 del field_val_strs_remove[fd.field_id] |
| 129 |
| 130 |
| 131 def _ParseOneFieldValue(cnxn, user_service, fd, val_str): |
| 132 """Make one FieldValue PB from the given user-supplied string.""" |
| 133 if fd.field_type == tracker_pb2.FieldTypes.INT_TYPE: |
| 134 try: |
| 135 return tracker_bizobj.MakeFieldValue( |
| 136 fd.field_id, int(val_str), None, None, False) |
| 137 except ValueError: |
| 138 return None # TODO(jrobbins): should bounce |
| 139 |
| 140 elif fd.field_type == tracker_pb2.FieldTypes.STR_TYPE: |
| 141 return tracker_bizobj.MakeFieldValue( |
| 142 fd.field_id, None, val_str, None, False) |
| 143 |
| 144 elif fd.field_type == tracker_pb2.FieldTypes.USER_TYPE: |
| 145 if val_str: |
| 146 try: |
| 147 user_id = user_service.LookupUserID(cnxn, val_str, autocreate=False) |
| 148 except user_svc.NoSuchUserException: |
| 149 # Set to invalid user ID to display error during the validation step. |
| 150 user_id = INVALID_USER_ID |
| 151 return tracker_bizobj.MakeFieldValue( |
| 152 fd.field_id, None, None, user_id, False) |
| 153 else: |
| 154 return None |
| 155 |
| 156 else: |
| 157 logging.error('Cant parse field with unexpected type %r', fd.field_type) |
| 158 return None |
| 159 |
| 160 |
| 161 def ParseFieldValues(cnxn, user_service, field_val_strs, config): |
| 162 """Return a list of FieldValue PBs based on the the given dict of strings.""" |
| 163 field_values = [] |
| 164 for fd in config.field_defs: |
| 165 if fd.field_id not in field_val_strs: |
| 166 continue |
| 167 for val_str in field_val_strs[fd.field_id]: |
| 168 fv = _ParseOneFieldValue(cnxn, user_service, fd, val_str) |
| 169 if fv: |
| 170 field_values.append(fv) |
| 171 |
| 172 return field_values |
| 173 |
| 174 |
| 175 def _ValidateOneCustomField(mr, services, field_def, field_val): |
| 176 """Validate one custom field value and return an error string or None.""" |
| 177 if field_def.field_type == tracker_pb2.FieldTypes.INT_TYPE: |
| 178 if (field_def.min_value is not None and |
| 179 field_val.int_value < field_def.min_value): |
| 180 return 'Value must be >= %d' % field_def.min_value |
| 181 if (field_def.max_value is not None and |
| 182 field_val.int_value > field_def.max_value): |
| 183 return 'Value must be <= %d' % field_def.max_value |
| 184 |
| 185 elif field_def.field_type == tracker_pb2.FieldTypes.STR_TYPE: |
| 186 if field_def.regex and field_val.str_value: |
| 187 try: |
| 188 regex = re.compile(field_def.regex) |
| 189 if not regex.match(field_val.str_value): |
| 190 return 'Value must match regular expression: %s' % field_def.regex |
| 191 except re.error: |
| 192 logging.info('Failed to process regex %r with value %r. Allowing.', |
| 193 field_def.regex, field_val.str_value) |
| 194 return None |
| 195 |
| 196 elif field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE: |
| 197 if field_val.user_id == INVALID_USER_ID: |
| 198 return 'User not found' |
| 199 if field_def.needs_member: |
| 200 auth = monorailrequest.AuthData.FromUserID( |
| 201 mr.cnxn, field_val.user_id, services) |
| 202 user_value_in_project = framework_bizobj.UserIsInProject( |
| 203 mr.project, auth.effective_ids) |
| 204 if not user_value_in_project: |
| 205 return 'User must be a member of the project' |
| 206 if field_def.needs_perm: |
| 207 field_val_user = services.user.GetUser(mr.cnxn, field_val.user_id) |
| 208 user_perms = permissions.GetPermissions( |
| 209 field_val_user, auth.effective_ids, mr.project) |
| 210 has_perm = user_perms.CanUsePerm( |
| 211 field_def.needs_perm, auth.effective_ids, mr.project, []) |
| 212 if not has_perm: |
| 213 return 'User must have permission "%s"' % field_def.needs_perm |
| 214 |
| 215 return None |
| 216 |
| 217 |
| 218 def ValidateCustomFields(mr, services, field_values, config, errors): |
| 219 """Validate each of the given fields and report problems in errors object.""" |
| 220 for fv in field_values: |
| 221 fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config) |
| 222 if fd: |
| 223 err_msg = _ValidateOneCustomField(mr, services, fd, fv) |
| 224 if err_msg: |
| 225 errors.SetCustomFieldError(fv.field_id, err_msg) |
OLD | NEW |