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