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