Index: appengine/monorail/framework/framework_bizobj.py |
diff --git a/appengine/monorail/framework/framework_bizobj.py b/appengine/monorail/framework/framework_bizobj.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..b1478fc36062a4fe32c3daf31271a4494c3af6b3 |
--- /dev/null |
+++ b/appengine/monorail/framework/framework_bizobj.py |
@@ -0,0 +1,156 @@ |
+# 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 |