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 |