blob: b1f2316dd11dd308f3fab671321c2a209dd6c1a3 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed 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"""A servlet for project owners to create a new field def."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import logging
12import re
13import time
14
15import ezt
16
17from framework import exceptions
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020018from framework import flaskservlet
Copybara854996b2021-09-07 19:36:02 +000019from framework import framework_helpers
20from framework import jsonfeed
21from framework import permissions
22from framework import servlet
23from framework import urls
24from proto import tracker_pb2
25from tracker import field_helpers
26from tracker import tracker_bizobj
27from tracker import tracker_constants
28from tracker import tracker_helpers
29
30
31class FieldCreate(servlet.Servlet):
32 """Servlet allowing project owners to create a custom field."""
33
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020034 _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
Copybara854996b2021-09-07 19:36:02 +000035 _PAGE_TEMPLATE = 'tracker/field-create-page.ezt'
36
37 def AssertBasePermission(self, mr):
38 """Check whether the user has any permission to visit this page.
39
40 Args:
41 mr: commonly used info parsed from the request.
42 """
43 super(FieldCreate, self).AssertBasePermission(mr)
44 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
45 raise permissions.PermissionException(
46 'You are not allowed to administer this project')
47
48 def GatherPageData(self, mr):
49 """Build up a dictionary of data values to use when rendering the page.
50
51 Args:
52 mr: commonly used info parsed from the request.
53
54 Returns:
55 Dict of values used by EZT for rendering the page.
56 """
57 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
58 well_known_issue_types = tracker_helpers.FilterIssueTypes(config)
59 approval_names = [fd.field_name for fd in config.field_defs if
60 fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE and
61 not fd.is_deleted]
62
63 return {
64 'admin_tab_mode': servlet.Servlet.PROCESS_TAB_LABELS,
65 'initial_field_name': '',
66 'initial_field_docstring': '',
67 'initial_importance': 'normal',
68 'initial_is_multivalued': ezt.boolean(False),
69 'initial_parent_approval_name': '',
70 'initial_choices': '',
71 'initial_admins': '',
72 'initial_editors': '',
73 'initial_type': 'enum_type',
74 'initial_applicable_type': '', # That means any issue type
75 'initial_applicable_predicate': '',
76 'initial_needs_member': ezt.boolean(False),
77 'initial_needs_perm': '',
78 'initial_grants_perm': '',
79 'initial_notify_on': 0,
80 'initial_date_action': 'no_action',
81 'well_known_issue_types': well_known_issue_types,
82 'initial_approvers': '',
83 'initial_survey': '',
84 'approval_names': approval_names,
85 'initial_is_phase_field': ezt.boolean(False),
86 'initial_is_restricted_field': ezt.boolean(False),
87 }
88
89 def ProcessFormData(self, mr, post_data):
90 """Validate and store the contents of the issues tracker admin page.
91
92 Args:
93 mr: commonly used info parsed from the request.
94 post_data: HTML form data from the request.
95
96 Returns:
97 String URL to redirect the user to, or None if response was already sent.
98 """
99 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
100 parsed = field_helpers.ParseFieldDefRequest(post_data, config)
101
102 if not tracker_constants.FIELD_NAME_RE.match(parsed.field_name):
103 mr.errors.field_name = 'Invalid field name'
104
105 field_name_error_msg = FieldNameErrorMessage(parsed.field_name, config)
106 if field_name_error_msg:
107 mr.errors.field_name = field_name_error_msg
108
109 admin_ids, admin_str = tracker_helpers.ParsePostDataUsers(
110 mr.cnxn, post_data['admin_names'], self.services.user)
111 editor_ids, editor_str = tracker_helpers.ParsePostDataUsers(
112 mr.cnxn, post_data.get('editor_names', ''), self.services.user)
113
114 field_helpers.ParsedFieldDefAssertions(mr, parsed)
115
116 if not (parsed.is_restricted_field):
117 assert not editor_ids, 'Editors are only for restricted fields.'
118
119 # TODO(crbug/monorail/7275): This condition could potentially be
120 # included in the field_helpers.ParsedFieldDefAssertions method,
121 # just remember that it should be compatible with its usage in
122 # fielddetail.py where there is a very similar condition.
123 if parsed.field_type_str == 'approval_type':
124 assert not (
125 parsed.is_restricted_field), 'Approval fields cannot be restricted.'
126 if parsed.approvers_str:
127 approver_ids_dict = self.services.user.LookupUserIDs(
128 mr.cnxn, re.split('[,;\s]+', parsed.approvers_str),
129 autocreate=True)
130 approver_ids = list(set(approver_ids_dict.values()))
131 else:
132 mr.errors.approvers = 'Please provide at least one default approver.'
133
134 if mr.errors.AnyErrors():
135 self.PleaseCorrect(
136 mr,
137 initial_field_name=parsed.field_name,
138 initial_type=parsed.field_type_str,
139 initial_parent_approval_name=parsed.parent_approval_name,
140 initial_field_docstring=parsed.field_docstring,
141 initial_applicable_type=parsed.applicable_type,
142 initial_applicable_predicate=parsed.applicable_predicate,
143 initial_needs_member=ezt.boolean(parsed.needs_member),
144 initial_needs_perm=parsed.needs_perm,
145 initial_importance=parsed.importance,
146 initial_is_multivalued=ezt.boolean(parsed.is_multivalued),
147 initial_grants_perm=parsed.grants_perm,
148 initial_notify_on=parsed.notify_on,
149 initial_date_action=parsed.date_action_str,
150 initial_choices=parsed.choices_text,
151 initial_approvers=parsed.approvers_str,
152 initial_survey=parsed.survey,
153 initial_is_phase_field=parsed.is_phase_field,
154 initial_admins=admin_str,
155 initial_editors=editor_str,
156 initial_is_restricted_field=parsed.is_restricted_field)
157 return
158
159 approval_id = None
160 if parsed.parent_approval_name and (
161 parsed.field_type_str != 'approval_type'):
162 approval_fd = tracker_bizobj.FindFieldDef(
163 parsed.parent_approval_name, config)
164 if approval_fd:
165 approval_id = approval_fd.field_id
166 field_id = self.services.config.CreateFieldDef(
167 mr.cnxn,
168 mr.project_id,
169 parsed.field_name,
170 parsed.field_type_str,
171 parsed.applicable_type,
172 parsed.applicable_predicate,
173 parsed.is_required,
174 parsed.is_niche,
175 parsed.is_multivalued,
176 parsed.min_value,
177 parsed.max_value,
178 parsed.regex,
179 parsed.needs_member,
180 parsed.needs_perm,
181 parsed.grants_perm,
182 parsed.notify_on,
183 parsed.date_action_str,
184 parsed.field_docstring,
185 admin_ids,
186 editor_ids,
187 approval_id,
188 parsed.is_phase_field,
189 is_restricted_field=parsed.is_restricted_field)
190 if parsed.field_type_str == 'approval_type':
191 revised_approvals = field_helpers.ReviseApprovals(
192 field_id, approver_ids, parsed.survey, config)
193 self.services.config.UpdateConfig(
194 mr.cnxn, mr.project, approval_defs=revised_approvals)
195 if parsed.field_type_str == 'enum_type':
196 self.services.config.UpdateConfig(
197 mr.cnxn, mr.project, well_known_labels=parsed.revised_labels)
198
199 return framework_helpers.FormatAbsoluteURL(
200 mr, urls.ADMIN_LABELS, saved=1, ts=int(time.time()))
201
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200202 # def GetFieldCreate(self, **kwargs):
203 # return self.handler(**kwargs)
204
205 # def PostFieldCreate(self, **kwargs):
206 # return self.handler(**kwargs)
207
Copybara854996b2021-09-07 19:36:02 +0000208
209def FieldNameErrorMessage(field_name, config):
210 """Return an error message for the given field name, or None."""
211 field_name_lower = field_name.lower()
212 if field_name_lower in tracker_constants.RESERVED_PREFIXES:
213 return 'That name is reserved.'
214 if field_name_lower.endswith(
215 tuple(tracker_constants.RESERVED_COL_NAME_SUFFIXES)):
216 return 'That suffix is reserved.'
217
218 for fd in config.field_defs:
219 fn_lower = fd.field_name.lower()
220 if field_name_lower == fn_lower:
221 return 'That name is already in use.'
222 if field_name_lower.startswith(fn_lower + '-'):
223 return 'An existing field name is a prefix of that name.'
224 if fn_lower.startswith(field_name_lower + '-'):
225 return 'That name is a prefix of an existing field name.'
226
227 return None