| Index: appengine/monorail/tracker/fieldcreate.py
|
| diff --git a/appengine/monorail/tracker/fieldcreate.py b/appengine/monorail/tracker/fieldcreate.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ce543532e212495558cb646e1720b87e98aa7a15
|
| --- /dev/null
|
| +++ b/appengine/monorail/tracker/fieldcreate.py
|
| @@ -0,0 +1,185 @@
|
| +# 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
|
| +
|
| +"""A servlet for project owners to create a new field def."""
|
| +
|
| +import logging
|
| +import re
|
| +import time
|
| +
|
| +from third_party import ezt
|
| +
|
| +from framework import framework_helpers
|
| +from framework import jsonfeed
|
| +from framework import permissions
|
| +from framework import servlet
|
| +from framework import urls
|
| +from tracker import field_helpers
|
| +from tracker import tracker_constants
|
| +from tracker import tracker_helpers
|
| +
|
| +
|
| +class FieldCreate(servlet.Servlet):
|
| + """Servlet allowing project owners to create a custom field."""
|
| +
|
| + _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
|
| + _PAGE_TEMPLATE = 'tracker/field-create-page.ezt'
|
| +
|
| + def AssertBasePermission(self, mr):
|
| + """Check whether the user has any permission to visit this page.
|
| +
|
| + Args:
|
| + mr: commonly used info parsed from the request.
|
| + """
|
| + super(FieldCreate, self).AssertBasePermission(mr)
|
| + if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
|
| + raise permissions.PermissionException(
|
| + 'You are not allowed to administer this project')
|
| +
|
| + def GatherPageData(self, mr):
|
| + """Build up a dictionary of data values to use when rendering the page.
|
| +
|
| + Args:
|
| + mr: commonly used info parsed from the request.
|
| +
|
| + Returns:
|
| + Dict of values used by EZT for rendering the page.
|
| + """
|
| + config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
|
| + well_known_issue_types = tracker_helpers.FilterIssueTypes(config)
|
| +
|
| + return {
|
| + 'admin_tab_mode': servlet.Servlet.PROCESS_TAB_LABELS,
|
| + 'initial_field_name': '',
|
| + 'initial_field_docstring': '',
|
| + 'initial_is_required': ezt.boolean(False),
|
| + 'initial_is_multivalued': ezt.boolean(False),
|
| + 'initial_choices': '',
|
| + 'initial_admins': '',
|
| + 'initial_type': 'enum_type',
|
| + 'initial_applicable_type': '', # That means any issue type
|
| + 'initial_applicable_predicate': '',
|
| + 'initial_needs_member': ezt.boolean(False),
|
| + 'initial_needs_perm': '',
|
| + 'initial_grants_perm': '',
|
| + 'initial_notify_on': 0,
|
| + 'well_known_issue_types': well_known_issue_types,
|
| + }
|
| +
|
| + def ProcessFormData(self, mr, post_data):
|
| + """Validate and store the contents of the issues tracker admin page.
|
| +
|
| + Args:
|
| + mr: commonly used info parsed from the request.
|
| + post_data: HTML form data from the request.
|
| +
|
| + Returns:
|
| + String URL to redirect the user to, or None if response was already sent.
|
| + """
|
| + config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
|
| + parsed = field_helpers.ParseFieldDefRequest(post_data, config)
|
| +
|
| + if not tracker_constants.FIELD_NAME_RE.match(parsed.field_name):
|
| + mr.errors.field_name = 'Invalid field name'
|
| +
|
| + field_name_error_msg = FieldNameErrorMessage(parsed.field_name, config)
|
| + if field_name_error_msg:
|
| + mr.errors.field_name = field_name_error_msg
|
| +
|
| + if (parsed.min_value is not None and parsed.max_value is not None and
|
| + parsed.min_value > parsed.max_value):
|
| + mr.errors.min_value = 'Minimum value must be less than maximum.'
|
| +
|
| + if parsed.regex:
|
| + try:
|
| + re.compile(parsed.regex)
|
| + except re.error:
|
| + mr.errors.regex = 'Invalid regular expression.'
|
| +
|
| + admin_ids, admin_str = tracker_helpers.ParseAdminUsers(
|
| + mr.cnxn, post_data['admin_names'], self.services.user)
|
| +
|
| + if mr.errors.AnyErrors():
|
| + self.PleaseCorrect(
|
| + mr, initial_field_name=parsed.field_name,
|
| + initial_type=parsed.field_type_str,
|
| + initial_field_docstring=parsed.field_docstring,
|
| + initial_applicable_type=parsed.applicable_type,
|
| + initial_applicable_predicate=parsed.applicable_predicate,
|
| + initial_needs_member=ezt.boolean(parsed.needs_member),
|
| + initial_needs_perm=parsed.needs_perm,
|
| + initial_is_required=ezt.boolean(parsed.is_required),
|
| + initial_is_multivalued=ezt.boolean(parsed.is_multivalued),
|
| + initial_grants_perm=parsed.grants_perm,
|
| + initial_notify_on=parsed.notify_on,
|
| + initial_choices=parsed.choices_text,
|
| + initial_admins=admin_str)
|
| + return
|
| +
|
| + self.services.config.CreateFieldDef(
|
| + mr.cnxn, mr.project_id, parsed.field_name, parsed.field_type_str,
|
| + parsed.applicable_type, parsed.applicable_predicate,
|
| + parsed.is_required, parsed.is_multivalued,
|
| + parsed.min_value, parsed.max_value, parsed.regex, parsed.needs_member,
|
| + parsed.needs_perm, parsed.grants_perm, parsed.notify_on,
|
| + parsed.field_docstring, admin_ids)
|
| + if parsed.field_type_str == 'enum_type':
|
| + self.services.config.UpdateConfig(
|
| + mr.cnxn, mr.project, well_known_labels=parsed.revised_labels)
|
| +
|
| + return framework_helpers.FormatAbsoluteURL(
|
| + mr, urls.ADMIN_LABELS, saved=1, ts=int(time.time()))
|
| +
|
| +
|
| +class CheckFieldNameJSON(jsonfeed.JsonFeed):
|
| + """JSON data for handling name checks when creating a field."""
|
| +
|
| + def HandleRequest(self, mr):
|
| + """Provide the UI with info about the availability of the field name.
|
| +
|
| + Args:
|
| + mr: common information parsed from the HTTP request.
|
| +
|
| + Returns:
|
| + Results dictionary in JSON format.
|
| + """
|
| + field_name = mr.GetParam('field')
|
| + config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
|
| + choices = ExistingEnumChoices(field_name, config)
|
| + choices_dicts = [dict(name=choice.name_padded, doc=choice.docstring)
|
| + for choice in choices]
|
| + message = FieldNameErrorMessage(field_name, config)
|
| +
|
| + return {
|
| + 'error_message': message,
|
| + 'choices': choices_dicts,
|
| + }
|
| +
|
| +
|
| +def FieldNameErrorMessage(field_name, config):
|
| + """Return an error message for the given field name, or None."""
|
| + field_name_lower = field_name.lower()
|
| + if field_name_lower in tracker_constants.RESERVED_PREFIXES:
|
| + return 'That name is reserved.'
|
| +
|
| + for fd in config.field_defs:
|
| + fn_lower = fd.field_name.lower()
|
| + if field_name_lower == fn_lower:
|
| + return 'That name is already in use.'
|
| + if field_name_lower.startswith(fn_lower + '-'):
|
| + return 'An existing field name is a prefix of that name.'
|
| + if fn_lower.startswith(field_name_lower + '-'):
|
| + return 'That name is a prefix of an existing field name.'
|
| +
|
| + return None
|
| +
|
| +
|
| +def ExistingEnumChoices(field_name, config):
|
| + """Return a list of existing label choices for the given prefix."""
|
| + # If there are existing labels with that prefix, then it must be enum.
|
| + # The existing labels will be treated as field values.
|
| + choices = tracker_helpers.LabelsMaskedByFields(
|
| + config, [field_name], trim_prefix=True)
|
| + return choices
|
|
|