blob: b1478fc36062a4fe32c3daf31271a4494c3af6b3 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is govered by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
"""Business objects for Monorail's framework.
These are classes and functions that operate on the objects that
users care about in Monorail but that are not part of just one specific
component: e.g., projects, users, and labels.
"""
import logging
import re
import string
import settings
from framework import framework_constants
# Pattern to match a valid project name. Users of this pattern MUST use
# the re.VERBOSE flag or the whitespace and comments we be considered
# significant and the pattern will not work. See "re" module documentation.
_RE_PROJECT_NAME_PATTERN_VERBOSE = r"""
(?=[-a-z0-9]*[a-z][-a-z0-9]*) # Lookahead to make sure there is at least
# one letter in the whole name.
[a-z0-9] # Start with a letter or digit.
[-a-z0-9]* # Follow with any number of valid characters.
[a-z0-9] # End with a letter or digit.
"""
# Compiled regexp to match the project name and nothing more before or after.
RE_PROJECT_NAME = re.compile(
'^%s$' % _RE_PROJECT_NAME_PATTERN_VERBOSE, re.VERBOSE)
def IsValidProjectName(s):
"""Return true if the given string is a valid project name."""
return (RE_PROJECT_NAME.match(s) and
len(s) <= framework_constants.MAX_PROJECT_NAME_LENGTH)
def UserOwnsProject(project, effective_ids):
"""Return True if any of the effective_ids is a project owner."""
return not effective_ids.isdisjoint(project.owner_ids or set())
def UserIsInProject(project, effective_ids):
"""Return True if any of the effective_ids is a project member.
Args:
project: Project PB for the current project.
effective_ids: set of int user IDs for the current user (including all
user groups). This will be an empty set for anonymous users.
Returns:
True if the user has any direct or indirect role in the project. The value
will actually be a set(), but it will have an ID in it if the user is in
the project, or it will be an empty set which is considered False.
"""
return (UserOwnsProject(project, effective_ids) or
not effective_ids.isdisjoint(project.committer_ids or set()) or
not effective_ids.isdisjoint(project.contributor_ids or set()))
def AllProjectMembers(project):
"""Return a list of user IDs of all members in the given project."""
return project.owner_ids + project.committer_ids + project.contributor_ids
def IsPriviledgedDomainUser(email):
"""Return True if the user's account is from a priviledged domain."""
if email and '@' in email:
_, user_domain = email.split('@', 1)
return user_domain in settings.priviledged_user_domains
return False
# String translation table to catch a common typos in label names.
_CANONICALIZATION_TRANSLATION_TABLE = {
ord(delete_u_char): None
for delete_u_char in u'!"#$%&\'()*+,/:;<>?@[\\]^`{|}~\t\n\x0b\x0c\r '
}
_CANONICALIZATION_TRANSLATION_TABLE.update({ord(u'='): ord(u'-')})
def CanonicalizeLabel(user_input):
"""Canonicalize a given label or status value.
When the user enters a string that represents a label or an enum,
convert it a canonical form that makes it more likely to match
existing values.
Args:
user_input: string that the user typed for a label.
Returns:
Canonical form of that label as a unicode string.
"""
if user_input is None:
return user_input
if not isinstance(user_input, unicode):
user_input = user_input.decode('utf-8')
canon_str = user_input.translate(_CANONICALIZATION_TRANSLATION_TABLE)
return canon_str
def MergeLabels(labels_list, labels_add, labels_remove, excl_prefixes):
"""Update a list of labels with the given add and remove label lists.
Args:
labels_list: list of current labels.
labels_add: labels that the user wants to add.
labels_remove: labels that the user wants to remove.
excl_prefixes: prefixes that can have only one value, e.g., Priority.
Returns:
(merged_labels, update_labels_add, update_labels_remove):
A new list of labels with the given labels added and removed, and
any exclusive label prefixes taken into account. Then two
lists of update strings to explain the changes that were actually
made.
"""
old_lower_labels = [lab.lower() for lab in labels_list]
labels_add = [lab for lab in labels_add
if lab.lower() not in old_lower_labels]
labels_remove = [lab for lab in labels_remove
if lab.lower() in old_lower_labels]
labels_remove_lower = [lab.lower() for lab in labels_remove]
config_excl = [lab.lower() for lab in excl_prefixes]
# "Old minus exclusive" is the set of old label values minus any
# that are implictly removed by newly set exclusive labels.
excl_add = [] # E.g., there can be only one "Priority-*" label
for lab in labels_add:
prefix = lab.split('-')[0].lower()
if prefix in config_excl:
excl_add.append('%s-' % prefix)
old_minus_excl = []
for lab in labels_list:
for prefix_dash in excl_add:
if lab.lower().startswith(prefix_dash):
# Note: don't add -lab to update_labels_remove, it is implicit.
break
else:
old_minus_excl.append(lab)
merged_labels = [lab for lab in old_minus_excl + labels_add
if lab.lower() not in labels_remove_lower]
return merged_labels, labels_add, labels_remove