Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
    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))
Ejemplo n.º 6
0
 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 ''
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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)
Ejemplo n.º 10
0
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, [])
Ejemplo n.º 11
0
    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)}
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
  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}))
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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.'
Ejemplo n.º 17
0
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]
Ejemplo n.º 18
0
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
Ejemplo n.º 19
0
    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)
Ejemplo n.º 20
0
    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
Ejemplo n.º 21
0
    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)
Ejemplo n.º 22
0
  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
Ejemplo n.º 23
0
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
Ejemplo n.º 24
0
  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
Ejemplo n.º 25
0
  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
Ejemplo n.º 26
0
    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
Ejemplo n.º 27
0
    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)