blob: ae607f2b1740dc19d2fe0e595721b142a3ba5ae9 [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 and component owners to view and edit field defs."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
Copybara854996b2021-09-07 19:36:02 +000010import time
11import re
12
13import ezt
14
Copybara854996b2021-09-07 19:36:02 +000015from framework import framework_helpers
16from framework import framework_views
17from framework import permissions
18from framework import servlet
19from framework import urls
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010020from mrproto import tracker_pb2
Copybara854996b2021-09-07 19:36:02 +000021from tracker import field_helpers
22from tracker import tracker_bizobj
23from tracker import tracker_helpers
24from tracker import tracker_views
25
26
27class FieldDetail(servlet.Servlet):
28 """Servlet allowing project owners to view and edit a custom field."""
29
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010030 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
Copybara854996b2021-09-07 19:36:02 +000031 _PAGE_TEMPLATE = 'tracker/field-detail-page.ezt'
32
33 def _GetFieldDef(self, mr):
34 """Get the config and field definition to be viewed or edited."""
35 # TODO(jrobbins): since so many requests get the config object, and
36 # it is usually cached in RAM, just always get it and include it
37 # in the MonorailRequest, mr.
38 config = self.services.config.GetProjectConfig(
39 mr.cnxn, mr.project_id, use_cache=False)
40 field_def = tracker_bizobj.FindFieldDef(mr.field_name, config)
41 if not field_def:
42 self.abort(404, 'custom field not found')
43 return config, field_def
44
45 def AssertBasePermission(self, mr):
46 """Check whether the user has any permission to visit this page.
47
48 Args:
49 mr: commonly used info parsed from the request.
50 """
51 super(FieldDetail, self).AssertBasePermission(mr)
52 _config, field_def = self._GetFieldDef(mr)
53
54 allow_view = permissions.CanViewFieldDef(
55 mr.auth.effective_ids, mr.perms, mr.project, field_def)
56 if not allow_view:
57 raise permissions.PermissionException(
58 'User is not allowed to view this field definition')
59
60 def GatherPageData(self, mr):
61 """Build up a dictionary of data values to use when rendering the page.
62
63 Args:
64 mr: commonly used info parsed from the request.
65
66 Returns:
67 Dict of values used by EZT for rendering the page.
68 """
69 config, field_def = self._GetFieldDef(mr)
70 approval_def, subfields = None, []
71 if field_def.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
72 approval_def = tracker_bizobj.FindApprovalDefByID(
73 field_def.field_id, config)
74 user_views = framework_views.MakeAllUserViews(
75 mr.cnxn, self.services.user, field_def.admin_ids,
76 approval_def.approver_ids)
77 subfields = tracker_bizobj.FindApprovalsSubfields(
78 [field_def.field_id], config)[field_def.field_id]
79 else:
80 user_views = framework_views.MakeAllUserViews(
81 mr.cnxn, self.services.user, field_def.admin_ids,
82 field_def.editor_ids)
83 field_def_view = tracker_views.FieldDefView(
84 field_def, config, user_views=user_views, approval_def=approval_def)
85
86 well_known_issue_types = tracker_helpers.FilterIssueTypes(config)
87
88 allow_edit = permissions.CanEditFieldDef(
89 mr.auth.effective_ids, mr.perms, mr.project, field_def)
90
91 # Right now we do not allow renaming of enum fields.
92 _uneditable_name = field_def.field_type == tracker_pb2.FieldTypes.ENUM_TYPE
93
94 initial_choices = '\n'.join(
95 [choice.name if not choice.docstring else (
96 choice.name + ' = ' + choice.docstring) for
97 choice in field_def_view.choices])
98
99 initial_approvers = ', '.join(sorted([
100 approver_view.email for approver_view in field_def_view.approvers]))
101
102 initial_admins = ', '.join(sorted([
103 uv.email for uv in field_def_view.admins]))
104 initial_editors = ', '.join(
105 sorted([uv.email for uv in field_def_view.editors]))
106
107 return {
108 'admin_tab_mode': servlet.Servlet.PROCESS_TAB_LABELS,
109 'field_def': field_def_view,
110 'allow_edit': ezt.boolean(allow_edit),
111 # TODO(jojwang): update when name changes are actually saved
112 'uneditable_name': ezt.boolean(True),
113 'initial_admins': initial_admins,
114 'initial_editors': initial_editors,
115 'initial_applicable_type': field_def.applicable_type,
116 'initial_applicable_predicate': field_def.applicable_predicate,
117 'initial_approvers': initial_approvers,
118 'initial_choices': initial_choices,
119 'approval_subfields': [fd for fd in subfields if not fd.is_deleted],
120 'well_known_issue_types': well_known_issue_types,
121 }
122
123 def ProcessFormData(self, mr, post_data):
124 """Validate and store the contents of the issues tracker admin page.
125
126 Args:
127 mr: commonly used info parsed from the request.
128 post_data: HTML form data from the request.
129
130 Returns:
131 String URL to redirect the user to, or None if response was already sent.
132 """
133 config, field_def = self._GetFieldDef(mr)
134 allow_edit = permissions.CanEditFieldDef(
135 mr.auth.effective_ids, mr.perms, mr.project, field_def)
136 if not allow_edit:
137 raise permissions.PermissionException(
138 'User is not allowed to delete this field')
139
140 if 'deletefield' in post_data:
141 return self._ProcessDeleteField(mr, config, field_def)
142 elif 'cancel' in post_data:
143 return framework_helpers.FormatAbsoluteURL(
144 mr, urls.ADMIN_LABELS, ts=int(time.time()))
145 else:
146 return self._ProcessEditField(mr, post_data, config, field_def)
147
148
149 def _ProcessDeleteField(self, mr, config, field_def):
150 """The user wants to delete the specified custom field definition."""
151 field_ids = [field_def.field_id]
152 if field_def.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE:
153 for fd in config.field_defs:
154 if fd.approval_id == field_def.field_id:
155 field_ids.append(fd.field_id)
156 self.services.config.SoftDeleteFieldDefs(
157 mr.cnxn, mr.project_id, field_ids)
158
159 return framework_helpers.FormatAbsoluteURL(
160 mr, urls.ADMIN_LABELS, deleted=1, ts=int(time.time()))
161
162 # TODO(jrobbins): add logic to reaper cron task to look for
163 # soft deleted field definitions that have no issues with
164 # any value and hard deleted them.
165
166 def _ProcessEditField(self, mr, post_data, config, field_def):
167 """The user wants to edit this field definition."""
168 # TODO(jrobbins): future feature: editable field names
169
170 parsed = field_helpers.ParseFieldDefRequest(post_data, config)
171
172 admin_ids, admin_str = tracker_helpers.ParsePostDataUsers(
173 mr.cnxn, post_data['admin_names'], self.services.user)
174 editor_ids, editor_str = tracker_helpers.ParsePostDataUsers(
175 mr.cnxn, post_data.get('editor_names', ''), self.services.user)
176
177 field_helpers.ParsedFieldDefAssertions(mr, parsed)
178
179 if not parsed.is_restricted_field:
180 assert not editor_ids, 'Editors are only for restricted fields.'
181
182 if field_def.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
183 assert not (
184 parsed.is_restricted_field), 'Approval fields cannot be restricted.'
185 assert not editor_ids, 'Approval fields cannot have editors.'
186
187 if parsed.approvers_str:
188 approver_ids_dict = self.services.user.LookupUserIDs(
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100189 mr.cnxn,
190 re.split(r'[,;\s]+', parsed.approvers_str),
Copybara854996b2021-09-07 19:36:02 +0000191 autocreate=True)
192 approver_ids = list(set(approver_ids_dict.values()))
193 else:
194 mr.errors.approvers = 'Please provide at least one default approver.'
195
196 if mr.errors.AnyErrors():
197 new_field_def = field_helpers.ReviseFieldDefFromParsed(parsed, field_def)
198
199 new_field_def_view = tracker_views.FieldDefView(
200 new_field_def, config)
201
202 self.PleaseCorrect(
203 mr,
204 field_def=new_field_def_view,
205 initial_applicable_type=parsed.applicable_type,
206 initial_choices=parsed.choices_text,
207 initial_admins=admin_str,
208 initial_editors=editor_str,
209 initial_approvers=parsed.approvers_str,
210 initial_is_restricted_field=parsed.is_restricted_field)
211 return
212
213 self.services.config.UpdateFieldDef(
214 mr.cnxn,
215 mr.project_id,
216 field_def.field_id,
217 applicable_type=parsed.applicable_type,
218 applicable_predicate=parsed.applicable_predicate,
219 is_required=parsed.is_required,
220 is_niche=parsed.is_niche,
221 min_value=parsed.min_value,
222 max_value=parsed.max_value,
223 regex=parsed.regex,
224 needs_member=parsed.needs_member,
225 needs_perm=parsed.needs_perm,
226 grants_perm=parsed.grants_perm,
227 notify_on=parsed.notify_on,
228 is_multivalued=parsed.is_multivalued,
229 date_action=parsed.date_action_str,
230 docstring=parsed.field_docstring,
231 admin_ids=admin_ids,
232 editor_ids=editor_ids,
233 is_restricted_field=parsed.is_restricted_field)
234
235 if field_def.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
236 approval_defs = field_helpers.ReviseApprovals(
237 field_def.field_id, approver_ids, parsed.survey, config)
238 self.services.config.UpdateConfig(
239 mr.cnxn, mr.project, approval_defs=approval_defs)
240
241 if field_def.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
242 self.services.config.UpdateConfig(
243 mr.cnxn, mr.project, well_known_labels=parsed.revised_labels)
244
245 return framework_helpers.FormatAbsoluteURL(
246 mr, urls.FIELD_DETAIL, field=field_def.field_name,
247 saved=1, ts=int(time.time()))
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200248
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100249 def GetFieldDetail(self, **kwargs):
250 return self.handler(**kwargs)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200251
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100252 def PostFieldDetail(self, **kwargs):
253 return self.handler(**kwargs)