Index: appengine/monorail/framework/monorailrequest.py |
diff --git a/appengine/monorail/framework/monorailrequest.py b/appengine/monorail/framework/monorailrequest.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8e4dcd0fa2d7370f839ffcd78d406e7c94180f1a |
--- /dev/null |
+++ b/appengine/monorail/framework/monorailrequest.py |
@@ -0,0 +1,691 @@ |
+# 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 |
+ |
+"""Classes to hold information parsed from a request. |
+ |
+To simplify our servlets and avoid duplication of code, we parse some |
+info out of the request as soon as we get it and then pass a MonorailRequest |
+object to the servlet-specific request handler methods. |
+""" |
+ |
+import endpoints |
+import logging |
+import re |
+import urllib |
+ |
+from third_party import ezt |
+ |
+from google.appengine.api import app_identity |
+from google.appengine.api import oauth |
+from google.appengine.api import users |
+ |
+import webapp2 |
+ |
+import settings |
+from framework import framework_constants |
+from framework import framework_views |
+from framework import permissions |
+from framework import sql |
+from framework import template_helpers |
+from proto import api_pb2_v1 |
+from proto import user_pb2 |
+from services import user_svc |
+from tracker import tracker_bizobj |
+from tracker import tracker_constants |
+ |
+ |
+_HOSTPORT_RE = re.compile('^[-a-z0-9.]+(:\d+)?$', re.I) |
+ |
+ |
+class AuthData(object): |
+ """This object holds authentication data about a user. |
+ |
+ This is used by MonorailRequest as it determines which user the |
+ requester is authenticated as and fetches the user's data. It can |
+ also be used to lookup perms for user IDs specified in issue fields. |
+ |
+ Attributes: |
+ user_id: The user ID of the user (or 0 if not signed in). |
+ effective_ids: A set of user IDs that includes the signed in user's |
+ direct user ID and the user IDs of all their user groups. |
+ This set will be empty for anonymous users. |
+ user_view: UserView object for the signed-in user. |
+ user_pb: User object for the signed-in user. |
+ email: email address for the user, or None. |
+ """ |
+ |
+ def __init__(self): |
+ self.user_id = 0 |
+ self.effective_ids = set() |
+ self.user_view = None |
+ self.user_pb = user_pb2.MakeUser() |
+ self.email = None |
+ |
+ @classmethod |
+ def FromRequest(cls, cnxn, services): |
+ """Determine auth information from the request and fetches user data. |
+ |
+ If everything works and the user is signed in, then all of the public |
+ attributes of the AuthData instance will be filled in appropriately. |
+ |
+ Args: |
+ cnxn: connection to the SQL database. |
+ services: Interface to all persistence storage backends. |
+ |
+ Returns: |
+ A new AuthData object. |
+ """ |
+ user = users.get_current_user() |
+ if user is None: |
+ return cls() |
+ else: |
+ # We create a User row for each user who visits the site. |
+ # TODO(jrobbins): we should really only do it when they take action. |
+ return cls.FromEmail(cnxn, user.email(), services, autocreate=True) |
+ |
+ @classmethod |
+ def FromEmail(cls, cnxn, email, services, autocreate=False): |
+ """Determine auth information for the given user email address. |
+ |
+ Args: |
+ cnxn: monorail connection to the database. |
+ email: string email address of the user. |
+ services: connections to backend servers. |
+ autocreate: set to True to create a new row in the Users table if needed. |
+ |
+ Returns: |
+ A new AuthData object. |
+ |
+ Raises: |
+ user_svc.NoSuchUserException: If the user of the email does not exist. |
+ """ |
+ auth = cls() |
+ auth.email = email |
+ if email: |
+ auth.user_id = services.user.LookupUserID( |
+ cnxn, email, autocreate=autocreate) |
+ assert auth.user_id |
+ |
+ cls._FinishInitialization(cnxn, auth, services) |
+ return auth |
+ |
+ @classmethod |
+ def FromUserID(cls, cnxn, user_id, services): |
+ """Determine auth information for the given user ID. |
+ |
+ Args: |
+ cnxn: monorail connection to the database. |
+ user_id: int user ID of the user. |
+ services: connections to backend servers. |
+ |
+ Returns: |
+ A new AuthData object. |
+ """ |
+ auth = cls() |
+ auth.user_id = user_id |
+ if auth.user_id: |
+ auth.email = services.user.LookupUserEmail(cnxn, user_id) |
+ |
+ cls._FinishInitialization(cnxn, auth, services) |
+ return auth |
+ |
+ @classmethod |
+ def _FinishInitialization(cls, cnxn, auth, services): |
+ """Fill in the test of the fields based on the user_id.""" |
+ # TODO(jrobbins): re-implement same_org |
+ if auth.user_id: |
+ auth.effective_ids = services.usergroup.LookupMemberships( |
+ cnxn, auth.user_id) |
+ auth.effective_ids.add(auth.user_id) |
+ auth.user_pb = services.user.GetUser(cnxn, auth.user_id) |
+ if auth.user_pb: |
+ auth.user_view = framework_views.UserView( |
+ auth.user_id, auth.email, |
+ auth.user_pb.obscure_email) |
+ |
+ |
+class MonorailApiRequest(object): |
+ """A class to hold information parsed from the Endpoints API request.""" |
+ |
+ # pylint: disable=attribute-defined-outside-init |
+ def __init__(self, request, services): |
+ requester = ( |
+ endpoints.get_current_user() or |
+ oauth.get_current_user( |
+ framework_constants.OAUTH_SCOPE)) |
+ requester_email = requester.email().lower() |
+ self.cnxn = sql.MonorailConnection() |
+ self.auth = AuthData.FromEmail( |
+ self.cnxn, requester_email, services) |
+ self.me_user_id = self.auth.user_id |
+ self.viewed_username = None |
+ self.viewed_user_auth = None |
+ self.project_name = None |
+ self.project = None |
+ self.issue = None |
+ self.config = None |
+ self.granted_perms = set() |
+ |
+ # query parameters |
+ self.params = { |
+ 'can': 1, |
+ 'start': 0, |
+ 'num': 100, |
+ 'q': '', |
+ 'sort': '', |
+ 'groupby': '', |
+ 'projects': []} |
+ self.use_cached_searches = True |
+ self.warnings = [] |
+ self.errors = template_helpers.EZTError() |
+ self.mode = None |
+ |
+ if hasattr(request, 'projectId'): |
+ self.project_name = request.projectId |
+ self.project = services.project.GetProjectByName( |
+ self.cnxn, self.project_name) |
+ self.params['projects'].append(self.project_name) |
+ self.config = services.config.GetProjectConfig( |
+ self.cnxn, self.project_id) |
+ if hasattr(request, 'additionalProject'): |
+ self.params['projects'].extend(request.additionalProject) |
+ self.params['projects'] = list(set(self.params['projects'])) |
+ if hasattr(request, 'issueId'): |
+ self.issue = services.issue.GetIssueByLocalID( |
+ self.cnxn, self.project_id, request.issueId) |
+ self.granted_perms = tracker_bizobj.GetGrantedPerms( |
+ self.issue, self.auth.effective_ids, self.config) |
+ if hasattr(request, 'userId'): |
+ self.viewed_username = request.userId.lower() |
+ if self.viewed_username == 'me': |
+ self.viewed_username = requester_email |
+ self.viewed_user_auth = AuthData.FromEmail( |
+ self.cnxn, self.viewed_username, services) |
+ elif hasattr(request, 'groupName'): |
+ self.viewed_username = request.groupName.lower() |
+ try: |
+ self.viewed_user_auth = AuthData.FromEmail( |
+ self.cnxn, self.viewed_username, services) |
+ except user_svc.NoSuchUserException: |
+ self.viewed_user_auth = None |
+ self.perms = permissions.GetPermissions( |
+ self.auth.user_pb, self.auth.effective_ids, self.project) |
+ |
+ # Build q. |
+ if hasattr(request, 'q') and request.q: |
+ self.params['q'] = request.q |
+ if hasattr(request, 'publishedMax') and request.publishedMax: |
+ self.params['q'] += ' opened<=%d' % request.publishedMax |
+ if hasattr(request, 'publishedMin') and request.publishedMin: |
+ self.params['q'] += ' opened>=%d' % request.publishedMin |
+ if hasattr(request, 'updatedMax') and request.updatedMax: |
+ self.params['q'] += ' modified<=%d' % request.updatedMax |
+ if hasattr(request, 'updatedMin') and request.updatedMin: |
+ self.params['q'] += ' modified>=%d' % request.updatedMin |
+ if hasattr(request, 'owner') and request.owner: |
+ self.params['q'] += ' owner:%s' % request.owner |
+ if hasattr(request, 'status') and request.status: |
+ self.params['q'] += ' status:%s' % request.status |
+ if hasattr(request, 'label') and request.label: |
+ self.params['q'] += ' label:%s' % request.label |
+ |
+ if hasattr(request, 'can') and request.can: |
+ if request.can == api_pb2_v1.CannedQuery.all: |
+ self.params['can'] = 1 |
+ elif request.can == api_pb2_v1.CannedQuery.new: |
+ self.params['can'] = 6 |
+ elif request.can == api_pb2_v1.CannedQuery.open: |
+ self.params['can'] = 2 |
+ elif request.can == api_pb2_v1.CannedQuery.owned: |
+ self.params['can'] = 3 |
+ elif request.can == api_pb2_v1.CannedQuery.reported: |
+ self.params['can'] = 4 |
+ elif request.can == api_pb2_v1.CannedQuery.starred: |
+ self.params['can'] = 5 |
+ elif request.can == api_pb2_v1.CannedQuery.to_verify: |
+ self.params['can'] = 7 |
+ else: # Endpoints should have caught this. |
+ raise InputException( |
+ 'Canned query %s is not supported.', request.can) |
+ if hasattr(request, 'startIndex') and request.startIndex: |
+ self.params['start'] = request.startIndex |
+ if hasattr(request, 'maxResults') and request.maxResults: |
+ self.params['num'] = request.maxResults |
+ if hasattr(request, 'sort') and request.sort: |
+ self.params['sort'] = request.sort |
+ |
+ self.query_project_names = self.GetParam('projects') |
+ self.group_by_spec = self.GetParam('groupby') |
+ self.sort_spec = self.GetParam('sort') |
+ self.query = self.GetParam('q') |
+ self.can = self.GetParam('can') |
+ self.start = self.GetParam('start') |
+ self.num = self.GetParam('num') |
+ |
+ @property |
+ def project_id(self): |
+ return self.project.project_id if self.project else None |
+ |
+ def GetParam(self, query_param_name, default_value=None, |
+ _antitamper_re=None): |
+ return self.params.get(query_param_name, default_value) |
+ |
+ def GetPositiveIntParam(self, query_param_name, default_value=None): |
+ """Returns 0 if the user-provided value is less than 0.""" |
+ return max(self.GetParam(query_param_name, default_value=default_value), |
+ 0) |
+ |
+ |
+class MonorailRequest(object): |
+ """A class to hold information parsed from the HTTP request. |
+ |
+ The goal of MonorailRequest is to do almost all URL path and query string |
+ procesing in one place, which makes the servlet code simpler. |
+ |
+ Attributes: |
+ cnxn: connection to the SQL databases. |
+ logged_in_user_id: int user ID of the signed-in user, or None. |
+ effective_ids: set of signed-in user ID and all their user group IDs. |
+ user_pb: User object for the signed in user. |
+ project_name: string name of the current project. |
+ project_id: int ID of the current projet. |
+ viewed_username: string username of the user whose profile is being viewed. |
+ can: int "canned query" number to scope the user's search. |
+ num: int number of results to show per pagination page. |
+ start: int position in result set to show on this pagination page. |
+ etc: there are many more, all read-only. |
+ """ |
+ |
+ # pylint: disable=attribute-defined-outside-init |
+ def __init__(self, params=None): |
+ """Initialize the MonorailRequest object.""" |
+ self.form_overrides = {} |
+ if params: |
+ self.form_overrides.update(params) |
+ self.warnings = [] |
+ self.errors = template_helpers.EZTError() |
+ self.debug_enabled = False |
+ self.use_cached_searches = True |
+ self.cnxn = sql.MonorailConnection() |
+ |
+ self.auth = AuthData() # Authentication info for logged-in user |
+ |
+ self.project_name = None |
+ self.project = None |
+ |
+ self.viewed_username = None |
+ self.viewed_user_auth = AuthData() |
+ |
+ @property |
+ def project_id(self): |
+ return self.project.project_id if self.project else None |
+ |
+ def CleanUp(self): |
+ """Close the database connection so that the app does not run out.""" |
+ if self.cnxn: |
+ self.cnxn.Close() |
+ self.cnxn = None |
+ |
+ def ParseRequest(self, request, services, prof, do_user_lookups=True): |
+ """Parse tons of useful info from the given request object. |
+ |
+ Args: |
+ request: webapp2 Request object w/ path and query params. |
+ services: connections to backend servers including DB. |
+ prof: Profiler instance. |
+ do_user_lookups: Set to False to disable lookups during testing. |
+ """ |
+ with prof.Phase('basic parsing'): |
+ self.request = request |
+ self.current_page_url = request.url |
+ self.current_page_url_encoded = urllib.quote_plus(self.current_page_url) |
+ |
+ # Only accept a hostport from the request that looks valid. |
+ if not _HOSTPORT_RE.match(request.host): |
+ raise InputException('request.host looks funny: %r', request.host) |
+ |
+ logging.info('Request: %s', self.current_page_url) |
+ |
+ with prof.Phase('path parsing'): |
+ viewed_user_val, self.project_name = _ParsePathIdentifiers( |
+ self.request.path) |
+ self.viewed_username = _GetViewedEmail( |
+ viewed_user_val, self.cnxn, services) |
+ with prof.Phase('qs parsing'): |
+ self._ParseQueryParameters() |
+ with prof.Phase('overrides parsing'): |
+ self._ParseFormOverrides() |
+ |
+ if not self.project: # It can be already set in unit tests. |
+ self._LookupProject(services, prof) |
+ if do_user_lookups: |
+ if self.viewed_username: |
+ self._LookupViewedUser(services, prof) |
+ self._LookupLoggedInUser(services, prof) |
+ # TODO(jrobbins): re-implement HandleLurkerViewingSelf() |
+ |
+ prod_debug_allowed = self.perms.HasPerm( |
+ permissions.VIEW_DEBUG, self.auth.user_id, None) |
+ self.debug_enabled = (request.params.get('debug') and |
+ (settings.dev_mode or prod_debug_allowed)) |
+ # temporary option for perf testing on staging instance. |
+ if request.params.get('disable_cache'): |
+ if settings.dev_mode or 'staging' in request.host: |
+ self.use_cached_searches = False |
+ |
+ def _ParseQueryParameters(self): |
+ """Parse and convert all the query string params used in any servlet.""" |
+ self.start = self.GetPositiveIntParam('start', default_value=0) |
+ self.num = self.GetPositiveIntParam('num', default_value=100) |
+ # Prevent DoS attacks that try to make us serve really huge result pages. |
+ self.num = min(self.num, settings.max_artifact_search_results_per_page) |
+ |
+ self.invalidation_timestep = self.GetIntParam( |
+ 'invalidation_timestep', default_value=0) |
+ |
+ self.continue_issue_id = self.GetIntParam( |
+ 'continue_issue_id', default_value=0) |
+ self.redir = self.GetParam('redir') |
+ |
+ # Search scope, a.k.a., canned query ID |
+ # TODO(jrobbins): make configurable |
+ self.can = self.GetIntParam( |
+ 'can', default_value=tracker_constants.OPEN_ISSUES_CAN) |
+ |
+ # Search query |
+ self.query = self.GetParam('q', default_value='').strip() |
+ |
+ # Sorting of search results (needed for result list and flipper) |
+ self.sort_spec = self.GetParam( |
+ 'sort', default_value='', |
+ antitamper_re=framework_constants.SORTSPEC_RE) |
+ |
+ # Note: This is set later in request handling by ComputeColSpec(). |
+ self.col_spec = None |
+ |
+ # Grouping of search results (needed for result list and flipper) |
+ self.group_by_spec = self.GetParam( |
+ 'groupby', default_value='', |
+ antitamper_re=framework_constants.SORTSPEC_RE) |
+ |
+ # For issue list and grid mode. |
+ self.cursor = self.GetParam('cursor') |
+ self.preview = self.GetParam('preview') |
+ self.mode = self.GetParam('mode', default_value='list') |
+ self.x = self.GetParam('x', default_value='') |
+ self.y = self.GetParam('y', default_value='') |
+ self.cells = self.GetParam('cells', default_value='ids') |
+ |
+ # For the dashboard and issue lists included in the dashboard. |
+ self.ajah = self.GetParam('ajah') # AJAH = Asychronous Javascript And HTML |
+ self.table_title = self.GetParam('table_title') |
+ self.panel_id = self.GetIntParam('panel') |
+ |
+ # For pagination of updates lists |
+ self.before = self.GetPositiveIntParam('before') |
+ self.after = self.GetPositiveIntParam('after') |
+ |
+ # For cron tasks and backend calls |
+ self.lower_bound = self.GetIntParam('lower_bound') |
+ self.upper_bound = self.GetIntParam('upper_bound') |
+ self.shard_id = self.GetIntParam('shard_id') |
+ |
+ # For specifying which objects to operate on |
+ self.local_id = self.GetIntParam('id') |
+ self.local_id_list = self.GetIntListParam('ids') |
+ self.seq = self.GetIntParam('seq') |
+ self.aid = self.GetIntParam('aid') |
+ self.specified_user_id = self.GetIntParam('u', default_value=0) |
+ self.specified_logged_in_user_id = self.GetIntParam( |
+ 'logged_in_user_id', default_value=0) |
+ self.specified_me_user_id = self.GetIntParam( |
+ 'me_user_id', default_value=0) |
+ self.specified_project = self.GetParam('project') |
+ self.specified_project_id = self.GetIntParam('project_id') |
+ self.query_project_names = self.GetListParam('projects', default_value=[]) |
+ self.template_name = self.GetParam('template') |
+ self.component_path = self.GetParam('component') |
+ self.field_name = self.GetParam('field') |
+ |
+ # For image attachments |
+ self.inline = bool(self.GetParam('inline')) |
+ self.thumb = bool(self.GetParam('thumb')) |
+ |
+ # For JS callbacks |
+ self.token = self.GetParam('token') |
+ self.starred = bool(self.GetIntParam('starred')) |
+ |
+ # For issue reindexing utility servlet |
+ self.auto_submit = self.GetParam('auto_submit') |
+ |
+ def _ParseFormOverrides(self): |
+ """Support deep linking by allowing the user to set form fields via QS.""" |
+ allowed_overrides = { |
+ 'template_name': self.GetParam('template_name'), |
+ 'initial_summary': self.GetParam('summary'), |
+ 'initial_description': (self.GetParam('description') or |
+ self.GetParam('comment')), |
+ 'initial_comment': self.GetParam('comment'), |
+ 'initial_status': self.GetParam('status'), |
+ 'initial_owner': self.GetParam('owner'), |
+ 'initial_cc': self.GetParam('cc'), |
+ 'initial_blocked_on': self.GetParam('blockedon'), |
+ 'initial_blocking': self.GetParam('blocking'), |
+ 'initial_merge_into': self.GetIntParam('mergeinto'), |
+ 'initial_components': self.GetParam('components'), |
+ |
+ # For the people pages |
+ 'initial_add_members': self.GetParam('add_members'), |
+ 'initially_expanded_form': ezt.boolean(self.GetParam('expand_form')), |
+ |
+ # For user group admin pages |
+ 'initial_name': (self.GetParam('group_name') or |
+ self.GetParam('proposed_project_name')), |
+ } |
+ |
+ # Only keep the overrides that were actually provided in the query string. |
+ self.form_overrides.update( |
+ (k, v) for (k, v) in allowed_overrides.iteritems() |
+ if v is not None) |
+ |
+ def _LookupViewedUser(self, services, prof): |
+ """Get information about the viewed user (if any) from the request.""" |
+ try: |
+ with prof.Phase('get viewed user, if any'): |
+ self.viewed_user_auth = AuthData.FromEmail( |
+ self.cnxn, self.viewed_username, services, autocreate=False) |
+ except user_svc.NoSuchUserException: |
+ logging.info('could not find user %r', self.viewed_username) |
+ webapp2.abort(404, 'user not found') |
+ |
+ if not self.viewed_user_auth.user_id: |
+ webapp2.abort(404, 'user not found') |
+ |
+ def _LookupProject(self, services, prof): |
+ """Get information about the current project (if any) from the request.""" |
+ with prof.Phase('get current project, if any'): |
+ if not self.project_name: |
+ logging.info('no project_name, so no project') |
+ else: |
+ self.project = services.project.GetProjectByName( |
+ self.cnxn, self.project_name) |
+ if not self.project: |
+ webapp2.abort(404, 'invalid project') |
+ |
+ def _LookupLoggedInUser(self, services, prof): |
+ """Get information about the signed-in user (if any) from the request.""" |
+ with prof.Phase('get user info, if any'): |
+ self.auth = AuthData.FromRequest(self.cnxn, services) |
+ self.me_user_id = (self.GetIntParam('me') or |
+ self.viewed_user_auth.user_id or self.auth.user_id) |
+ |
+ with prof.Phase('looking up signed in user permissions'): |
+ self.perms = permissions.GetPermissions( |
+ self.auth.user_pb, self.auth.effective_ids, self.project) |
+ |
+ def ComputeColSpec(self, config): |
+ """Set col_spec based on param, default in the config, or site default.""" |
+ if self.col_spec is not None: |
+ return # Already set. |
+ default_col_spec = '' |
+ if config: |
+ default_col_spec = config.default_col_spec |
+ |
+ col_spec = self.GetParam( |
+ 'colspec', default_value=default_col_spec, |
+ antitamper_re=framework_constants.COLSPEC_RE) |
+ |
+ if not col_spec: |
+ # If col spec is still empty then default to the global col spec. |
+ col_spec = tracker_constants.DEFAULT_COL_SPEC |
+ |
+ self.col_spec = ' '.join(ParseColSpec(col_spec)) |
+ |
+ def PrepareForReentry(self, echo_data): |
+ """Expose the results of form processing as if it was a new GET. |
+ |
+ This method is called only when the user submits a form with invalid |
+ information which they are being asked to correct it. Updating the MR |
+ object allows the normal servlet get() method to populate the form with |
+ the entered values and error messages. |
+ |
+ Args: |
+ echo_data: dict of {page_data_key: value_to_reoffer, ...} that will |
+ override whatever HTML form values are nomally shown to the |
+ user when they initially view the form. This allows them to |
+ fix user input that was not valid. |
+ """ |
+ self.form_overrides.update(echo_data) |
+ |
+ def GetParam(self, query_param_name, default_value=None, |
+ antitamper_re=None): |
+ """Get a query parameter from the URL as a utf8 string.""" |
+ value = self.request.params.get(query_param_name) |
+ assert value is None or isinstance(value, unicode) |
+ using_default = value is None |
+ if using_default: |
+ value = default_value |
+ |
+ if antitamper_re and not antitamper_re.match(value): |
+ if using_default: |
+ logging.error('Default value fails antitamper for %s field: %s', |
+ query_param_name, value) |
+ else: |
+ logging.info('User seems to have tampered with %s field: %s', |
+ query_param_name, value) |
+ raise InputException() |
+ |
+ return value |
+ |
+ def GetIntParam(self, query_param_name, default_value=None): |
+ """Get an integer param from the URL or default.""" |
+ value = self.request.params.get(query_param_name) |
+ if value is None: |
+ return default_value |
+ |
+ try: |
+ return int(value) |
+ except (TypeError, ValueError): |
+ return default_value |
+ |
+ def GetPositiveIntParam(self, query_param_name, default_value=None): |
+ """Returns 0 if the user-provided value is less than 0.""" |
+ return max(self.GetIntParam(query_param_name, default_value=default_value), |
+ 0) |
+ |
+ def GetListParam(self, query_param_name, default_value=None): |
+ """Get a list of strings from the URL or default.""" |
+ params = self.request.params.get(query_param_name) |
+ if params is None: |
+ return default_value |
+ if not params: |
+ return [] |
+ return params.split(',') |
+ |
+ def GetIntListParam(self, query_param_name, default_value=None): |
+ """Get a list of ints from the URL or default.""" |
+ param_list = self.GetListParam(query_param_name) |
+ if param_list is None: |
+ return default_value |
+ |
+ try: |
+ return [int(p) for p in param_list] |
+ except (TypeError, ValueError): |
+ return default_value |
+ |
+ |
+def _ParsePathIdentifiers(path): |
+ """Parse out the workspace being requested (if any). |
+ |
+ Args: |
+ path: A string beginning with the request's path info. |
+ |
+ Returns: |
+ (viewed_user_val, project_name). |
+ """ |
+ viewed_user_val = None |
+ project_name = None |
+ |
+ # Strip off any query params |
+ split_path = path.lstrip('/').split('?')[0].split('/') |
+ |
+ if len(split_path) >= 2: |
+ if split_path[0] == 'p': |
+ project_name = split_path[1] |
+ if split_path[0] == 'u': |
+ viewed_user_val = urllib.unquote(split_path[1]) |
+ if split_path[0] == 'g': |
+ viewed_user_val = urllib.unquote(split_path[1]) |
+ |
+ return viewed_user_val, project_name |
+ |
+ |
+def _GetViewedEmail(viewed_user_val, cnxn, services): |
+ """Returns the viewed user's email. |
+ |
+ Args: |
+ viewed_user_val: Could be either int (user_id) or str (email). |
+ cnxn: connection to the SQL database. |
+ services: Interface to all persistence storage backends. |
+ |
+ Returns: |
+ viewed_email |
+ """ |
+ if not viewed_user_val: |
+ return None |
+ |
+ try: |
+ viewed_userid = int(viewed_user_val) |
+ viewed_email = services.user.LookupUserEmail(cnxn, viewed_userid) |
+ if not viewed_email: |
+ logging.info('userID %s not found', viewed_userid) |
+ webapp2.abort(404, 'user not found') |
+ except ValueError: |
+ viewed_email = viewed_user_val |
+ |
+ return viewed_email |
+ |
+ |
+def ParseColSpec(col_spec): |
+ """Split a string column spec into a list of column names. |
+ |
+ Args: |
+ col_spec: a unicode string containing a list of labels. |
+ |
+ Returns: |
+ A list of the extracted labels. Non-alphanumeric |
+ characters other than the period will be stripped from the text. |
+ """ |
+ return framework_constants.COLSPEC_COL_RE.findall(col_spec) |
+ |
+ |
+class Error(Exception): |
+ """Base class for errors from this module.""" |
+ pass |
+ |
+ |
+class InputException(Error): |
+ """Error in user input processing.""" |
+ pass |