Index: appengine/monorail/tracker/field_helpers.py |
diff --git a/appengine/monorail/tracker/field_helpers.py b/appengine/monorail/tracker/field_helpers.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..452f329df6a6fec48af933aa6ca9fae113ffef0e |
--- /dev/null |
+++ b/appengine/monorail/tracker/field_helpers.py |
@@ -0,0 +1,225 @@ |
+# Copyright 2016 The Chromium Authors. All rights reserved. |
+# Use of this source code is govered by a BSD-style |
+# license that can be found in the LICENSE file or at |
+# https://developers.google.com/open-source/licenses/bsd |
+ |
+"""Helper functions for custom field sevlets.""" |
+ |
+import collections |
+import logging |
+import re |
+ |
+from framework import framework_bizobj |
+from framework import framework_constants |
+from framework import monorailrequest |
+from framework import permissions |
+from proto import tracker_pb2 |
+from services import config_svc |
+from services import user_svc |
+from tracker import tracker_bizobj |
+ |
+ |
+INVALID_USER_ID = -1 |
+ |
+ParsedFieldDef = collections.namedtuple( |
+ 'ParsedFieldDef', |
+ 'field_name, field_type_str, min_value, max_value, regex, ' |
+ 'needs_member, needs_perm, grants_perm, notify_on, is_required, ' |
+ 'is_multivalued, field_docstring, choices_text, applicable_type, ' |
+ 'applicable_predicate, revised_labels') |
+ |
+ |
+def ParseFieldDefRequest(post_data, config): |
+ """Parse the user's HTML form data to update a field definition.""" |
+ field_name = post_data.get('name', '') |
+ field_type_str = post_data.get('field_type') |
+ # TODO(jrobbins): once a min or max is set, it cannot be completely removed. |
+ min_value_str = post_data.get('min_value') |
+ try: |
+ min_value = int(min_value_str) |
+ except (ValueError, TypeError): |
+ min_value = None |
+ max_value_str = post_data.get('max_value') |
+ try: |
+ max_value = int(max_value_str) |
+ except (ValueError, TypeError): |
+ max_value = None |
+ regex = post_data.get('regex') |
+ needs_member = 'needs_member' in post_data |
+ needs_perm = post_data.get('needs_perm', '').strip() |
+ grants_perm = post_data.get('grants_perm', '').strip() |
+ notify_on_str = post_data.get('notify_on') |
+ if notify_on_str in config_svc.NOTIFY_ON_ENUM: |
+ notify_on = config_svc.NOTIFY_ON_ENUM.index(notify_on_str) |
+ else: |
+ notify_on = 0 |
+ is_required = 'is_required' in post_data |
+ is_multivalued = 'is_multivalued' in post_data |
+ field_docstring = post_data.get('docstring', '') |
+ choices_text = post_data.get('choices', '') |
+ applicable_type = post_data.get('applicable_type', '') |
+ applicable_predicate = '' # TODO(jrobbins): placeholder for future feature |
+ revised_labels = _ParseChoicesIntoWellKnownLabels( |
+ choices_text, field_name, config) |
+ |
+ return ParsedFieldDef( |
+ field_name, field_type_str, min_value, max_value, regex, |
+ needs_member, needs_perm, grants_perm, notify_on, is_required, |
+ is_multivalued, field_docstring, choices_text, applicable_type, |
+ applicable_predicate, revised_labels) |
+ |
+ |
+def _ParseChoicesIntoWellKnownLabels(choices_text, field_name, config): |
+ """Parse a field's possible choices and integrate them into the config. |
+ |
+ Args: |
+ choices_text: string with one label and optional docstring per line. |
+ field_name: string name of the field definition being edited. |
+ config: ProjectIssueConfig PB of the current project. |
+ |
+ Returns: |
+ A revised list of labels that can be used to update the config. |
+ """ |
+ matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(choices_text) |
+ new_labels = [ |
+ ('%s-%s' % (field_name, label), choice_docstring.strip(), False) |
+ for label, choice_docstring in matches] |
+ kept_labels = [ |
+ (wkl.label, wkl.label_docstring, False) |
+ for wkl in config.well_known_labels |
+ if not tracker_bizobj.LabelIsMaskedByField( |
+ wkl.label, [field_name.lower()])] |
+ revised_labels = kept_labels + new_labels |
+ return revised_labels |
+ |
+ |
+def ShiftEnumFieldsIntoLabels( |
+ labels, labels_remove, field_val_strs, field_val_strs_remove, config): |
+ """Look at the custom field values and treat enum fields as labels. |
+ |
+ Args: |
+ labels: list of labels to add/set on the issue. |
+ labels_remove: list of labels to remove from the issue. |
+ field_val_strs: {field_id: [val_str, ...]} of custom fields to add/set. |
+ field_val_strs_remove: {field_id: [val_str, ...]} of custom fields to |
+ remove. |
+ config: ProjectIssueConfig PB including custom field definitions. |
+ |
+ SIDE-EFFECT: the labels and labels_remove lists will be extended with |
+ key-value labels corresponding to the enum field values. Those field |
+ entries will be removed from field_vals and field_vals_remove. |
+ """ |
+ for fd in config.field_defs: |
+ if fd.field_type != tracker_pb2.FieldTypes.ENUM_TYPE: |
+ continue |
+ |
+ if fd.field_id in field_val_strs: |
+ labels.extend( |
+ '%s-%s' % (fd.field_name, val) |
+ for val in field_val_strs[fd.field_id] |
+ if val and val != '--') |
+ del field_val_strs[fd.field_id] |
+ |
+ if fd.field_id in field_val_strs_remove: |
+ labels_remove.extend( |
+ '%s-%s' % (fd.field_name, val) |
+ for val in field_val_strs_remove[fd.field_id] |
+ if val and val != '--') |
+ del field_val_strs_remove[fd.field_id] |
+ |
+ |
+def _ParseOneFieldValue(cnxn, user_service, fd, val_str): |
+ """Make one FieldValue PB from the given user-supplied string.""" |
+ if fd.field_type == tracker_pb2.FieldTypes.INT_TYPE: |
+ try: |
+ return tracker_bizobj.MakeFieldValue( |
+ fd.field_id, int(val_str), None, None, False) |
+ except ValueError: |
+ return None # TODO(jrobbins): should bounce |
+ |
+ elif fd.field_type == tracker_pb2.FieldTypes.STR_TYPE: |
+ return tracker_bizobj.MakeFieldValue( |
+ fd.field_id, None, val_str, None, False) |
+ |
+ elif fd.field_type == tracker_pb2.FieldTypes.USER_TYPE: |
+ if val_str: |
+ try: |
+ user_id = user_service.LookupUserID(cnxn, val_str, autocreate=False) |
+ except user_svc.NoSuchUserException: |
+ # Set to invalid user ID to display error during the validation step. |
+ user_id = INVALID_USER_ID |
+ return tracker_bizobj.MakeFieldValue( |
+ fd.field_id, None, None, user_id, False) |
+ else: |
+ return None |
+ |
+ else: |
+ logging.error('Cant parse field with unexpected type %r', fd.field_type) |
+ return None |
+ |
+ |
+def ParseFieldValues(cnxn, user_service, field_val_strs, config): |
+ """Return a list of FieldValue PBs based on the the given dict of strings.""" |
+ field_values = [] |
+ for fd in config.field_defs: |
+ if fd.field_id not in field_val_strs: |
+ continue |
+ for val_str in field_val_strs[fd.field_id]: |
+ fv = _ParseOneFieldValue(cnxn, user_service, fd, val_str) |
+ if fv: |
+ field_values.append(fv) |
+ |
+ return field_values |
+ |
+ |
+def _ValidateOneCustomField(mr, services, field_def, field_val): |
+ """Validate one custom field value and return an error string or None.""" |
+ if field_def.field_type == tracker_pb2.FieldTypes.INT_TYPE: |
+ if (field_def.min_value is not None and |
+ field_val.int_value < field_def.min_value): |
+ return 'Value must be >= %d' % field_def.min_value |
+ if (field_def.max_value is not None and |
+ field_val.int_value > field_def.max_value): |
+ return 'Value must be <= %d' % field_def.max_value |
+ |
+ elif field_def.field_type == tracker_pb2.FieldTypes.STR_TYPE: |
+ if field_def.regex and field_val.str_value: |
+ try: |
+ regex = re.compile(field_def.regex) |
+ if not regex.match(field_val.str_value): |
+ return 'Value must match regular expression: %s' % field_def.regex |
+ except re.error: |
+ logging.info('Failed to process regex %r with value %r. Allowing.', |
+ field_def.regex, field_val.str_value) |
+ return None |
+ |
+ elif field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE: |
+ if field_val.user_id == INVALID_USER_ID: |
+ return 'User not found' |
+ if field_def.needs_member: |
+ auth = monorailrequest.AuthData.FromUserID( |
+ mr.cnxn, field_val.user_id, services) |
+ user_value_in_project = framework_bizobj.UserIsInProject( |
+ mr.project, auth.effective_ids) |
+ if not user_value_in_project: |
+ return 'User must be a member of the project' |
+ if field_def.needs_perm: |
+ field_val_user = services.user.GetUser(mr.cnxn, field_val.user_id) |
+ user_perms = permissions.GetPermissions( |
+ field_val_user, auth.effective_ids, mr.project) |
+ has_perm = user_perms.CanUsePerm( |
+ field_def.needs_perm, auth.effective_ids, mr.project, []) |
+ if not has_perm: |
+ return 'User must have permission "%s"' % field_def.needs_perm |
+ |
+ return None |
+ |
+ |
+def ValidateCustomFields(mr, services, field_values, config, errors): |
+ """Validate each of the given fields and report problems in errors object.""" |
+ for fv in field_values: |
+ fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config) |
+ if fd: |
+ err_msg = _ValidateOneCustomField(mr, services, fd, fv) |
+ if err_msg: |
+ errors.SetCustomFieldError(fv.field_id, err_msg) |