def _ShouldRevealEmail(auth, project, viewed_email): """Decide whether to publish a user's email address. Args: auth: The AuthData of the user viewing the email addresses. project: The project to which the viewed users belong. viewed_email: The email of the viewed user. Returns: True if email addresses should be published to the logged-in user. """ # Case 1: Anon users don't see anything revealed. if auth.user_pb is None: return False # Case 2: site admins always see unobscured email addresses. if auth.user_pb.is_site_admin: return True # Case 3: Project members see the unobscured email of everyone in a project. if project and framework_bizobj.UserIsInProject(project, auth.effective_ids): return True # Case 4: Do not obscure your own email. if viewed_email and auth.user_pb.email == viewed_email: return True return False
def IsValidIssueOwner(cnxn, project, owner_id, services): """Return True if the given user ID can be an issue owner. Args: cnxn: connection to SQL database. project: the current Project PB. owner_id: the user ID of the proposed issue owner. services: connections to backends. It is OK to have 0 for the owner_id, that simply means that the issue is unassigned. Returns: A pair (valid, err_msg). valid is True if the given user ID can be an issue owner. err_msg is an error message string to display to the user if valid == False, and is None if valid == True. """ # An issue is always allowed to have no owner specified. if owner_id == framework_constants.NO_USER_SPECIFIED: return True, None try: auth = authdata.AuthData.FromUserID(cnxn, owner_id, services) if not framework_bizobj.UserIsInProject(project, auth.effective_ids): return False, 'Issue owner must be a project member' except exceptions.NoSuchUserException: return False, 'Issue owner user ID not found' group_ids = services.usergroup.DetermineWhichUserIDsAreGroups( cnxn, [owner_id]) if owner_id in group_ids: return False, 'Issue owner cannot be a user group' return True, None
def ValidateMemberID(self, cnxn, member_id, project): """Lookup a project member by user_id. Args: cnxn: connection to SQL database. member_id: int user_id, same format as user profile page. project: the current Project PB. Returns: The user ID of the project member. Raises an exception if the username cannot be looked up, or if that user is not in the project. """ if not member_id: self.abort(404, 'project member not specified') member_username = None try: member_username = self.services.user.LookupUserEmail( cnxn, member_id) except exceptions.NoSuchUserException: logging.info('user_id %s not found', member_id) if not member_username: logging.info('There is no such user id %r', member_id) self.abort(404, 'project member not found') if not framework_bizobj.UserIsInProject(project, {member_id}): logging.info('User %r is not a member of %r', member_username, project.project_name) self.abort(404, 'project member not found') return member_id
def FindExtraPerms(project, member_id): """Return a ExtraPerms PB for the given user in the project. Args: project: Project PB for the current project, or None if the user is not currently in a project. member_id: user ID of a project owner, member, or contributor. Returns: An ExtraPerms PB, or None. """ if not project: # TODO(jrobbins): maybe define extra perms for site-wide operations. return None # Users who have no current role cannot have any extra perms. Don't # consider effective_ids (which includes user groups) for this check. if not framework_bizobj.UserIsInProject(project, {member_id}): return None for extra_perms in project.extra_perms: if extra_perms.member_id == member_id: return extra_perms return None
def GetPresentationConfig(self, mc, request): """Return the UI centric pieces of the project config.""" project = self._GetProject(mc, request) with work_env.WorkEnv(mc, self.services) as we: config = we.GetProjectConfig(project.project_id) project_thumbnail_url = tracker_views.LogoView(project).thumbnail_url project_summary = project.summary custom_issue_entry_url = config.custom_issue_entry_url default_query = None saved_queries = None # Only show default query or project saved queries for project # members, in case they contain sensitive information. if framework_bizobj.UserIsInProject(project, mc.auth.effective_ids): default_query = config.member_default_query saved_queries = self.services.features.GetCannedQueriesByProjectID( mc.cnxn, project.project_id) return project_objects_pb2.PresentationConfig( project_thumbnail_url=project_thumbnail_url, project_summary=project_summary, custom_issue_entry_url=custom_issue_entry_url, default_query=default_query, saved_queries=converters.IngestSavedQueries( mc.cnxn, self.services.project, saved_queries))
def _CalcDefaultQuery(self): """When URL has no q= param, return the default for members or ''.""" if (self.can == 2 and self.project and self.auth.effective_ids and framework_bizobj.UserIsInProject( self.project, self.auth.effective_ids) and self.config): return self.config.member_default_query else: return ''
def IssueListURL(mr, config, query_string=None): """Make an issue list URL for non-members or members.""" url = '/p/%s%s' % (mr.project_name, urls.ISSUE_LIST) if query_string: url += '?' + query_string elif framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids): if config and config.member_default_query: url += '?q=' + urllib.quote_plus(config.member_default_query) return url
def ValidateCustomField(mr, project, services, field_def, field_val): """Validate one custom field value and return an error string or None.""" if field_def.field_type == tracker_pb2.FieldTypes.INT_TYPE: if (field_def.min_value is not None and field_val.int_value < field_def.min_value): return 'Value must be >= %d' % field_def.min_value if (field_def.max_value is not None and field_val.int_value > field_def.max_value): return 'Value must be <= %d' % field_def.max_value elif field_def.field_type == tracker_pb2.FieldTypes.STR_TYPE: if field_def.regex and field_val.str_value: try: regex = re.compile(field_def.regex) if not regex.match(field_val.str_value): return 'Value must match regular expression: %s' % field_def.regex except re.error: logging.info('Failed to process regex %r with value %r. Allowing.', field_def.regex, field_val.str_value) return None elif field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE: field_val_user = services.user.GetUser(mr.cnxn, field_val.user_id) auth = authdata.AuthData.FromUser(mr.cnxn, field_val_user, services) if auth.user_pb.user_id == INVALID_USER_ID: return 'User not found' if field_def.needs_member: user_value_in_project = framework_bizobj.UserIsInProject( project, auth.effective_ids) if not user_value_in_project: return 'User must be a member of the project' if field_def.needs_perm: user_perms = permissions.GetPermissions( auth.user_pb, auth.effective_ids, project) has_perm = user_perms.CanUsePerm( field_def.needs_perm, auth.effective_ids, project, []) if not has_perm: return 'User must have permission "%s"' % field_def.needs_perm return None elif field_def.field_type == tracker_pb2.FieldTypes.DATE_TYPE: # TODO(jrobbins): date validation pass elif field_def.field_type == tracker_pb2.FieldTypes.URL_TYPE: if field_val.url_value: if not (validate.IsValidURL(field_val.url_value) or autolink_constants.IS_A_SHORT_LINK_RE.match( field_val.url_value) or autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE.match( field_val.url_value) or autolink_constants.IS_IMPLIED_LINK_RE.match( field_val.url_value)): return 'Value must be a valid url' return None
def GetPermissions(user, effective_ids, project): """Return a permission set appropriate for the user and project. Args: user: The User PB for the signed-in user, or None for anon users. effective_ids: set of int user IDs for the current user and all user groups that s/he is a member of. This will be an empty set for anonymous users. project: either a Project protobuf, or None for a page whose scope is wider than a single project. Returns: a PermissionSet object for the current user and project (or for site-wide operations if project is None). If an exact match for the user's role and project status is found, that is returned. Otherwise, we look for permissions for the user's role that is not specific to any project status, or not specific to any project access level. If neither of those are defined, we give the user an empty permission set. """ # Site admins get ADMIN_PERMISSIONSET regardless of groups or projects. if user and user.is_site_admin: return ADMIN_PERMISSIONSET # Grant the borg job permission to view/edit groups if user and user.email == settings.borg_service_account: return GROUP_IMPORT_BORG_PERMISSIONSET # Anon users don't need to accumulate anything. if not effective_ids: role, status, access = _GetPermissionKey(None, project) return _LookupPermset(role, status, access) effective_perms = set() consider_restrictions = True # Check for signed-in user with no roles in the current project. if not project or not framework_bizobj.UserIsInProject( project, effective_ids): role, status, access = _GetPermissionKey(None, project) return _LookupPermset(USER_ROLE, status, access) # Signed-in user gets the union of all his/her PermissionSets from the table. for user_id in effective_ids: role, status, access = _GetPermissionKey(user_id, project) role_perms = _LookupPermset(role, status, access) # Accumulate a union of all the user's permissions. effective_perms.update(role_perms.perm_names) # If any role allows the user to ignore restriction labels, then # ignore them overall. if not role_perms.consider_restrictions: consider_restrictions = False return PermissionSet( effective_perms, consider_restrictions=consider_restrictions)
def CanViewTemplate(effective_ids, perms, project, template): """Return True if a user can view the given issue template.""" if not effective_ids.isdisjoint(template.admin_ids): return True # template admins can view that template. # Members-only templates are only shown to members, other templates are # shown to any user that is generally allowed to view project content. if template.members_only: return framework_bizobj.UserIsInProject(project, effective_ids) else: return perms.CanUsePerm(VIEW, effective_ids, project, [])
def GatherCaptchaData(self, mr): """If this page needs a captcha, return captcha info for use in EZT.""" if (mr.project and framework_bizobj.UserIsInProject( mr.project, mr.auth.effective_ids)): # Don't show users CAPTCHAs within their own projects. return {'show_captcha': ezt.boolean(False)} show_captcha = any( actionlimit.NeedCaptcha(mr.auth.user_pb, action_type) for action_type in self._CAPTCHA_ACTION_TYPES) logging.info('show_captcha: %r', show_captcha) return {'show_captcha': ezt.boolean(show_captcha)}
def _ValidateOneCustomField(mr, services, field_def, field_val): """Validate one custom field value and return an error string or None.""" if field_def.field_type == tracker_pb2.FieldTypes.INT_TYPE: if (field_def.min_value is not None and field_val.int_value < field_def.min_value): return 'Value must be >= %d' % field_def.min_value if (field_def.max_value is not None and field_val.int_value > field_def.max_value): return 'Value must be <= %d' % field_def.max_value elif field_def.field_type == tracker_pb2.FieldTypes.STR_TYPE: if field_def.regex and field_val.str_value: try: regex = re.compile(field_def.regex) if not regex.match(field_val.str_value): return 'Value must match regular expression: %s' % field_def.regex except re.error: logging.info( 'Failed to process regex %r with value %r. Allowing.', field_def.regex, field_val.str_value) return None elif field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE: if field_val.user_id == INVALID_USER_ID: return 'User not found' if field_def.needs_member: auth = monorailrequest.AuthData.FromUserID(mr.cnxn, field_val.user_id, services) user_value_in_project = framework_bizobj.UserIsInProject( mr.project, auth.effective_ids) if not user_value_in_project: return 'User must be a member of the project' if field_def.needs_perm: field_val_user = services.user.GetUser(mr.cnxn, field_val.user_id) user_perms = permissions.GetPermissions( field_val_user, auth.effective_ids, mr.project) has_perm = user_perms.CanUsePerm(field_def.needs_perm, auth.effective_ids, mr.project, []) if not has_perm: return 'User must have permission "%s"' % field_def.needs_perm elif field_def.field_type == tracker_pb2.FieldTypes.DATE_TYPE: # TODO(jrobbins): date validation pass return None
def testUserIsInProject(self): p = project_pb2.Project() self.assertFalse(framework_bizobj.UserIsInProject(p, {10})) self.assertFalse(framework_bizobj.UserIsInProject(p, set())) p.owner_ids.extend([1, 2, 3]) p.committer_ids.extend([4, 5, 6]) p.contributor_ids.extend([7, 8, 9]) self.assertTrue(framework_bizobj.UserIsInProject(p, {1})) self.assertTrue(framework_bizobj.UserIsInProject(p, {4})) self.assertTrue(framework_bizobj.UserIsInProject(p, {7})) self.assertFalse(framework_bizobj.UserIsInProject(p, {10})) # Membership via group membership self.assertTrue(framework_bizobj.UserIsInProject(p, {10, 4})) # Membership via several group memberships self.assertTrue(framework_bizobj.UserIsInProject(p, {1, 4})) # Several irrelevant group memberships self.assertFalse(framework_bizobj.UserIsInProject(p, {10, 11, 12}))
def GatherHelpData(self, mr, page_data): """Return a dict of values to drive on-page user help. Args: mr: common information parsed from the HTTP request. page_data: Dictionary of base and page template data. Returns: A dict of values to drive on-page user help, to be added to page_data. """ help_data = super(PeopleList, self).GatherHelpData(mr, page_data) if (mr.auth.user_id and not framework_bizobj.UserIsInProject( mr.project, mr.auth.effective_ids) and 'how_to_join_project' not in mr.auth.user_pb.dismissed_cues): help_data['cue'] = 'how_to_join_project' return help_data
def GatherHelpData(self, mr, page_data): """Return a dict of values to drive on-page user help. Args: mr: common information parsed from the HTTP request. page_data: Dictionary of base and page template data. Returns: A dict of values to drive on-page user help, to be added to page_data. """ help_data = super(PeopleList, self).GatherHelpData(mr, page_data) with work_env.WorkEnv(mr, self.services) as we: userprefs = we.GetUserPrefs(mr.auth.user_id) dismissed = [pv.name for pv in userprefs.prefs if pv.value == 'true'] if (mr.auth.user_id and not framework_bizobj.UserIsInProject( mr.project, mr.auth.effective_ids) and 'how_to_join_project' not in dismissed): help_data['cue'] = 'how_to_join_project' return help_data
def CheckCaptcha(self, mr, post_data): """Check the provided CAPTCHA solution and add an error if it is wrong.""" if (mr.project and framework_bizobj.UserIsInProject( mr.project, mr.auth.effective_ids)): logging.info('Project member is exempt from CAPTCHA') return # Don't check a user's actions within their own projects. if not any( actionlimit.NeedCaptcha(mr.auth.user_pb, action_type) for action_type in self._CAPTCHA_ACTION_TYPES): logging.info('No CAPTCHA was required') return # no captcha was needed. remote_ip = mr.request.remote_addr captcha_response = post_data.get('g-recaptcha-response') correct, _msg = captcha.Verify(remote_ip, captcha_response) if correct: logging.info('CAPTCHA was solved') else: logging.info('BZzzz! Bad captcha solution.') mr.errors.captcha = 'Captcha check failed.'
def FindExtraPerms(project, member_id): """Return a ExtraPerms PB for the given user in the project. Args: project: Project PB for the current project, or None if the user is not currently in a project. member_id: user ID of a project owner, member, or contributor. Returns: A pair (idx, extra_perms). * If project is None or member_id is not part of the project, both are None. * If member_id has no extra_perms, extra_perms is None, and idx points to the position where it should go to keep the ExtraPerms sorted in project. * Otherwise, idx is the position of member_id in the project's extra_perms, and extra_perms is an ExtraPerms PB. """ class ExtraPermsView(object): def __len__(self): return len(project.extra_perms) def __getitem__(self, idx): return project.extra_perms[idx].member_id if not project: # TODO(jrobbins): maybe define extra perms for site-wide operations. return None, None # Users who have no current role cannot have any extra perms. Don't # consider effective_ids (which includes user groups) for this check. if not framework_bizobj.UserIsInProject(project, {member_id}): return None, None extra_perms_view = ExtraPermsView() # Find the index of the first extra_perms.member_id greater than or equal to # member_id. idx = bisect.bisect_left(extra_perms_view, member_id) if idx >= len(project.extra_perms) or extra_perms_view[idx] > member_id: return idx, None return idx, project.extra_perms[idx]
def GetTemplateInfoFromParsed(mr, services, parsed, config): """Get Template field info and PBs from a ParsedTemplate.""" admin_ids, _ = tracker_helpers.ParseAdminUsers(mr.cnxn, parsed.admin_str, services.user) owner_id = 0 if parsed.owner_str: try: user_id = services.user.LookupUserID(mr.cnxn, parsed.owner_str) auth = authdata.AuthData.FromUserID(mr.cnxn, user_id, services) if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids): owner_id = user_id else: mr.errors.owner = 'User is not a member of this project.' except exceptions.NoSuchUserException: mr.errors.owner = 'Owner not found.' component_ids = tracker_helpers.LookupComponentIDs(parsed.component_paths, config, mr.errors) # TODO(jojwang): monorail:4678 Process phase field values. phase_field_val_strs = {} field_values = field_helpers.ParseFieldValues(mr.cnxn, services.user, parsed.field_val_strs, phase_field_val_strs, config) for fv in field_values: logging.info('field_value is %r: %r', fv.field_id, tracker_bizobj.GetFieldValue(fv, {})) phases = [] approvals = [] if parsed.add_approvals: phases, approvals = _GetPhasesAndApprovalsFromParsed( mr, parsed.phase_names, parsed.approvals_to_phase_idx, parsed.required_approval_ids) return admin_ids, owner_id, component_ids, field_values, phases, approvals
def CountRateLimitedActions(self, mr, action_counts): """Count attempted actions against non-member's action limits. Note that users can take any number of actions in their own projects. Args: mr: commonly used info parsed from the request. action_counts: {action_type: delta, ... } a dictionary mapping action type constants to the number of times that action was performed during the current request (usually 1). """ if (mr.project and framework_bizobj.UserIsInProject( mr.project, mr.auth.effective_ids)): # Don't count a user's actions within their own projects... return for action_type in action_counts: actionlimit.CountAction(mr.auth.user_pb, action_type, delta=action_counts[action_type]) self.services.user.UpdateUser(mr.cnxn, mr.auth.user_id, mr.auth.user_pb)
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. """ with mr.profiler.Phase('getting config'): config = self.services.config.GetProjectConfig( mr.cnxn, mr.project_id) # In addition to checking perms, we adjust some default field values for # project members. is_member = framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids) page_perms = self.MakePagePerms( mr, None, permissions.CREATE_ISSUE, permissions.SET_STAR, permissions.EDIT_ISSUE, permissions.EDIT_ISSUE_SUMMARY, permissions.EDIT_ISSUE_STATUS, permissions.EDIT_ISSUE_OWNER, permissions.EDIT_ISSUE_CC) with work_env.WorkEnv(mr, self.services) as we: userprefs = we.GetUserPrefs(mr.auth.user_id) code_font = any( pref for pref in userprefs.prefs if pref.name == 'code_font' and pref.value == 'true') template = self._GetTemplate(mr.cnxn, config, mr.template_name, is_member) if template.summary: initial_summary = template.summary initial_summary_must_be_edited = template.summary_must_be_edited else: initial_summary = PLACEHOLDER_SUMMARY initial_summary_must_be_edited = True if template.status: initial_status = template.status elif is_member: initial_status = 'Accepted' else: initial_status = 'New' # not offering meta, only used in hidden field. component_paths = [] for component_id in template.component_ids: component_paths.append( tracker_bizobj.FindComponentDefByID(component_id, config).path) initial_components = ', '.join(component_paths) if template.owner_id: initial_owner = framework_views.MakeUserView( mr.cnxn, self.services.user, template.owner_id) elif template.owner_defaults_to_member and page_perms.EditIssue: initial_owner = mr.auth.user_view else: initial_owner = None if initial_owner: initial_owner_name = initial_owner.email owner_avail_state = initial_owner.avail_state owner_avail_message_short = initial_owner.avail_message_short else: initial_owner_name = '' owner_avail_state = None owner_avail_message_short = None # Check whether to allow attachments from the entry page allow_attachments = tracker_helpers.IsUnderSoftAttachmentQuota( mr.project) config_view = tracker_views.ConfigView(mr, self.services, config, template) # If the user followed a link that specified the template name, make sure # that it is also in the menu as the current choice. # TODO(jeffcarp): Unit test this. config_view.template_view.can_view = ezt.boolean(True) # TODO(jeffcarp): Unit test this. offer_templates = len(config_view.template_names) > 1 restrict_to_known = config.restrict_to_known enum_field_name_set = { fd.field_name.lower() for fd in config.field_defs if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and not fd.is_deleted } # TODO(jrobbins): restrictions link_or_template_labels = mr.GetListParam('labels', template.labels) labels = [ lab for lab in link_or_template_labels if not tracker_bizobj.LabelIsMaskedByField(lab, enum_field_name_set) ] # Corp-mode users automatically add R-V-G. with work_env.WorkEnv(mr, self.services) as we: userprefs = we.GetUserPrefs(mr.auth.user_id) corp_mode = any( up.name == 'restrict_new_issues' and up.value == 'true' for up in userprefs.prefs) if corp_mode: if not any(lab.lower().startswith('restrict-view-') for lab in labels): labels.append(CORP_RESTRICTION_LABEL) field_user_views = tracker_views.MakeFieldUserViews( mr.cnxn, template, self.services.user) approval_ids = [av.approval_id for av in template.approval_values] field_views = tracker_views.MakeAllFieldValueViews( config, link_or_template_labels, [], template.field_values, field_user_views, parent_approval_ids=approval_ids, phases=template.phases) # TODO(jojwang): monorail:6305, remove this hack when Edit perms for field # values are implemented. field_views = [ view for view in field_views if view.field_name.lower() not in RESTRICTED_FLT_FIELDS ] # TODO(jrobbins): remove "or []" after next release. (prechecked_approvals, required_approval_ids, phases) = issue_tmpl_helpers.GatherApprovalsPageData( template.approval_values or [], template.phases, config) approvals = [ view for view in field_views if view.field_id in approval_ids ] page_data = { 'issue_tab_mode': 'issueEntry', 'initial_summary': initial_summary, 'template_summary': initial_summary, 'clear_summary_on_click': ezt.boolean(initial_summary_must_be_edited and 'initial_summary' not in mr.form_overrides), 'must_edit_summary': ezt.boolean(initial_summary_must_be_edited), 'initial_description': template.content, 'template_name': template.name, 'component_required': ezt.boolean(template.component_required), 'initial_status': initial_status, 'initial_owner': initial_owner_name, 'owner_avail_state': owner_avail_state, 'owner_avail_message_short': owner_avail_message_short, 'initial_components': initial_components, 'initial_cc': '', 'initial_blocked_on': '', 'initial_blocking': '', 'initial_hotlists': '', 'labels': labels, 'fields': field_views, 'any_errors': ezt.boolean(mr.errors.AnyErrors()), 'page_perms': page_perms, 'allow_attachments': ezt.boolean(allow_attachments), 'max_attach_size': template_helpers.BytesKbOrMb( framework_constants.MAX_POST_BODY_SIZE), 'offer_templates': ezt.boolean(offer_templates), 'config': config_view, 'restrict_to_known': ezt.boolean(restrict_to_known), 'is_member': ezt.boolean(is_member), 'code_font': ezt.boolean(code_font), # The following are necessary for displaying phases that come with # this template. These are read-only. 'allow_edit': ezt.boolean(False), 'initial_phases': phases, 'approvals': approvals, 'prechecked_approvals': prechecked_approvals, 'required_approval_ids': required_approval_ids, # See monorail:4692 and the use of PHASES_WITH_MILESTONES # in elements/flt/mr-launch-overview/mr-phase.js 'issue_phase_names': list({ phase.name.lower() for phase in phases if phase.name in PHASES_WITH_MILESTONES }), } return page_data
def ProcessFormData(self, mr, post_data): """Process the issue entry form. Args: mr: commonly used info parsed from the request. post_data: The post_data dict for the current request. Returns: String URL to redirect the user to after processing. """ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) parsed = tracker_helpers.ParseIssueRequest(mr.cnxn, post_data, self.services, mr.errors, mr.project_name) bounce_labels = parsed.labels[:] bounce_fields = tracker_views.MakeBounceFieldValueViews( parsed.fields.vals, parsed.fields.phase_vals, config) field_helpers.ShiftEnumFieldsIntoLabels(parsed.labels, parsed.labels_remove, parsed.fields.vals, parsed.fields.vals_remove, config) is_member = framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids) template = self._GetTemplate(mr.cnxn, config, parsed.template_name, is_member) (approval_values, phases) = issue_tmpl_helpers.FilterApprovalsAndPhases( template.approval_values or [], template.phases, config) phase_ids_by_name = { phase.name.lower(): [phase.phase_id] for phase in template.phases } field_values = field_helpers.ParseFieldValues( mr.cnxn, self.services.user, parsed.fields.vals, parsed.fields.phase_vals, config, phase_ids_by_name=phase_ids_by_name) labels = _DiscardUnusedTemplateLabelPrefixes(parsed.labels) component_ids = tracker_helpers.LookupComponentIDs( parsed.components.paths, config, mr.errors) # TODO(jrobbins): consider captcha 3 score in API if not parsed.summary.strip() or parsed.summary == PLACEHOLDER_SUMMARY: mr.errors.summary = 'Summary is required' if not parsed.comment.strip(): mr.errors.comment = 'A description is required' if len(parsed.comment) > tracker_constants.MAX_COMMENT_CHARS: mr.errors.comment = 'Comment is too long' if len(parsed.summary) > tracker_constants.MAX_SUMMARY_CHARS: mr.errors.summary = 'Summary is too long' if _MatchesTemplate(parsed.comment, template): mr.errors.comment = 'Template must be filled out.' if parsed.users.owner_id is None: mr.errors.owner = 'Invalid owner username' else: valid, msg = tracker_helpers.IsValidIssueOwner( mr.cnxn, mr.project, parsed.users.owner_id, self.services) if not valid: mr.errors.owner = msg if None in parsed.users.cc_ids: mr.errors.cc = 'Invalid Cc username' field_helpers.ValidateCustomFields(mr, self.services, field_values, config, mr.errors) hotlist_pbs = ProcessParsedHotlistRefs(mr, self.services, parsed.hotlists.hotlist_refs) if not mr.errors.AnyErrors(): with work_env.WorkEnv(mr, self.services) as we: try: if parsed.attachments: new_bytes_used = tracker_helpers.ComputeNewQuotaBytesUsed( mr.project, parsed.attachments) # TODO(jrobbins): Make quota be calculated and stored as # part of applying the comment. self.services.project.UpdateProject( mr.cnxn, mr.project.project_id, attachment_bytes_used=new_bytes_used) marked_description = tracker_helpers.MarkupDescriptionOnInput( parsed.comment, template.content) has_star = 'star' in post_data and post_data['star'] == '1' if approval_values: _AttachDefaultApprovers(config, approval_values) issue, _ = we.CreateIssue( mr.project_id, parsed.summary, parsed.status, parsed.users.owner_id, parsed.users.cc_ids, labels, field_values, component_ids, marked_description, blocked_on=parsed.blocked_on.iids, blocking=parsed.blocking.iids, attachments=parsed.attachments, approval_values=approval_values, phases=phases) if has_star: we.StarIssue(issue, True) if hotlist_pbs: hotlist_ids = { hotlist.hotlist_id for hotlist in hotlist_pbs } issue_tuple = (issue.issue_id, mr.auth.user_id, int(time.time()), '') self.services.features.AddIssueToHotlists( mr.cnxn, hotlist_ids, issue_tuple, self.services.issue, self.services.chart) except tracker_helpers.OverAttachmentQuota: mr.errors.attachments = 'Project attachment quota exceeded.' mr.template_name = parsed.template_name if mr.errors.AnyErrors(): self.PleaseCorrect( mr, initial_summary=parsed.summary, initial_status=parsed.status, initial_owner=parsed.users.owner_username, initial_cc=', '.join(parsed.users.cc_usernames), initial_components=', '.join(parsed.components.paths), initial_comment=parsed.comment, labels=bounce_labels, fields=bounce_fields, template_name=parsed.template_name, initial_blocked_on=parsed.blocked_on.entered_str, initial_blocking=parsed.blocking.entered_str, initial_hotlists=parsed.hotlists.entered_str, component_required=ezt.boolean(template.component_required)) return # format a redirect url return framework_helpers.FormatAbsoluteURL(mr, urls.ISSUE_DETAIL, id=issue.local_id)
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. """ with self.profiler.Phase('getting config'): config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) # In addition to checking perms, we adjust some default field values for # project members. is_member = framework_bizobj.UserIsInProject( mr.project, mr.auth.effective_ids) page_perms = self.MakePagePerms( mr, None, permissions.CREATE_ISSUE, permissions.SET_STAR, permissions.EDIT_ISSUE, permissions.EDIT_ISSUE_SUMMARY, permissions.EDIT_ISSUE_STATUS, permissions.EDIT_ISSUE_OWNER, permissions.EDIT_ISSUE_CC) wkp = _SelectTemplate(mr.template_name, config, is_member) if wkp.summary: initial_summary = wkp.summary initial_summary_must_be_edited = wkp.summary_must_be_edited else: initial_summary = PLACEHOLDER_SUMMARY initial_summary_must_be_edited = True if wkp.status: initial_status = wkp.status elif is_member: initial_status = 'Accepted' else: initial_status = 'New' # not offering meta, only used in hidden field. component_paths = [] for component_id in wkp.component_ids: component_paths.append( tracker_bizobj.FindComponentDefByID(component_id, config).path) initial_components = ', '.join(component_paths) if wkp.owner_id: initial_owner = framework_views.MakeUserView( mr.cnxn, self.services.user, wkp.owner_id) elif wkp.owner_defaults_to_member and page_perms.EditIssue: initial_owner = mr.auth.user_view else: initial_owner = None if initial_owner: initial_owner_name = initial_owner.email owner_avail_state = initial_owner.avail_state owner_avail_message_short = initial_owner.avail_message_short else: initial_owner_name = '' owner_avail_state = None owner_avail_message_short = None # Check whether to allow attachments from the entry page allow_attachments = tracker_helpers.IsUnderSoftAttachmentQuota(mr.project) config_view = tracker_views.ConfigView(mr, self.services, config) # If the user followed a link that specified the template name, make sure # that it is also in the menu as the current choice. for template_view in config_view.templates: if template_view.name == mr.template_name: template_view.can_view = ezt.boolean(True) offer_templates = len(list( tmpl for tmpl in config_view.templates if tmpl.can_view)) > 1 restrict_to_known = config.restrict_to_known field_name_set = {fd.field_name.lower() for fd in config.field_defs if not fd.is_deleted} # TODO(jrobbins): restrictions link_or_template_labels = mr.GetListParam('labels', wkp.labels) labels = [lab for lab in link_or_template_labels if not tracker_bizobj.LabelIsMaskedByField(lab, field_name_set)] field_user_views = tracker_views.MakeFieldUserViews( mr.cnxn, wkp, self.services.user) field_views = [ tracker_views.MakeFieldValueView( fd, config, link_or_template_labels, [], wkp.field_values, field_user_views) # TODO(jrobbins): field-level view restrictions, display options for fd in config.field_defs if not fd.is_deleted] page_data = { 'issue_tab_mode': 'issueEntry', 'initial_summary': initial_summary, 'template_summary': initial_summary, 'clear_summary_on_click': ezt.boolean( initial_summary_must_be_edited and 'initial_summary' not in mr.form_overrides), 'must_edit_summary': ezt.boolean(initial_summary_must_be_edited), 'initial_description': wkp.content, 'template_name': wkp.name, 'component_required': ezt.boolean(wkp.component_required), 'initial_status': initial_status, 'initial_owner': initial_owner_name, 'owner_avail_state': owner_avail_state, 'owner_avail_message_short': owner_avail_message_short, 'initial_components': initial_components, 'initial_cc': '', 'initial_blocked_on': '', 'initial_blocking': '', 'labels': labels, 'fields': field_views, 'any_errors': ezt.boolean(mr.errors.AnyErrors()), 'page_perms': page_perms, 'allow_attachments': ezt.boolean(allow_attachments), 'max_attach_size': template_helpers.BytesKbOrMb( framework_constants.MAX_POST_BODY_SIZE), 'offer_templates': ezt.boolean(offer_templates), 'config': config_view, 'restrict_to_known': ezt.boolean(restrict_to_known), } return page_data
def ComputeIssueChangeAddressPermList( cnxn, ids_to_consider, project, issue, services, omit_addrs, users_by_id, pref_check_function=lambda u: u.notify_issue_change): """Return a list of user email addresses to notify of an issue change. User email addresses are determined by looking up the given user IDs in the given users_by_id dict. Args: cnxn: connection to SQL database. ids_to_consider: list of user IDs for users interested in this issue. project: Project PB for the project containing this issue. issue: Issue PB for the issue that was updated. services: Services. omit_addrs: set of strings for email addresses to not notify because they already know. users_by_id: dict {user_id: user_view} user info. pref_check_function: optional function to use to check if a certain User PB has a preference set to receive the email being sent. It defaults to "If I am in the issue's owner or cc field", but it can be set to check "If I starred the issue." Returns: A list of AddrPerm objects. """ memb_addr_perm_list = [] logging.info('Considering %r ', ids_to_consider) all_user_prefs = services.user.GetUsersPrefs(cnxn, ids_to_consider) for user_id in ids_to_consider: if user_id == framework_constants.NO_USER_SPECIFIED: continue user = services.user.GetUser(cnxn, user_id) # Notify people who have a pref set, or if they have no User PB # because the pref defaults to True. if user and not pref_check_function(user): logging.info('Not notifying %r: user preference', user.email) continue # TODO(jrobbins): doing a bulk operation would reduce DB load. auth = authdata.AuthData.FromUserID(cnxn, user_id, services) perms = permissions.GetPermissions(user, auth.effective_ids, project) config = services.config.GetProjectConfig(cnxn, project.project_id) granted_perms = tracker_bizobj.GetGrantedPerms( issue, auth.effective_ids, config) if not permissions.CanViewIssue( auth.effective_ids, perms, project, issue, granted_perms=granted_perms): logging.info('Not notifying %r: user cannot view issue', user.email) continue addr = users_by_id[user_id].email if addr in omit_addrs: logging.info('Not notifying %r: user already knows', user.email) continue recipient_is_member = bool(framework_bizobj.UserIsInProject( project, auth.effective_ids)) reply_perm = REPLY_NOT_ALLOWED if project.process_inbound_email: if permissions.CanEditIssue(auth.effective_ids, perms, project, issue): reply_perm = REPLY_MAY_UPDATE elif permissions.CanCommentIssue( auth.effective_ids, perms, project, issue): reply_perm = REPLY_MAY_COMMENT memb_addr_perm_list.append( AddrPerm(recipient_is_member, addr, user, reply_perm, all_user_prefs[user_id])) logging.info('For %s %s, will notify: %r', project.project_name, issue.local_id, [ap.address for ap in memb_addr_perm_list]) return memb_addr_perm_list
def _MakeEmailTasks( self, cnxn, project, issue, config, old_owner_id, users_by_id, all_comments, comment, starrer_ids, contributor_could_view, hostport, omit_ids, perms): """Formulate emails to be sent.""" detail_url = framework_helpers.IssueCommentURL( hostport, project, issue.local_id, seq_num=comment.sequence) # TODO(jrobbins): avoid the need to make a MonorailRequest object. mr = monorailrequest.MonorailRequest(self.services) mr.project_name = project.project_name mr.project = project mr.perms = perms # We do not autolink in the emails, so just use an empty # registry of autolink rules. # TODO(jrobbins): offer users an HTML email option w/ autolinks. autolinker = autolink.Autolink() was_created = ezt.boolean(comment.sequence == 0) email_data = { # Pass open_related and closed_related into this method and to # the issue view so that we can show it on new issue email. 'issue': tracker_views.IssueView(issue, users_by_id, config), 'summary': issue.summary, 'comment': tracker_views.IssueCommentView( project.project_name, comment, users_by_id, autolinker, {}, mr, issue), 'comment_text': comment.content, 'detail_url': detail_url, 'was_created': was_created, } # Generate three versions of email body: link-only is just the link, # non-members see some obscured email addresses, and members version has # all full email addresses exposed. body_link_only = self.link_only_email_template.GetResponse( {'detail_url': detail_url, 'was_created': was_created}) body_for_non_members = self.email_template.GetResponse(email_data) framework_views.RevealAllEmails(users_by_id) email_data['comment'] = tracker_views.IssueCommentView( project.project_name, comment, users_by_id, autolinker, {}, mr, issue) body_for_members = self.email_template.GetResponse(email_data) logging.info('link-only body is:\n%r' % body_link_only) logging.info('body for non-members is:\n%r' % body_for_non_members) logging.info('body for members is:\n%r' % body_for_members) commenter_email = users_by_id[comment.user_id].email omit_addrs = set([commenter_email] + [users_by_id[omit_id].email for omit_id in omit_ids]) auth = authdata.AuthData.FromUserID( cnxn, comment.user_id, self.services) commenter_in_project = framework_bizobj.UserIsInProject( project, auth.effective_ids) noisy = tracker_helpers.IsNoisy(len(all_comments) - 1, len(starrer_ids)) # Give each user a bullet-list of all the reasons that apply for that user. group_reason_list = notify_reasons.ComputeGroupReasonList( cnxn, self.services, project, issue, config, users_by_id, omit_addrs, contributor_could_view, noisy=noisy, starrer_ids=starrer_ids, old_owner_id=old_owner_id, commenter_in_project=commenter_in_project) commenter_view = users_by_id[comment.user_id] detail_url = framework_helpers.FormatAbsoluteURLForDomain( hostport, issue.project_name, urls.ISSUE_DETAIL, id=issue.local_id) email_tasks = notify_helpers.MakeBulletedEmailWorkItems( group_reason_list, issue, body_link_only, body_for_non_members, body_for_members, project, hostport, commenter_view, detail_url, seq_num=comment.sequence) return email_tasks
def _BulkEditEmailTasks( self, cnxn, issues, old_owner_ids, omit_addrs, project, non_private_issues, users_by_id, ids_in_issues, starrers, commenter_view, hostport, comment_text, amendments, config): """Generate Email PBs to notify interested users after a bulk edit.""" # 1. Get the user IDs of everyone who could be notified, # and make all their user proxies. Also, build a dictionary # of all the users to notify and the issues that they are # interested in. Also, build a dictionary of additional email # addresses to notify and the issues to notify them of. users_by_id = {} ids_to_notify_of_issue = {} additional_addrs_to_notify_of_issue = collections.defaultdict(list) users_to_queries = notify_reasons.GetNonOmittedSubscriptions( cnxn, self.services, [project.project_id], {}) config = self.services.config.GetProjectConfig( cnxn, project.project_id) for issue, old_owner_id in zip(issues, old_owner_ids): issue_participants = set( [tracker_bizobj.GetOwnerId(issue), old_owner_id] + tracker_bizobj.GetCcIds(issue)) # users named in user-value fields that notify. for fd in config.field_defs: issue_participants.update( notify_reasons.ComputeNamedUserIDsToNotify(issue.field_values, fd)) for user_id in ids_in_issues[issue.local_id]: # TODO(jrobbins): implement batch GetUser() for speed. if not user_id: continue auth = authdata.AuthData.FromUserID( cnxn, user_id, self.services) if (auth.user_pb.notify_issue_change and not auth.effective_ids.isdisjoint(issue_participants)): ids_to_notify_of_issue.setdefault(user_id, []).append(issue) elif (auth.user_pb.notify_starred_issue_change and user_id in starrers[issue.local_id]): # Skip users who have starred issues that they can no longer view. starrer_perms = permissions.GetPermissions( auth.user_pb, auth.effective_ids, project) granted_perms = tracker_bizobj.GetGrantedPerms( issue, auth.effective_ids, config) starrer_can_view = permissions.CanViewIssue( auth.effective_ids, starrer_perms, project, issue, granted_perms=granted_perms) if starrer_can_view: ids_to_notify_of_issue.setdefault(user_id, []).append(issue) logging.info( 'ids_to_notify_of_issue[%s] = %s', user_id, [i.local_id for i in ids_to_notify_of_issue.get(user_id, [])]) # Find all subscribers that should be notified. subscribers_to_consider = notify_reasons.EvaluateSubscriptions( cnxn, issue, users_to_queries, self.services, config) for sub_id in subscribers_to_consider: auth = authdata.AuthData.FromUserID(cnxn, sub_id, self.services) sub_perms = permissions.GetPermissions( auth.user_pb, auth.effective_ids, project) granted_perms = tracker_bizobj.GetGrantedPerms( issue, auth.effective_ids, config) sub_can_view = permissions.CanViewIssue( auth.effective_ids, sub_perms, project, issue, granted_perms=granted_perms) if sub_can_view: ids_to_notify_of_issue.setdefault(sub_id, []) if issue not in ids_to_notify_of_issue[sub_id]: ids_to_notify_of_issue[sub_id].append(issue) if issue in non_private_issues: for notify_addr in issue.derived_notify_addrs: additional_addrs_to_notify_of_issue[notify_addr].append(issue) # 2. Compose an email specifically for each user, and one email to each # notify_addr with all the issues that it. # Start from non-members first, then members to reveal email addresses. email_tasks = [] needed_user_view_ids = [uid for uid in ids_to_notify_of_issue if uid not in users_by_id] users_by_id.update(framework_views.MakeAllUserViews( cnxn, self.services.user, needed_user_view_ids)) member_ids_to_notify_of_issue = {} non_member_ids_to_notify_of_issue = {} member_additional_addrs = {} non_member_additional_addrs = {} addr_to_addrperm = {} # {email_address: AddrPerm object} all_user_prefs = self.services.user.GetUsersPrefs( cnxn, ids_to_notify_of_issue) # TODO(jrobbins): Merge ids_to_notify_of_issue entries for linked accounts. for user_id in ids_to_notify_of_issue: if not user_id: continue # Don't try to notify NO_USER_SPECIFIED if users_by_id[user_id].email in omit_addrs: logging.info('Omitting %s', user_id) continue user_issues = ids_to_notify_of_issue[user_id] if not user_issues: continue # user's prefs indicate they don't want these notifications auth = authdata.AuthData.FromUserID( cnxn, user_id, self.services) is_member = bool(framework_bizobj.UserIsInProject( project, auth.effective_ids)) if is_member: member_ids_to_notify_of_issue[user_id] = user_issues else: non_member_ids_to_notify_of_issue[user_id] = user_issues addr = users_by_id[user_id].email omit_addrs.add(addr) addr_to_addrperm[addr] = notify_reasons.AddrPerm( is_member, addr, users_by_id[user_id].user, notify_reasons.REPLY_NOT_ALLOWED, all_user_prefs[user_id]) for addr, addr_issues in additional_addrs_to_notify_of_issue.items(): auth = None try: auth = authdata.AuthData.FromEmail(cnxn, addr, self.services) except: # pylint: disable=bare-except logging.warning('Cannot find user of email %s ', addr) if auth: is_member = bool(framework_bizobj.UserIsInProject( project, auth.effective_ids)) else: is_member = False if is_member: member_additional_addrs[addr] = addr_issues else: non_member_additional_addrs[addr] = addr_issues omit_addrs.add(addr) addr_to_addrperm[addr] = notify_reasons.AddrPerm( is_member, addr, None, notify_reasons.REPLY_NOT_ALLOWED, None) for user_id, user_issues in non_member_ids_to_notify_of_issue.items(): addr = users_by_id[user_id].email email = self._FormatBulkIssuesEmail( addr_to_addrperm[addr], user_issues, users_by_id, commenter_view, hostport, comment_text, amendments, config, project) email_tasks.append(email) logging.info('about to bulk notify non-member %s (%s) of %s', users_by_id[user_id].email, user_id, [issue.local_id for issue in user_issues]) for addr, addr_issues in non_member_additional_addrs.items(): email = self._FormatBulkIssuesEmail( addr_to_addrperm[addr], addr_issues, users_by_id, commenter_view, hostport, comment_text, amendments, config, project) email_tasks.append(email) logging.info('about to bulk notify non-member additional addr %s of %s', addr, [addr_issue.local_id for addr_issue in addr_issues]) framework_views.RevealAllEmails(users_by_id) commenter_view.RevealEmail() for user_id, user_issues in member_ids_to_notify_of_issue.items(): addr = users_by_id[user_id].email email = self._FormatBulkIssuesEmail( addr_to_addrperm[addr], user_issues, users_by_id, commenter_view, hostport, comment_text, amendments, config, project) email_tasks.append(email) logging.info('about to bulk notify member %s (%s) of %s', addr, user_id, [issue.local_id for issue in user_issues]) for addr, addr_issues in member_additional_addrs.items(): email = self._FormatBulkIssuesEmail( addr_to_addrperm[addr], addr_issues, users_by_id, commenter_view, hostport, comment_text, amendments, config, project) email_tasks.append(email) logging.info('about to bulk notify member additional addr %s of %s', addr, [addr_issue.local_id for addr_issue in addr_issues]) # 4. Add in the project's issue_notify_address. This happens even if it # is the same as the commenter's email address (which would be an unusual # but valid project configuration). Only issues that any contributor could # view are included in emails to the all-issue-activity mailing lists. if (project.issue_notify_address and project.issue_notify_address not in omit_addrs): non_private_issues_live = [] for issue in issues: contributor_could_view = permissions.CanViewIssue( set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project, issue) if contributor_could_view: non_private_issues_live.append(issue) if non_private_issues_live: project_notify_addrperm = notify_reasons.AddrPerm( True, project.issue_notify_address, None, notify_reasons.REPLY_NOT_ALLOWED, None) email = self._FormatBulkIssuesEmail( project_notify_addrperm, non_private_issues_live, users_by_id, commenter_view, hostport, comment_text, amendments, config, project) email_tasks.append(email) omit_addrs.add(project.issue_notify_address) logging.info('about to bulk notify all-issues %s of %s', project.issue_notify_address, [issue.local_id for issue in non_private_issues]) return email_tasks
def _ParseTemplate(self, post_data, mr, i, orig_template, config): """Parse an issue template. Return orig_template if cannot edit.""" if not self._CanEditTemplate(mr, orig_template): return orig_template name = post_data['name_%s' % i] if name == tracker_constants.DELETED_TEMPLATE_NAME: return None members_only = False if ('members_only_%s' % i) in post_data: members_only = (post_data['members_only_%s' % i] == 'yes') summary = '' if ('summary_%s' % i) in post_data: summary = post_data['summary_%s' % i] summary_must_be_edited = False if ('summary_must_be_edited_%s' % i) in post_data: summary_must_be_edited = (post_data['summary_must_be_edited_%s' % i] == 'yes') content = '' if ('content_%s' % i) in post_data: content = post_data['content_%s' % i] # wrap="hard" has no effect on the content because we copy it to # a hidden form field before submission. So, server-side word wrap. content = framework_helpers.WordWrapSuperLongLines(content, max_cols=75) status = '' if ('status_%s' % i) in post_data: status = post_data['status_%s' % i] owner_id = 0 if ('owner_%s' % i) in post_data: owner = post_data['owner_%s' % i] if owner: user_id = self.services.user.LookupUserID(mr.cnxn, owner) auth = monorailrequest.AuthData.FromUserID( mr.cnxn, user_id, self.services) if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids): owner_id = user_id labels = post_data.getall('label_%s' % i) labels_remove = [] field_val_strs = collections.defaultdict(list) for fd in config.field_defs: field_value_key = 'field_value_%d_%d' % (i, fd.field_id) if post_data.get(field_value_key): field_val_strs[fd.field_id].append(post_data[field_value_key]) field_helpers.ShiftEnumFieldsIntoLabels(labels, labels_remove, field_val_strs, {}, config) field_values = field_helpers.ParseFieldValues(mr.cnxn, self.services.user, field_val_strs, config) for fv in field_values: logging.info('field_value is %r: %r', fv.field_id, tracker_bizobj.GetFieldValue(fv, {})) admin_ids = [] if ('admin_names_%s' % i) in post_data: admin_ids, _admin_str = tracker_helpers.ParseAdminUsers( mr.cnxn, post_data['admin_names_%s' % i], self.services.user) component_ids = [] if ('components_%s' % i) in post_data: component_paths = [] for component_path in post_data['components_%s' % i].split(','): if component_path.strip() not in component_paths: component_paths.append(component_path.strip()) component_ids = tracker_helpers.LookupComponentIDs( component_paths, config, mr.errors) owner_defaults_to_member = False if ('owner_defaults_to_member_%s' % i) in post_data: owner_defaults_to_member = ( post_data['owner_defaults_to_member_%s' % i] == 'yes') component_required = False if ('component_required_%s' % i) in post_data: component_required = post_data['component_required_%s' % i] == 'yes' template = tracker_bizobj.MakeIssueTemplate( name, summary, status, owner_id, content, labels, field_values, admin_ids, component_ids, summary_must_be_edited=summary_must_be_edited, owner_defaults_to_member=owner_defaults_to_member, component_required=component_required, members_only=members_only) template_id = int(post_data['template_id_%s' % i]) if template_id: # new templates have ID 0, so leave that None in PB. template.template_id = template_id logging.info('template is %r', template) return template
def __init__(self, mr, services, config, template=None, load_all_templates=False): """Gather data for the issue section of a project admin page. Args: mr: MonorailRequest, including a database connection, the current project, and authenticated user IDs. services: Persist services with ProjectService, ConfigService, TemplateService and UserService included. config: ProjectIssueConfig for the current project.. template (TemplateDef, optional): the current template. load_all_templates (boolean): default False. If true loads self.templates. Returns: Project info in a dict suitable for EZT. """ super(ConfigView, self).__init__(config) self.open_statuses = [] self.closed_statuses = [] for wks in config.well_known_statuses: item = template_helpers.EZTItem( name=wks.status, name_padded=wks.status.ljust(20), commented='#' if wks.deprecated else '', docstring=wks.status_docstring) if tracker_helpers.MeansOpenInProject(wks.status, config): self.open_statuses.append(item) else: self.closed_statuses.append(item) is_member = framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids) template_set = services.template.GetTemplateSetForProject( mr.cnxn, config.project_id) # Filter non-viewable templates self.template_names = [] for _, template_name, members_only in template_set: if members_only and not is_member: continue self.template_names.append(template_name) if load_all_templates: templates = services.template.GetProjectTemplates( mr.cnxn, config.project_id) self.templates = [ IssueTemplateView(mr, tmpl, services.user, config) for tmpl in templates ] for index, template_view in enumerate(self.templates): template_view.index = index if template: self.template_view = IssueTemplateView(mr, template, services.user, config) self.field_names = [ # TODO(jrobbins): field-level controls fd.field_name for fd in config.field_defs if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE and not fd.is_deleted ] self.issue_labels = tracker_helpers.LabelsNotMaskedByFields( config, self.field_names) self.excl_prefixes = [ prefix.lower() for prefix in config.exclusive_label_prefixes ] self.restrict_to_known = ezt.boolean(config.restrict_to_known) self.default_col_spec = (config.default_col_spec or tracker_constants.DEFAULT_COL_SPEC)