Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/sitewide/__init__.py b/sitewide/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/sitewide/__init__.py
@@ -0,0 +1 @@
+
diff --git a/sitewide/custom_404.py b/sitewide/custom_404.py
new file mode 100644
index 0000000..397bd1d
--- /dev/null
+++ b/sitewide/custom_404.py
@@ -0,0 +1,41 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Page class for generating somewhat informative project-page 404s.
+
+This page class produces a mostly-empty project subpage, which helps
+users find what they're looking for by providing navigational menus,
+rather than telling them "404. That's an error. That's all we know."
+which is maddeningly not helpful when we already have a project pb
+loaded.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import httplib
+from framework import exceptions
+from framework import servlet
+
+
+class ErrorPage(servlet.Servlet):
+ """Page class for generating somewhat informative project-page 404s.
+
+ This page class produces a mostly-empty project subpage, which helps
+ users find what they're looking for by providing navigational menus,
+ rather than telling them "404. That's an error. That's all we know."
+ which is maddeningly not helpful when we already have a project pb
+ loaded.
+ """
+
+ _PAGE_TEMPLATE = 'sitewide/project-404-page.ezt'
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ if not mr.project_name:
+ raise exceptions.InputException('No project specified')
+ return {
+ 'http_response_code': httplib.NOT_FOUND,
+ }
diff --git a/sitewide/group_helpers.py b/sitewide/group_helpers.py
new file mode 100644
index 0000000..b0195c5
--- /dev/null
+++ b/sitewide/group_helpers.py
@@ -0,0 +1,78 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Helper functions used in user group modules."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from framework import framework_views
+from proto import usergroup_pb2
+
+
+class GroupVisibilityView(object):
+ """Object for group visibility information that can be easily used in EZT."""
+
+ VISIBILITY_NAMES = {
+ usergroup_pb2.MemberVisibility.ANYONE: 'Anyone on the Internet',
+ usergroup_pb2.MemberVisibility.MEMBERS: 'Group Members',
+ usergroup_pb2.MemberVisibility.OWNERS: 'Group Owners'}
+
+ def __init__(self, group_visibility_enum):
+ self.key = int(group_visibility_enum)
+ self.name = self.VISIBILITY_NAMES[group_visibility_enum]
+
+
+class GroupTypeView(object):
+ """Object for group type information that can be easily used in EZT."""
+
+ TYPE_NAMES = {
+ usergroup_pb2.GroupType.CHROME_INFRA_AUTH: 'Chrome-infra-auth',
+ usergroup_pb2.GroupType.MDB: 'MDB',
+ usergroup_pb2.GroupType.BAGGINS: 'Baggins',
+ usergroup_pb2.GroupType.COMPUTED: 'Computed',
+ }
+
+ def __init__(self, group_type_enum):
+ self.key = int(group_type_enum)
+ self.name = self.TYPE_NAMES[group_type_enum]
+
+
+class GroupMemberView(framework_views.UserView):
+ """Wrapper class to display basic group member information in a template."""
+
+ def __init__(self, user, group_id, role):
+ assert role in ['member', 'owner']
+ super(GroupMemberView, self).__init__(user)
+ self.group_id = group_id
+ self.role = role
+
+
+def BuildUserGroupVisibilityOptions():
+ """Return a list of user group visibility values for use in an HTML menu.
+
+ Returns:
+ A list of GroupVisibilityView objects that can be used in EZT.
+ """
+ vis_levels = [usergroup_pb2.MemberVisibility.OWNERS,
+ usergroup_pb2.MemberVisibility.MEMBERS,
+ usergroup_pb2.MemberVisibility.ANYONE]
+
+ return [GroupVisibilityView(vis) for vis in vis_levels]
+
+
+def BuildUserGroupTypeOptions():
+ """Return a list of user group types for use in an HTML menu.
+
+ Returns:
+ A list of GroupTypeView objects that can be used in EZT.
+ """
+ group_types = [usergroup_pb2.GroupType.CHROME_INFRA_AUTH,
+ usergroup_pb2.GroupType.MDB,
+ usergroup_pb2.GroupType.BAGGINS,
+ usergroup_pb2.GroupType.COMPUTED]
+
+ return sorted([GroupTypeView(gt) for gt in group_types],
+ key=lambda gtv: gtv.name)
diff --git a/sitewide/groupadmin.py b/sitewide/groupadmin.py
new file mode 100644
index 0000000..32ba007
--- /dev/null
+++ b/sitewide/groupadmin.py
@@ -0,0 +1,123 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""A class to display user group admin page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+import ezt
+
+from framework import framework_helpers
+from framework import permissions
+from framework import servlet
+from framework import urls
+from proto import usergroup_pb2
+from services import usergroup_svc
+from sitewide import group_helpers
+
+
+class GroupAdmin(servlet.Servlet):
+ """The group admin page."""
+
+ _PAGE_TEMPLATE = 'sitewide/group-admin-page.ezt'
+
+ def AssertBasePermission(self, mr):
+ """Assert that the user has the permissions needed to view this page."""
+ super(GroupAdmin, self).AssertBasePermission(mr)
+
+ _, owner_ids_dict = self.services.usergroup.LookupMembers(
+ mr.cnxn, [mr.viewed_user_auth.user_id])
+ owner_ids = owner_ids_dict[mr.viewed_user_auth.user_id]
+ if not permissions.CanEditGroup(
+ mr.perms, mr.auth.effective_ids, owner_ids):
+ raise permissions.PermissionException(
+ 'User is not allowed to edit a user group')
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ group_id = mr.viewed_user_auth.user_id
+ group_settings = self.services.usergroup.GetGroupSettings(
+ mr.cnxn, group_id)
+ visibility_levels = group_helpers.BuildUserGroupVisibilityOptions()
+ initial_visibility = group_helpers.GroupVisibilityView(
+ group_settings.who_can_view_members)
+ group_types = group_helpers.BuildUserGroupTypeOptions()
+ import_group = bool(group_settings.ext_group_type)
+ if import_group:
+ initial_group_type = group_helpers.GroupTypeView(
+ group_settings.ext_group_type)
+ else:
+ initial_group_type = ''
+
+ if group_settings.friend_projects:
+ initial_friendprojects = ', '.join(
+ list(self.services.project.LookupProjectNames(
+ mr.cnxn, group_settings.friend_projects).values()))
+ else:
+ initial_friendprojects = ''
+
+ return {
+ 'admin_tab_mode': 'st2',
+ 'groupadmin': True,
+ 'groupid': group_id,
+ 'groupname': mr.viewed_username,
+ 'group_types': group_types,
+ 'import_group': import_group or '',
+ 'initial_friendprojects': initial_friendprojects,
+ 'initial_group_type': initial_group_type,
+ 'initial_visibility': initial_visibility,
+ 'offer_membership_editing': True,
+ 'visibility_levels': visibility_levels,
+ }
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ # 1. Gather data from the request.
+ group_name = mr.viewed_username
+ group_id = mr.viewed_user_auth.user_id
+
+ if post_data.get('import_group'):
+ vis_level = usergroup_pb2.MemberVisibility.OWNERS
+ ext_group_type = post_data.get('group_type')
+ friend_projects = ''
+ if not ext_group_type:
+ mr.errors.groupimport = 'Please provide external group type'
+ else:
+ ext_group_type = usergroup_pb2.GroupType(int(ext_group_type))
+ else:
+ vis_level = post_data.get('visibility')
+ ext_group_type = None
+ friend_projects = post_data.get('friendprojects', '')
+ if vis_level:
+ vis_level = usergroup_pb2.MemberVisibility(int(vis_level))
+ else:
+ mr.errors.groupimport = 'Cannot update settings for imported group'
+
+ if not mr.errors.AnyErrors():
+ project_ids, error = self.services.usergroup.ValidateFriendProjects(
+ mr.cnxn, self.services, friend_projects)
+ if error:
+ mr.errors.friendprojects = error
+
+ # 2. Call services layer to save changes.
+ if not mr.errors.AnyErrors():
+ group_settings = usergroup_pb2.UserGroupSettings(
+ who_can_view_members=vis_level,
+ ext_group_type=ext_group_type,
+ friend_projects=project_ids)
+ self.services.usergroup.UpdateSettings(
+ mr.cnxn, group_id, group_settings)
+
+ # 3. Determine the next page in the UI flow.
+ if mr.errors.AnyErrors():
+ self.PleaseCorrect(mr, initial_name=group_name)
+ else:
+ return framework_helpers.FormatAbsoluteURL(
+ mr, '/g/%s%s' % (group_name, urls.GROUP_ADMIN),
+ include_project=False, saved=1, ts=int(time.time()))
diff --git a/sitewide/groupcreate.py b/sitewide/groupcreate.py
new file mode 100644
index 0000000..2dac146
--- /dev/null
+++ b/sitewide/groupcreate.py
@@ -0,0 +1,104 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""A page for site admins to create a new user group."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import re
+
+from framework import exceptions
+from framework import framework_helpers
+from framework import permissions
+from framework import servlet
+from proto import usergroup_pb2
+from sitewide import group_helpers
+
+
+class GroupCreate(servlet.Servlet):
+ """Shows a page with a simple form to create a user group."""
+
+ _PAGE_TEMPLATE = 'sitewide/group-create-page.ezt'
+
+ def AssertBasePermission(self, mr):
+ """Assert that the user has the permissions needed to view this page."""
+ super(GroupCreate, self).AssertBasePermission(mr)
+
+ if not permissions.CanCreateGroup(mr.perms):
+ raise permissions.PermissionException(
+ 'User is not allowed to create a user group')
+
+ def GatherPageData(self, _mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ visibility_levels = group_helpers.BuildUserGroupVisibilityOptions()
+ initial_visibility = group_helpers.GroupVisibilityView(
+ usergroup_pb2.MemberVisibility.ANYONE)
+ group_types = group_helpers.BuildUserGroupTypeOptions()
+
+ return {
+ 'groupadmin': '',
+ 'group_types': group_types,
+ 'import_group': '',
+ 'initial_friendprojects': '',
+ 'initial_group_type': '',
+ 'initial_name': '',
+ 'initial_visibility': initial_visibility,
+ 'visibility_levels': visibility_levels,
+ }
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ # 1. Gather data from the request.
+ group_name = post_data.get('groupname')
+ try:
+ existing_group_id = self.services.user.LookupUserID(mr.cnxn, group_name)
+ existing_settings = self.services.usergroup.GetGroupSettings(
+ mr.cnxn, existing_group_id)
+ if existing_settings:
+ mr.errors.groupname = 'That user group already exists'
+ except exceptions.NoSuchUserException:
+ pass
+
+ if post_data.get('import_group'):
+ vis = usergroup_pb2.MemberVisibility.OWNERS
+ ext_group_type = post_data.get('group_type')
+ friend_projects = ''
+ if not ext_group_type:
+ mr.errors.groupimport = 'Please provide external group type'
+ else:
+ ext_group_type = str(
+ usergroup_pb2.GroupType(int(ext_group_type))).lower()
+
+ if (ext_group_type == 'computed' and
+ not group_name.startswith('everyone@')):
+ mr.errors.groupimport = 'Computed groups must be named everyone@'
+
+ else:
+ vis = usergroup_pb2.MemberVisibility(int(post_data['visibility']))
+ ext_group_type = None
+ friend_projects = post_data.get('friendprojects', '')
+ who_can_view_members = str(vis).lower()
+
+ if not mr.errors.AnyErrors():
+ project_ids, error = self.services.usergroup.ValidateFriendProjects(
+ mr.cnxn, self.services, friend_projects)
+ if error:
+ mr.errors.friendprojects = error
+
+ # 2. Call services layer to save changes.
+ if not mr.errors.AnyErrors():
+ group_id = self.services.usergroup.CreateGroup(
+ mr.cnxn, self.services, group_name, who_can_view_members,
+ ext_group_type, project_ids)
+
+ # 3. Determine the next page in the UI flow.
+ if mr.errors.AnyErrors():
+ self.PleaseCorrect(mr, initial_name=group_name)
+ else:
+ # Go to the new user group's detail page.
+ return framework_helpers.FormatAbsoluteURL(
+ mr, '/g/%s/' % group_id, include_project=False)
diff --git a/sitewide/groupdetail.py b/sitewide/groupdetail.py
new file mode 100644
index 0000000..b28baa9
--- /dev/null
+++ b/sitewide/groupdetail.py
@@ -0,0 +1,210 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""A class to display a user group, including a paginated list of members."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+import ezt
+
+from framework import exceptions
+from framework import framework_helpers
+from framework import framework_views
+from framework import paginate
+from framework import permissions
+from framework import servlet
+from project import project_helpers
+from proto import usergroup_pb2
+from sitewide import group_helpers
+from sitewide import sitewide_views
+
+MEMBERS_PER_PAGE = 50
+
+
+class GroupDetail(servlet.Servlet):
+ """The group detail page presents information about one user group."""
+
+ _PAGE_TEMPLATE = 'sitewide/group-detail-page.ezt'
+
+ def AssertBasePermission(self, mr):
+ """Assert that the user has the permissions needed to view this page."""
+ super(GroupDetail, self).AssertBasePermission(mr)
+
+ group_id = mr.viewed_user_auth.user_id
+ group_settings = self.services.usergroup.GetGroupSettings(
+ mr.cnxn, group_id)
+ if not group_settings:
+ return
+
+ member_ids, owner_ids = self.services.usergroup.LookupAllMembers(
+ mr.cnxn, [group_id])
+ (owned_project_ids, membered_project_ids,
+ contrib_project_ids) = self.services.project.GetUserRolesInAllProjects(
+ mr.cnxn, mr.auth.effective_ids)
+ project_ids = owned_project_ids.union(
+ membered_project_ids).union(contrib_project_ids)
+ if not permissions.CanViewGroupMembers(
+ mr.perms, mr.auth.effective_ids, group_settings, member_ids[group_id],
+ owner_ids[group_id], project_ids):
+ raise permissions.PermissionException(
+ 'User is not allowed to view a user group')
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ group_id = mr.viewed_user_auth.user_id
+ group_settings = self.services.usergroup.GetGroupSettings(
+ mr.cnxn, group_id)
+ if not group_settings:
+ raise exceptions.NoSuchGroupException()
+
+ member_ids_dict, owner_ids_dict = (
+ self.services.usergroup.LookupVisibleMembers(
+ mr.cnxn, [group_id], mr.perms, mr.auth.effective_ids,
+ self.services))
+ member_ids = member_ids_dict[group_id]
+ owner_ids = owner_ids_dict[group_id]
+ member_pbs_dict = self.services.user.GetUsersByIDs(
+ mr.cnxn, member_ids)
+ owner_pbs_dict = self.services.user.GetUsersByIDs(
+ mr.cnxn, owner_ids)
+ member_dict = {}
+ for user_id, user_pb in member_pbs_dict.items():
+ member_view = group_helpers.GroupMemberView(user_pb, group_id, 'member')
+ member_dict[user_id] = member_view
+ owner_dict = {}
+ for user_id, user_pb in owner_pbs_dict.items():
+ member_view = group_helpers.GroupMemberView(user_pb, group_id, 'owner')
+ owner_dict[user_id] = member_view
+
+ member_user_views = []
+ member_user_views.extend(
+ sorted(list(owner_dict.values()), key=lambda u: u.email))
+ member_user_views.extend(
+ sorted(list(member_dict.values()), key=lambda u: u.email))
+
+ group_view = sitewide_views.GroupView(
+ mr.viewed_user_auth.email, len(member_ids), group_settings,
+ mr.viewed_user_auth.user_id)
+ url_params = [(name, mr.GetParam(name)) for name in
+ framework_helpers.RECOGNIZED_PARAMS]
+ pagination = paginate.ArtifactPagination(
+ member_user_views, mr.GetPositiveIntParam('num', MEMBERS_PER_PAGE),
+ mr.GetPositiveIntParam('start'), mr.project_name, group_view.detail_url,
+ url_params=url_params)
+
+ is_imported_group = bool(group_settings.ext_group_type)
+
+ offer_membership_editing = permissions.CanEditGroup(
+ mr.perms, mr.auth.effective_ids, owner_ids) and not is_imported_group
+
+ group_type = 'Monorail user group'
+ if group_settings.ext_group_type:
+ group_type = str(group_settings.ext_group_type).capitalize()
+
+ return {
+ 'admin_tab_mode': self.ADMIN_TAB_META,
+ 'offer_membership_editing': ezt.boolean(offer_membership_editing),
+ 'initial_add_members': '',
+ 'initially_expand_form': ezt.boolean(False),
+ 'groupid': group_id,
+ 'groupname': mr.viewed_username,
+ 'settings': group_settings,
+ 'group_type': group_type,
+ 'pagination': pagination,
+ }
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ _, owner_ids_dict = self.services.usergroup.LookupMembers(
+ mr.cnxn, [mr.viewed_user_auth.user_id])
+ owner_ids = owner_ids_dict[mr.viewed_user_auth.user_id]
+ permit_edit = permissions.CanEditGroup(
+ mr.perms, mr.auth.effective_ids, owner_ids)
+ if not permit_edit:
+ raise permissions.PermissionException(
+ 'User is not permitted to edit group membership')
+
+ group_settings = self.services.usergroup.GetGroupSettings(
+ mr.cnxn, mr.viewed_user_auth.user_id)
+ if bool(group_settings.ext_group_type):
+ raise permissions.PermissionException(
+ 'Imported groups are read-only')
+
+ if 'addbtn' in post_data:
+ return self.ProcessAddMembers(mr, post_data)
+ elif 'removebtn' in post_data:
+ return self.ProcessRemoveMembers(mr, post_data)
+
+ def ProcessAddMembers(self, mr, post_data):
+ """Process the user's request to add members.
+
+ Args:
+ mr: common information parsed from the HTTP request.
+ post_data: dictionary of form data.
+
+ Returns:
+ String URL to redirect the user to after processing.
+ """
+ # 1. Gather data from the request.
+ group_id = mr.viewed_user_auth.user_id
+ add_members_str = post_data.get('addmembers')
+ new_member_ids = project_helpers.ParseUsernames(
+ mr.cnxn, self.services.user, add_members_str)
+ role = post_data['role']
+
+ # 2. Call services layer to save changes.
+ if not mr.errors.AnyErrors():
+ try:
+ self.services.usergroup.UpdateMembers(
+ mr.cnxn, group_id, new_member_ids, role)
+ except exceptions.CircularGroupException:
+ mr.errors.addmembers = (
+ 'The members are already ancestors of current group.')
+
+ # 3. Determine the next page in the UI flow.
+ if mr.errors.AnyErrors():
+ self.PleaseCorrect(
+ mr, initial_add_members=add_members_str,
+ initially_expand_form=ezt.boolean(True))
+ else:
+ return framework_helpers.FormatAbsoluteURL(
+ mr, '/g/%s/' % mr.viewed_username, include_project=False,
+ saved=1, ts=int(time.time()))
+
+ def ProcessRemoveMembers(self, mr, post_data):
+ """Process the user's request to remove members.
+
+ Args:
+ mr: common information parsed from the HTTP request.
+ post_data: dictionary of form data.
+
+ Returns:
+ String URL to redirect the user to after processing.
+ """
+ # 1. Gather data from the request.
+ remove_strs = post_data.getall('remove')
+ logging.info('remove_strs = %r', remove_strs)
+
+ if not remove_strs:
+ mr.errors.remove = 'No users specified'
+
+ # 2. Call services layer to save changes.
+ if not mr.errors.AnyErrors():
+ remove_ids = set(
+ self.services.user.LookupUserIDs(mr.cnxn, remove_strs).values())
+ self.services.usergroup.RemoveMembers(
+ mr.cnxn, mr.viewed_user_auth.user_id, remove_ids)
+
+ # 3. Determine the next page in the UI flow.
+ if mr.errors.AnyErrors():
+ self.PleaseCorrect(mr)
+ else:
+ return framework_helpers.FormatAbsoluteURL(
+ mr, '/g/%s/' % mr.viewed_username, include_project=False,
+ saved=1, ts=int(time.time()))
diff --git a/sitewide/grouplist.py b/sitewide/grouplist.py
new file mode 100644
index 0000000..3adfaa3
--- /dev/null
+++ b/sitewide/grouplist.py
@@ -0,0 +1,78 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed 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 list user groups."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+import ezt
+
+from framework import framework_helpers
+from framework import permissions
+from framework import servlet
+from framework import urls
+from framework import xsrf
+from sitewide import sitewide_views
+
+
+class GroupList(servlet.Servlet):
+ """Shows a page with a simple form to create a user group."""
+
+ _PAGE_TEMPLATE = 'sitewide/group-list-page.ezt'
+
+ def AssertBasePermission(self, mr):
+ """Assert that the user has the permissions needed to view this page."""
+ super(GroupList, self).AssertBasePermission(mr)
+
+ if not mr.perms.HasPerm(permissions.VIEW_GROUP, None, None):
+ raise permissions.PermissionException(
+ 'User is not allowed to view list of user groups')
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ group_views = [
+ sitewide_views.GroupView(*groupinfo) for groupinfo in
+ self.services.usergroup.GetAllUserGroupsInfo(mr.cnxn)]
+ group_views.sort(key=lambda gv: gv.name)
+ offer_group_deletion = mr.perms.CanUsePerm(
+ permissions.DELETE_GROUP, mr.auth.effective_ids, None, [])
+ offer_group_creation = mr.perms.CanUsePerm(
+ permissions.CREATE_GROUP, mr.auth.effective_ids, None, [])
+
+ return {
+ 'form_token': xsrf.GenerateToken(
+ mr.auth.user_id, '%s.do' % urls.GROUP_DELETE),
+ 'groups': group_views,
+ 'offer_group_deletion': ezt.boolean(offer_group_deletion),
+ 'offer_group_creation': ezt.boolean(offer_group_creation),
+ }
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ if 'removebtn' in post_data:
+ return self.ProcessDeleteGroups(mr, post_data)
+
+ def ProcessDeleteGroups(self, mr, post_data):
+ """Process request to delete groups."""
+ if not mr.perms.CanUsePerm(
+ permissions.DELETE_GROUP, mr.auth.effective_ids, None, []):
+ raise permissions.PermissionException(
+ 'User is not permitted to delete groups')
+
+ remove_groups = [int(g) for g in post_data.getall('remove')]
+
+ if not mr.errors.AnyErrors():
+ self.services.usergroup.DeleteGroups(mr.cnxn, remove_groups)
+
+ if mr.errors.AnyErrors():
+ self.PleaseCorrect(mr)
+ else:
+ return framework_helpers.FormatAbsoluteURL(
+ mr, '/g', include_project=False,
+ saved=1, ts=int(time.time()))
diff --git a/sitewide/hostinghome.py b/sitewide/hostinghome.py
new file mode 100644
index 0000000..4a0a47d
--- /dev/null
+++ b/sitewide/hostinghome.py
@@ -0,0 +1,107 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""A class to display the hosting home page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import ezt
+
+import settings
+from businesslogic import work_env
+from framework import exceptions
+from framework import permissions
+from framework import servlet
+from framework import template_helpers
+from framework import urls
+from project import project_views
+from sitewide import projectsearch
+from sitewide import sitewide_helpers
+
+
+class HostingHome(servlet.Servlet):
+ """HostingHome shows the project list and link to create a project."""
+
+ _PAGE_TEMPLATE = 'sitewide/hosting-home-page.ezt'
+
+ 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.
+ """
+ redirect_msg = self._MaybeRedirectToDomainDefaultProject(mr)
+ logging.info(redirect_msg)
+
+ can_create_project = permissions.CanCreateProject(mr.perms)
+
+ # Kick off the search pipeline, it has its own promises for parallelism.
+ pipeline = projectsearch.ProjectSearchPipeline(mr, self.services)
+
+ # Meanwhile, determine which projects the signed-in user has starred.
+ with work_env.WorkEnv(mr, self.services) as we:
+ starred_projects = we.ListStarredProjects()
+ starred_project_ids = {p.project_id for p in starred_projects}
+
+ # A dict of project id to the user's membership status.
+ project_memberships = {}
+ if mr.auth.user_id:
+ with work_env.WorkEnv(mr, self.services) as we:
+ owned, _archive_owned, member_of, contrib_of = (
+ we.GetUserProjects(mr.auth.effective_ids))
+ project_memberships.update({proj.project_id: 'Owner' for proj in owned})
+ project_memberships.update(
+ {proj.project_id: 'Member' for proj in member_of})
+ project_memberships.update(
+ {proj.project_id: 'Contributor' for proj in contrib_of})
+
+ # Finish the project search pipeline.
+ pipeline.SearchForIDs(domain=mr.request.host)
+ pipeline.GetProjectsAndPaginate(mr.cnxn, urls.HOSTING_HOME)
+ project_ids = [p.project_id for p in pipeline.visible_results]
+ star_count_dict = self.services.project_star.CountItemsStars(
+ mr.cnxn, project_ids)
+
+ # Make ProjectView objects
+ project_view_list = [
+ project_views.ProjectView(
+ p, starred=p.project_id in starred_project_ids,
+ num_stars=star_count_dict.get(p.project_id),
+ membership_desc=project_memberships.get(p.project_id))
+ for p in pipeline.visible_results]
+ return {
+ 'can_create_project': ezt.boolean(can_create_project),
+ 'learn_more_link': settings.learn_more_link,
+ 'projects': project_view_list,
+ 'pagination': pipeline.pagination,
+ }
+
+ def _MaybeRedirectToDomainDefaultProject(self, mr):
+ """If there is a relevant default project, redirect to it."""
+ project_name = settings.domain_to_default_project.get(mr.request.host)
+ if not project_name:
+ return 'No configured default project redirect for this domain.'
+
+ project = None
+ try:
+ project = self.services.project.GetProjectByName(mr.cnxn, project_name)
+ except exceptions.NoSuchProjectException:
+ pass
+
+ if not project:
+ return 'Domain default project %s not found' % project_name
+
+ if not permissions.UserCanViewProject(
+ mr.auth.user_pb, mr.auth.effective_ids, project):
+ return 'User cannot view default project: %r' % project
+
+ project_url = '/p/%s' % project_name
+ self.redirect(project_url, abort=True)
+ return 'Redirected to %r' % project_url
diff --git a/sitewide/moved.py b/sitewide/moved.py
new file mode 100644
index 0000000..3f63d24
--- /dev/null
+++ b/sitewide/moved.py
@@ -0,0 +1,62 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""A class to display a message explaining that a project has moved.
+
+When a project moves, we just display a link to the new location.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+from framework import exceptions
+from framework import framework_helpers
+from framework import servlet
+from framework import urls
+from project import project_constants
+
+
+class ProjectMoved(servlet.Servlet):
+ """The ProjectMoved page explains that the project has moved."""
+
+ _PAGE_TEMPLATE = 'sitewide/moved-page.ezt'
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+
+ # We are not actually in /p/PROJECTNAME, so mr.project_name is None.
+ # Putting the ProjectMoved page inside a moved project would make
+ # the redirect logic much more complicated.
+ if not mr.specified_project:
+ raise exceptions.InputException('No project specified')
+
+ project = self.services.project.GetProjectByName(
+ mr.cnxn, mr.specified_project)
+ if not project:
+ self.abort(404, 'project not found')
+
+ if not project.moved_to:
+ # Only show this page for projects that are actually moved.
+ # Don't allow hackers to construct misleading links to this servlet.
+ logging.info('attempt to view ProjectMoved for non-moved project: %s',
+ mr.specified_project)
+ self.abort(400, 'This project has not been moved')
+
+ if project_constants.RE_PROJECT_NAME.match(project.moved_to):
+ moved_to_url = framework_helpers.FormatAbsoluteURL(
+ mr, urls.SUMMARY, include_project=True, project_name=project.moved_to)
+ elif (project.moved_to.startswith('https://') or
+ project.moved_to.startswith('http://')):
+ moved_to_url = project.moved_to
+ else:
+ # Prevent users from using javascript: or any other tricky URL scheme.
+ moved_to_url = '#invalid-destination-url'
+
+ return {
+ 'project_name': mr.specified_project,
+ 'moved_to_url': moved_to_url,
+ }
diff --git a/sitewide/projectcreate.py b/sitewide/projectcreate.py
new file mode 100644
index 0000000..83862f6
--- /dev/null
+++ b/sitewide/projectcreate.py
@@ -0,0 +1,157 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Classes for users to create a new project."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+
+import logging
+from six import string_types
+import ezt
+
+import settings
+from businesslogic import work_env
+from framework import exceptions
+from framework import filecontent
+from framework import framework_helpers
+from framework import gcs_helpers
+from framework import jsonfeed
+from framework import permissions
+from framework import servlet
+from framework import urls
+from project import project_constants
+from project import project_helpers
+from project import project_views
+from services import project_svc
+from tracker import tracker_bizobj
+from tracker import tracker_views
+
+
+_MSG_PROJECT_NAME_NOT_AVAIL = 'That project name is not available.'
+_MSG_MISSING_PROJECT_NAME = 'Missing project name'
+_MSG_INVALID_PROJECT_NAME = 'Invalid project name'
+_MSG_MISSING_PROJECT_SUMMARY = 'Missing project summary'
+
+
+class ProjectCreate(servlet.Servlet):
+ """Shows a page with a simple form to create a project."""
+
+ _PAGE_TEMPLATE = 'sitewide/project-create-page.ezt'
+
+ def AssertBasePermission(self, mr):
+ """Assert that the user has the permissions needed to view this page."""
+ super(ProjectCreate, self).AssertBasePermission(mr)
+
+ if not permissions.CanCreateProject(mr.perms):
+ raise permissions.PermissionException(
+ 'User is not allowed to create a project')
+
+ def GatherPageData(self, _mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ available_access_levels = project_helpers.BuildProjectAccessOptions(None)
+ offer_access_level = len(available_access_levels) > 1
+ if settings.default_access_level:
+ access_view = project_views.ProjectAccessView(
+ settings.default_access_level)
+ else:
+ access_view = None
+
+ return {
+ 'initial_name': '',
+ 'initial_summary': '',
+ 'initial_description': '',
+ 'initial_project_home': '',
+ 'initial_docs_url': '',
+ 'initial_source_url': '',
+ 'initial_logo_gcs_id': '',
+ 'initial_logo_file_name': '',
+ 'logo_view': tracker_views.LogoView(None),
+ 'labels': [],
+ 'max_project_name_length': project_constants.MAX_PROJECT_NAME_LENGTH,
+ 'offer_access_level': ezt.boolean(offer_access_level),
+ 'initial_access': access_view,
+ 'available_access_levels': available_access_levels,
+ }
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ # 1. Parse and validate user input.
+ # Project name is taken from post_data because we are creating it.
+ project_name = post_data.get('projectname')
+ if not project_name:
+ mr.errors.projectname = _MSG_MISSING_PROJECT_NAME
+ elif not project_helpers.IsValidProjectName(project_name):
+ mr.errors.projectname = _MSG_INVALID_PROJECT_NAME
+
+ summary = post_data.get('summary')
+ if not summary:
+ mr.errors.summary = _MSG_MISSING_PROJECT_SUMMARY
+ description = post_data.get('description', '')
+
+ access = project_helpers.ParseProjectAccess(None, post_data.get('access'))
+ home_page = post_data.get('project_home')
+ if home_page and not (
+ home_page.startswith('http://') or home_page.startswith('https://')):
+ mr.errors.project_home = 'Home page link must start with http(s)://'
+ docs_url = post_data.get('docs_url')
+ if docs_url and not (
+ docs_url.startswith('http:') or docs_url.startswith('https:')):
+ mr.errors.docs_url = 'Documentation link must start with http: or https:'
+
+ # These are not specified on via the ProjectCreate form,
+ # the user must edit the project after creation to set them.
+ committer_ids = []
+ contributor_ids = []
+
+ # Validate that provided logo is supported.
+ logo_provided = 'logo' in post_data and not isinstance(
+ post_data['logo'], string_types)
+ if logo_provided:
+ item = post_data['logo']
+ try:
+ gcs_helpers.CheckMimeTypeResizable(
+ filecontent.GuessContentTypeFromFilename(item.filename))
+ except gcs_helpers.UnsupportedMimeType, e:
+ mr.errors.logo = e.message
+
+ # 2. Call services layer to save changes.
+ if not mr.errors.AnyErrors():
+ with work_env.WorkEnv(mr, self.services) as we:
+ try:
+ project_id = we.CreateProject(
+ project_name, [mr.auth.user_id],
+ committer_ids, contributor_ids, summary, description,
+ access=access, home_page=home_page, docs_url=docs_url)
+
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(project_id)
+ self.services.config.StoreConfig(mr.cnxn, config)
+ # Note: No need to store any canned queries or rules yet.
+ self.services.issue.InitializeLocalID(mr.cnxn, project_id)
+
+ # Update project with logo if specified.
+ if logo_provided:
+ item = post_data['logo']
+ logo_file_name = item.filename
+ logo_gcs_id = gcs_helpers.StoreLogoInGCS(
+ logo_file_name, item.value, project_id)
+ we.UpdateProject(
+ project_id, logo_gcs_id=logo_gcs_id,
+ logo_file_name=logo_file_name)
+
+ except exceptions.ProjectAlreadyExists:
+ mr.errors.projectname = _MSG_PROJECT_NAME_NOT_AVAIL
+
+ # 3. Determine the next page in the UI flow.
+ if mr.errors.AnyErrors():
+ access_view = project_views.ProjectAccessView(access)
+ self.PleaseCorrect(
+ mr, initial_summary=summary, initial_description=description,
+ initial_name=project_name, initial_access=access_view)
+ else:
+ # Go to the new project's introduction page.
+ return framework_helpers.FormatAbsoluteURL(
+ mr, urls.ADMIN_INTRO, project_name=project_name)
diff --git a/sitewide/projectsearch.py b/sitewide/projectsearch.py
new file mode 100644
index 0000000..8ef5fee
--- /dev/null
+++ b/sitewide/projectsearch.py
@@ -0,0 +1,63 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Helper functions and classes used when searching for projects."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+from businesslogic import work_env
+from framework import framework_helpers
+from framework import paginate
+from framework import permissions
+
+
+DEFAULT_RESULTS_PER_PAGE = 100
+
+
+class ProjectSearchPipeline(object):
+ """Manage the process of project search, filter, fetch, and pagination."""
+
+ def __init__(self, mr, services,
+ default_results_per_page=DEFAULT_RESULTS_PER_PAGE):
+
+ self.mr = mr
+ self.services = services
+ self.default_results_per_page = default_results_per_page
+ self.pagination = None
+ self.allowed_project_ids = None
+ self.visible_results = None
+
+ def SearchForIDs(self, domain=None):
+ """Get project IDs the user has permission to view."""
+ with work_env.WorkEnv(self.mr, self.services) as we:
+ self.allowed_project_ids = we.ListProjects(domain=domain)
+ logging.info('allowed_project_ids is %r', self.allowed_project_ids)
+
+ def GetProjectsAndPaginate(self, cnxn, list_page_url):
+ """Paginate the filtered list of project names and retrieve Project PBs.
+
+ Args:
+ cnxn: connection to SQL database.
+ list_page_url: string page URL for prev and next links.
+ """
+ with self.mr.profiler.Phase('getting all projects'):
+ project_dict = self.services.project.GetProjects(
+ cnxn, self.allowed_project_ids)
+ project_list = sorted(
+ project_dict.values(),
+ key=lambda p: p.project_name)
+ logging.info('project_list is %r', project_list)
+
+ url_params = [(name, self.mr.GetParam(name)) for name in
+ framework_helpers.RECOGNIZED_PARAMS]
+ self.pagination = paginate.ArtifactPagination(
+ project_list,
+ self.mr.GetPositiveIntParam('num', self.default_results_per_page),
+ self.mr.GetPositiveIntParam('start'), self.mr.project_name,
+ list_page_url, url_params=url_params)
+ self.visible_results = self.pagination.visible_results
diff --git a/sitewide/sitewide_helpers.py b/sitewide/sitewide_helpers.py
new file mode 100644
index 0000000..33f53c3
--- /dev/null
+++ b/sitewide/sitewide_helpers.py
@@ -0,0 +1,38 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Helper functions used in sitewide servlets."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+from framework import permissions
+from proto import project_pb2
+
+
+def GetViewableStarredProjects(
+ cnxn, services, viewed_user_id, effective_ids, logged_in_user):
+ """Returns a list of viewable starred projects."""
+ starred_project_ids = services.project_star.LookupStarredItemIDs(
+ cnxn, viewed_user_id)
+ projects = list(
+ services.project.GetProjects(cnxn, starred_project_ids).values())
+ viewable_projects = FilterViewableProjects(
+ projects, logged_in_user, effective_ids)
+ return viewable_projects
+
+
+def FilterViewableProjects(project_list, logged_in_user, effective_ids):
+ """Return subset of LIVE project protobufs viewable by the given user."""
+ viewable_projects = []
+ for project in project_list:
+ if (project.state == project_pb2.ProjectState.LIVE and
+ permissions.UserCanViewProject(
+ logged_in_user, effective_ids, project)):
+ viewable_projects.append(project)
+
+ return viewable_projects
diff --git a/sitewide/sitewide_views.py b/sitewide/sitewide_views.py
new file mode 100644
index 0000000..64b33bd
--- /dev/null
+++ b/sitewide/sitewide_views.py
@@ -0,0 +1,23 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""View objects to help display users and groups in UI templates."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+
+class GroupView(object):
+ """Class to make it easier to display user group metadata."""
+
+ def __init__(self, name, num_members, group_settings, group_id):
+ self.name = name
+ self.num_members = num_members
+ self.who_can_view_members = str(group_settings.who_can_view_members)
+ self.group_id = group_id
+
+ self.detail_url = '/g/%s/' % group_id
diff --git a/sitewide/test/__init__.py b/sitewide/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sitewide/test/__init__.py
diff --git a/sitewide/test/custom_404_test.py b/sitewide/test/custom_404_test.py
new file mode 100644
index 0000000..71b52f8
--- /dev/null
+++ b/sitewide/test/custom_404_test.py
@@ -0,0 +1,44 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for the custom_404 servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import httplib
+import unittest
+
+from framework import exceptions
+from services import service_manager
+from sitewide import custom_404
+from testing import fake
+from testing import testing_helpers
+
+
+class Custom404Test(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService())
+ self.servlet = custom_404.ErrorPage('req', 'res', services=self.services)
+
+ def testGatherPageData_NoProjectSpecified(self):
+ """Project was not included in URL, so raise exception, will cause 400."""
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/not/a/project/url')
+
+ with self.assertRaises(exceptions.InputException):
+ self.servlet.GatherPageData(mr)
+
+ def testGatherPageData_Normal(self):
+ """Return page_data dict with a 404 response code specified."""
+ _project = self.services.project.TestAddProject('proj')
+ _, mr = testing_helpers.GetRequestObjects(path='/p/proj/junk')
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(
+ {'http_response_code': httplib.NOT_FOUND},
+ page_data)
diff --git a/sitewide/test/group_helpers_test.py b/sitewide/test/group_helpers_test.py
new file mode 100644
index 0000000..af03d08
--- /dev/null
+++ b/sitewide/test/group_helpers_test.py
@@ -0,0 +1,51 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group helpers."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import user_pb2
+from proto import usergroup_pb2
+from sitewide import group_helpers
+
+
+class GroupHelpersTest(unittest.TestCase):
+
+ def testGroupVisibilityView(self):
+ gvv_anyone = group_helpers.GroupVisibilityView(
+ usergroup_pb2.MemberVisibility.ANYONE)
+ gvv_members = group_helpers.GroupVisibilityView(
+ usergroup_pb2.MemberVisibility.MEMBERS)
+ gvv_owners = group_helpers.GroupVisibilityView(
+ usergroup_pb2.MemberVisibility.OWNERS)
+ self.assertEqual('Anyone on the Internet', gvv_anyone.name)
+ self.assertEqual('Group Members', gvv_members.name)
+ self.assertEqual('Group Owners', gvv_owners.name)
+
+ def testGroupMemberView(self):
+ user = user_pb2.MakeUser(1, email='test@example.com')
+ gmv = group_helpers.GroupMemberView(user, 888, 'member')
+ self.assertEqual(888, gmv.group_id)
+ self.assertEqual('member', gmv.role)
+
+ def testBuildUserGroupVisibilityOptions(self):
+ vis_views = group_helpers.BuildUserGroupVisibilityOptions()
+ self.assertEqual(3, len(vis_views))
+
+ def testGroupTypeView(self):
+ gt_cia = group_helpers.GroupTypeView(
+ usergroup_pb2.GroupType.CHROME_INFRA_AUTH)
+ gt_mdb = group_helpers.GroupTypeView(
+ usergroup_pb2.GroupType.MDB)
+ self.assertEqual('Chrome-infra-auth', gt_cia.name)
+ self.assertEqual('MDB', gt_mdb.name)
+
+ def testBuildUserGroupTypeOptions(self):
+ group_types = group_helpers.BuildUserGroupTypeOptions()
+ self.assertEqual(4, len(group_types))
diff --git a/sitewide/test/groupadmin_test.py b/sitewide/test/groupadmin_test.py
new file mode 100644
index 0000000..d1f7e0f
--- /dev/null
+++ b/sitewide/test/groupadmin_test.py
@@ -0,0 +1,85 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group admin servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from framework import permissions
+from proto import usergroup_pb2
+from services import service_manager
+from sitewide import groupadmin
+from testing import fake
+from testing import testing_helpers
+
+
+class GrouAdminTest(unittest.TestCase):
+ """Tests for the GroupAdmin servlet."""
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService())
+ self.services.user.TestAddUser('a@example.com', 111)
+ self.services.user.TestAddUser('b@example.com', 222)
+ self.services.user.TestAddUser('c@example.com', 333)
+ self.services.user.TestAddUser('group@example.com', 888)
+ self.services.user.TestAddUser('importgroup@example.com', 999)
+ self.services.usergroup.TestAddGroupSettings(888, 'group@example.com')
+ self.services.usergroup.TestAddGroupSettings(
+ 999, 'importgroup@example.com', external_group_type='mdb')
+ self.servlet = groupadmin.GroupAdmin(
+ 'req', 'res', services=self.services)
+ self.mr = testing_helpers.MakeMonorailRequest()
+ self.mr.viewed_username = 'group@example.com'
+ self.mr.viewed_user_auth.user_id = 888
+
+ def testAssertBasePermission(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ mr.viewed_user_auth.user_id = 888
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+ self.services.usergroup.TestAddMembers(888, [111], 'owner')
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testGatherPageData_Normal(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual('group@example.com', page_data['groupname'])
+ self.assertEqual('Group Members', page_data['initial_visibility'].name)
+ self.assertEqual(3, len(page_data['visibility_levels']))
+
+ def testGatherPageData_Import(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.viewed_username = 'importgroup@example.com'
+ mr.viewed_user_auth.user_id = 999
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual('importgroup@example.com', page_data['groupname'])
+ self.assertTrue(page_data['import_group'])
+ self.assertEqual('MDB', page_data['initial_group_type'].name)
+
+ def testProcessFormData_Normal(self):
+ post_data = fake.PostData(visibility='0')
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertIn('/g/group@example.com/groupadmin', url)
+ group_settings = self.services.usergroup.GetGroupSettings(None, 888)
+ self.assertEqual(usergroup_pb2.MemberVisibility.OWNERS,
+ group_settings.who_can_view_members)
+
+ def testProcessFormData_Import(self):
+ post_data = fake.PostData(
+ group_type='1', import_group=['on'])
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertIn('/g/group@example.com/groupadmin', url)
+ group_settings = self.services.usergroup.GetGroupSettings(None, 888)
+ self.assertEqual(usergroup_pb2.MemberVisibility.OWNERS,
+ group_settings.who_can_view_members)
+ self.assertEqual(usergroup_pb2.GroupType.MDB,
+ group_settings.ext_group_type)
diff --git a/sitewide/test/groupcreate_test.py b/sitewide/test/groupcreate_test.py
new file mode 100644
index 0000000..bf7be8d
--- /dev/null
+++ b/sitewide/test/groupcreate_test.py
@@ -0,0 +1,101 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group creation servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import settings
+from framework import permissions
+from proto import site_pb2
+from proto import usergroup_pb2
+from services import service_manager
+from sitewide import groupcreate
+from testing import fake
+from testing import testing_helpers
+
+
+class GroupCreateTest(unittest.TestCase):
+ """Tests for the GroupCreate servlet."""
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project=fake.ProjectService())
+ self.servlet = groupcreate.GroupCreate(
+ 'req', 'res', services=self.services)
+ self.mr = testing_helpers.MakeMonorailRequest()
+
+ def CheckAssertBasePermissions(
+ self, restriction, expect_admin_ok, expect_nonadmin_ok):
+ old_group_creation_restriction = settings.group_creation_restriction
+ settings.group_creation_restriction = restriction
+
+ # Anon users can never do it
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ mr = testing_helpers.MakeMonorailRequest()
+ if expect_admin_ok:
+ self.servlet.AssertBasePermission(mr)
+ else:
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(mr.auth.user_pb, {111}, None))
+ if expect_nonadmin_ok:
+ self.servlet.AssertBasePermission(mr)
+ else:
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ settings.group_creation_restriction = old_group_creation_restriction
+
+ def testAssertBasePermission(self):
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.ANYONE, True, True)
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.ADMIN_ONLY, True, False)
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.NO_ONE, False, False)
+
+ def testGatherPageData(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual('', page_data['initial_name'])
+
+ def testProcessFormData_Normal(self):
+ post_data = fake.PostData(
+ groupname=['group@example.com'], visibility='1')
+ url = self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertIn('/g/3444127190/', url)
+ group_id = self.services.user.LookupUserID('cnxn', 'group@example.com')
+ group_settings = self.services.usergroup.GetGroupSettings('cnxn', group_id)
+ self.assertIsNotNone(group_settings)
+ members_after, owners_after = self.services.usergroup.LookupMembers(
+ 'cnxn', [group_id])
+ self.assertEqual(0, len(members_after[group_id] + owners_after[group_id]))
+
+ def testProcessFormData_Import(self):
+ post_data = fake.PostData(
+ groupname=['group@example.com'], group_type='1',
+ import_group=['on'])
+ self.servlet.ProcessFormData(self.mr, post_data)
+ group_id = self.services.user.LookupUserID('cnxn', 'group@example.com')
+ group_settings = self.services.usergroup.GetGroupSettings('cnxn', group_id)
+ self.assertIsNotNone(group_settings)
+ self.assertEqual(usergroup_pb2.MemberVisibility.OWNERS,
+ group_settings.who_can_view_members)
+ self.assertEqual(usergroup_pb2.GroupType.MDB,
+ group_settings.ext_group_type)
diff --git a/sitewide/test/groupdetail_test.py b/sitewide/test/groupdetail_test.py
new file mode 100644
index 0000000..4440bb8
--- /dev/null
+++ b/sitewide/test/groupdetail_test.py
@@ -0,0 +1,146 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group Detail servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from framework import exceptions
+from framework import permissions
+from services import service_manager
+from sitewide import groupdetail
+from testing import fake
+from testing import testing_helpers
+
+
+class GroupDetailTest(unittest.TestCase):
+ """Tests for the GroupDetail servlet."""
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService())
+ self.services.user.TestAddUser('a@example.com', 111)
+ self.services.user.TestAddUser('b@example.com', 222)
+ self.services.user.TestAddUser('c@example.com', 333)
+ self.services.user.TestAddUser('group@example.com', 888)
+ self.services.usergroup.TestAddGroupSettings(888, 'group@example.com')
+ self.servlet = groupdetail.GroupDetail(
+ 'req', 'res', services=self.services)
+ self.mr = testing_helpers.MakeMonorailRequest()
+ self.mr.viewed_username = 'group@example.com'
+ self.mr.viewed_user_auth.user_id = 888
+
+ def testAssertBasePermission(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ mr.viewed_user_auth.user_id = 888
+ mr.auth.effective_ids = set([111])
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+ self.services.usergroup.TestAddMembers(888, [111], 'member')
+ self.servlet.AssertBasePermission(mr)
+
+ def testAssertBasePermission_IgnoreNoSuchGroup(self):
+ """The permission check does not crash for non-existent user groups."""
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ mr.viewed_user_auth.user_id = 404
+ mr.auth.effective_ids = set([111])
+ self.servlet.AssertBasePermission(mr)
+
+ def testAssertBasePermission_IndirectMembership(self):
+ self.services.usergroup.TestAddGroupSettings(999, 'subgroup@example.com')
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ mr.viewed_user_auth.user_id = 888
+ mr.auth.effective_ids = set([111])
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+ self.services.usergroup.TestAddMembers(888, [999], 'member')
+ self.services.usergroup.TestAddMembers(999, [111], 'member')
+ self.servlet.AssertBasePermission(mr)
+
+ def testGatherPagData_ZeroMembers(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ pagination = page_data['pagination']
+ self.assertEqual(0, len(pagination.visible_results))
+
+ def testGatherPagData_NonzeroMembers(self):
+ self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+ page_data = self.servlet.GatherPageData(self.mr)
+ pagination = page_data['pagination']
+ self.assertEqual(3, len(pagination.visible_results))
+ self.assertEqual(3, pagination.total_count)
+ self.assertEqual(1, pagination.start)
+ self.assertEqual(3, pagination.last)
+ user_view_a, user_view_b, user_view_c = pagination.visible_results
+ self.assertEqual('a@example.com', user_view_a.email)
+ self.assertEqual('b@example.com', user_view_b.email)
+ self.assertEqual('c@example.com', user_view_c.email)
+
+ def testProcessAddMembers_NoneAdded(self):
+ post_data = fake.PostData(addmembers=[''], role=['member'])
+ url = self.servlet.ProcessAddMembers(self.mr, post_data)
+ self.assertIn('/g/group@example.com/?', url)
+ members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+ self.assertEqual(0, len(members_after[888]))
+
+ self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+ url = self.servlet.ProcessAddMembers(self.mr, post_data)
+ self.assertIn('/g/group@example.com/?', url)
+ members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+ self.assertEqual(3, len(members_after[888]))
+
+ def testProcessAddMembers_SomeAdded(self):
+ self.services.usergroup.TestAddMembers(888, [111])
+ post_data = fake.PostData(
+ addmembers=['b@example.com, c@example.com'], role=['member'])
+ url = self.servlet.ProcessAddMembers(self.mr, post_data)
+ self.assertIn('/g/group@example.com/?', url)
+ members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+ self.assertEqual(3, len(members_after[888]))
+
+ def testProcessRemoveMembers_SomeRemoved(self):
+ self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+ post_data = fake.PostData(remove=['b@example.com', 'c@example.com'])
+ url = self.servlet.ProcessRemoveMembers(self.mr, post_data)
+ self.assertIn('/g/group@example.com/?', url)
+ members_after, _ = self.services.usergroup.LookupMembers('cnxn', [888])
+ self.assertEqual(1, len(members_after[888]))
+
+ def testProcessFormData_NoPermission(self):
+ """Group members cannot edit group."""
+ self.services.usergroup.TestAddMembers(888, [111], 'member')
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ mr.viewed_user_auth.user_id = 888
+ mr.auth.effective_ids = set([111])
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.ProcessFormData, mr, {})
+
+ def testProcessFormData_OwnerPermission(self):
+ """Group owners cannot edit group."""
+ self.services.usergroup.TestAddMembers(888, [111], 'owner')
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ mr.viewed_user_auth.user_id = 888
+ mr.auth.effective_ids = set([111])
+ self.servlet.ProcessFormData(mr, {})
+
+ def testGatherPagData_NoSuchUserGroup(self):
+ """If there is no such user group, raise an exception."""
+ self.mr.viewed_user_auth.user_id = 404
+ self.assertRaises(
+ exceptions.NoSuchGroupException,
+ self.servlet.GatherPageData, self.mr)
+
+
diff --git a/sitewide/test/grouplist_test.py b/sitewide/test/grouplist_test.py
new file mode 100644
index 0000000..9ec6bd5
--- /dev/null
+++ b/sitewide/test/grouplist_test.py
@@ -0,0 +1,84 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit test for User Group List servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from google.appengine.ext import testbed
+
+from framework import permissions
+from services import service_manager
+from sitewide import grouplist
+from testing import fake
+from testing import testing_helpers
+
+
+class GroupListTest(unittest.TestCase):
+ """Tests for the GroupList servlet."""
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ usergroup=fake.UserGroupService())
+ self.servlet = grouplist.GroupList('req', 'res', services=self.services)
+ self.mr = testing_helpers.MakeMonorailRequest()
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ def tearDown(self):
+ self.testbed.deactivate()
+
+ def testAssertBasePermission_Anon(self):
+ self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
+ with self.assertRaises(permissions.PermissionException):
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testAssertBasePermission_RegularUsers(self):
+ self.mr.perms = permissions.COMMITTER_ACTIVE_PERMISSIONSET
+ with self.assertRaises(permissions.PermissionException):
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testAssertBasePermission_SiteAdmin(self):
+ self.mr.perms = permissions.ADMIN_PERMISSIONSET
+ self.servlet.AssertBasePermission(self.mr)
+
+ def testGatherPagData_ZeroGroups(self):
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual([], page_data['groups'])
+
+ def testGatherPagData_NonzeroGroups(self):
+ self.services.usergroup.TestAddGroupSettings(777, 'group_a@example.com')
+ self.services.usergroup.TestAddGroupSettings(888, 'group_b@example.com')
+ self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+ page_data = self.servlet.GatherPageData(self.mr)
+ group_view_a, group_view_b = page_data['groups']
+ self.assertEqual('group_a@example.com', group_view_a.name)
+ self.assertEqual(0, group_view_a.num_members)
+ self.assertEqual('group_b@example.com', group_view_b.name)
+ self.assertEqual(3, group_view_b.num_members)
+
+ def testProcessFormData_NoPermission(self):
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.USER_PERMISSIONSET)
+ post_data = fake.PostData(
+ removebtn=[1])
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.ProcessFormData, mr, post_data)
+
+ def testProcessFormData_Normal(self):
+ self.services.usergroup.TestAddGroupSettings(
+ 888, 'group_b@example.com', friend_projects=[789])
+ self.services.usergroup.TestAddMembers(888, [111, 222, 333])
+
+ post_data = fake.PostData(
+ remove=[888],
+ removebtn=[1])
+ self.servlet.ProcessFormData(self.mr, post_data)
+ self.assertNotIn(888, self.services.usergroup.group_settings)
diff --git a/sitewide/test/hostinghome_test.py b/sitewide/test/hostinghome_test.py
new file mode 100644
index 0000000..f51c9ec
--- /dev/null
+++ b/sitewide/test/hostinghome_test.py
@@ -0,0 +1,146 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the Monorail home page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+
+import ezt
+
+import settings
+from framework import permissions
+from proto import project_pb2
+from proto import site_pb2
+from services import service_manager
+from sitewide import hostinghome
+from sitewide import projectsearch
+from testing import fake
+from testing import testing_helpers
+
+
+class MockProjectSearchPipeline(object):
+
+ def __init__(self, _mr, services):
+ self.visible_results = services.mock_visible_results
+ self.pagination = None
+
+ def SearchForIDs(self, domain=None):
+ pass
+
+ def GetProjectsAndPaginate(self, cnxn, list_page_url):
+ pass
+
+
+class HostingHomeTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ project_star=fake.ProjectStarService())
+ self.services.mock_visible_results = []
+ self.project_a = self.services.project.TestAddProject('a', project_id=1)
+ self.project_b = self.services.project.TestAddProject('b', project_id=2)
+
+ self.servlet = hostinghome.HostingHome('req', 'res', services=self.services)
+ self.mr = testing_helpers.MakeMonorailRequest(user_info={'user_id': 111})
+
+ self.orig_pipeline_class = projectsearch.ProjectSearchPipeline
+ projectsearch.ProjectSearchPipeline = MockProjectSearchPipeline
+
+ def tearDown(self):
+ projectsearch.ProjectSearchPipeline = self.orig_pipeline_class
+
+ def testSearch_ZeroResults(self):
+ self.services.mock_visible_results = []
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual([], page_data['projects'])
+
+ def testSearch_NonzeroResults(self):
+ self.services.mock_visible_results = [self.project_a, self.project_b]
+ page_data = self.servlet.GatherPageData(self.mr)
+ self.assertEqual(['a', 'b'],
+ [pv.project_name for pv in page_data['projects']])
+
+ def testStarCounts(self):
+ """Test the display of star counts on each displayed project."""
+ self.services.mock_visible_results = [self.project_a, self.project_b]
+ # We go straight to the services layer because this is a test set up
+ # rather than an actual user request.
+ self.services.project_star.SetStar('fake cnxn', 1, 111, True)
+ self.services.project_star.SetStar('fake cnxn', 1, 222, True)
+ page_data = self.servlet.GatherPageData(self.mr)
+ project_view_a, project_view_b = page_data['projects']
+ self.assertEqual(2, project_view_a.num_stars)
+ self.assertEqual(0, project_view_b.num_stars)
+
+ def testStarredProjects(self):
+ self.services.mock_visible_results = [self.project_a, self.project_b]
+ self.services.project_star.SetStar('fake cnxn', 1, 111, True)
+ page_data = self.servlet.GatherPageData(self.mr)
+ project_view_a, project_view_b = page_data['projects']
+ self.assertTrue(project_view_a.starred)
+ self.assertFalse(project_view_b.starred)
+
+ def testGatherPageData(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(settings.learn_more_link, page_data['learn_more_link'])
+
+ def testGatherPageData_CanCreateProject(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.perms = permissions.PermissionSet([permissions.CREATE_PROJECT])
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(
+ ezt.boolean(settings.project_creation_restriction ==
+ site_pb2.UserTypeRestriction.ANYONE),
+ page_data['can_create_project'])
+
+ mr.perms = permissions.PermissionSet([])
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual(ezt.boolean(False), page_data['can_create_project'])
+
+ @mock.patch('settings.domain_to_default_project', {})
+ def testMaybeRedirectToDomainDefaultProject_NoMatch(self):
+ """No redirect if the user is not accessing via a configured domain."""
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.startswith('No configured'))
+
+ @mock.patch('settings.domain_to_default_project', {'example.com': 'huh'})
+ def testMaybeRedirectToDomainDefaultProject_NoSuchProject(self):
+ """No redirect if the configured project does not exist."""
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ print('host is %r' % mr.request.host)
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.endswith('not found'))
+
+ @mock.patch('settings.domain_to_default_project', {'example.com': 'a'})
+ def testMaybeRedirectToDomainDefaultProject_CantView(self):
+ """No redirect if the user can't view the configured project."""
+ self.project_a.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.startswith('User cannot'))
+
+ @mock.patch('settings.domain_to_default_project', {'example.com': 'a'})
+ def testMaybeRedirectToDomainDefaultProject_Redirect(self):
+ """We redirect if there's a configured project that the user can view."""
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.request.host = 'example.com'
+ self.servlet.redirect = mock.Mock()
+ msg = self.servlet._MaybeRedirectToDomainDefaultProject(mr)
+ print('msg: ' + msg)
+ self.assertTrue(msg.startswith('Redirected'))
+ self.servlet.redirect.assert_called_once()
diff --git a/sitewide/test/moved_test.py b/sitewide/test/moved_test.py
new file mode 100644
index 0000000..04b9165
--- /dev/null
+++ b/sitewide/test/moved_test.py
@@ -0,0 +1,113 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for the moved project notification page servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import webapp2
+
+from framework import exceptions
+from services import service_manager
+from sitewide import moved
+from testing import fake
+from testing import testing_helpers
+
+
+class MovedTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService())
+ self.servlet = moved.ProjectMoved('req', 'res', services=self.services)
+ self.old_project = 'old-project'
+
+ def testGatherPageData_NoProjectSpecified(self):
+ # Project was not included in URL, so raise exception, will cause 400.
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/hosting/moved')
+
+ with self.assertRaises(exceptions.InputException):
+ self.servlet.GatherPageData(mr)
+
+ def testGatherPageData_NoSuchProject(self):
+ # Project doesn't exist, so 404 NOT FOUND.
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/hosting/moved?project=nonexistent')
+
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet.GatherPageData(mr)
+ self.assertEqual(404, cm.exception.code)
+
+ def testGatherPageData_NotMoved(self):
+ # Project exists but has not been moved, so 400 BAD_REQUEST.
+ self.services.project.TestAddProject(self.old_project)
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/hosting/moved?project=%s' % self.old_project)
+
+ with self.assertRaises(webapp2.HTTPException) as cm:
+ self.servlet.GatherPageData(mr)
+ self.assertEqual(400, cm.exception.code)
+
+ def testGatherPageData_URL(self):
+ # Display the moved_to url if it is valid.
+ project = self.services.project.TestAddProject(self.old_project)
+ project.moved_to = 'https://other-tracker.bugs'
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/hosting/moved?project=%s' % self.old_project)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertItemsEqual(
+ ['project_name', 'moved_to_url'],
+ list(page_data.keys()))
+ self.assertEqual(self.old_project, page_data['project_name'])
+ self.assertEqual('https://other-tracker.bugs', page_data['moved_to_url'])
+
+ def testGatherPageData_ProjectName(self):
+ # Construct the moved-to url from just the project name.
+ project = self.services.project.TestAddProject(self.old_project)
+ project.moved_to = 'new-project'
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/hosting/moved?project=%s' % self.old_project)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertItemsEqual(
+ ['project_name', 'moved_to_url'],
+ list(page_data.keys()))
+ self.assertEqual(self.old_project, page_data['project_name'])
+ self.assertEqual('http://127.0.0.1/p/new-project/',
+ page_data['moved_to_url'])
+
+ def testGatherPageData_HttpProjectName(self):
+ # A project named "http-foo" gets treated as a project, not a url.
+ project = self.services.project.TestAddProject(self.old_project)
+ project.moved_to = 'http-project'
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/hosting/moved?project=%s' % self.old_project)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertItemsEqual(
+ ['project_name', 'moved_to_url'],
+ list(page_data.keys()))
+ self.assertEqual(self.old_project, page_data['project_name'])
+ self.assertEqual('http://127.0.0.1/p/http-project/',
+ page_data['moved_to_url'])
+
+ def testGatherPageData_BadScheme(self):
+ # We only display URLs that start with 'http(s)://'.
+ project = self.services.project.TestAddProject(self.old_project)
+ project.moved_to = 'javascript:alert(1)'
+ _, mr = testing_helpers.GetRequestObjects(
+ path='/hosting/moved?project=%s' % self.old_project)
+
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertItemsEqual(
+ ['project_name', 'moved_to_url'],
+ list(page_data.keys()))
+ self.assertEqual(self.old_project, page_data['project_name'])
+ self.assertEqual('#invalid-destination-url', page_data['moved_to_url'])
diff --git a/sitewide/test/projectcreate_test.py b/sitewide/test/projectcreate_test.py
new file mode 100644
index 0000000..8f468dd
--- /dev/null
+++ b/sitewide/test/projectcreate_test.py
@@ -0,0 +1,74 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for the Project Creation servlet."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import settings
+from framework import permissions
+from proto import project_pb2
+from proto import site_pb2
+from services import service_manager
+from sitewide import projectcreate
+from testing import fake
+from testing import testing_helpers
+
+
+class ProjectCreateTest(unittest.TestCase):
+
+ def setUp(self):
+ services = service_manager.Services()
+ self.servlet = projectcreate.ProjectCreate('req', 'res', services=services)
+
+ def CheckAssertBasePermissions(
+ self, restriction, expect_admin_ok, expect_nonadmin_ok):
+ old_project_creation_restriction = settings.project_creation_restriction
+ settings.project_creation_restriction = restriction
+
+ # Anon users can never do it
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(None, {}, None))
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ mr = testing_helpers.MakeMonorailRequest()
+ if expect_admin_ok:
+ self.servlet.AssertBasePermission(mr)
+ else:
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ mr = testing_helpers.MakeMonorailRequest(
+ perms=permissions.GetPermissions(mr.auth.user_pb, {111}, None))
+ if expect_nonadmin_ok:
+ self.servlet.AssertBasePermission(mr)
+ else:
+ self.assertRaises(
+ permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ settings.project_creation_restriction = old_project_creation_restriction
+
+ def testAssertBasePermission(self):
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.ANYONE, True, True)
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.ADMIN_ONLY, True, False)
+ self.CheckAssertBasePermissions(
+ site_pb2.UserTypeRestriction.NO_ONE, False, False)
+
+ def testGatherPageData(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ page_data = self.servlet.GatherPageData(mr)
+ self.assertEqual('', page_data['initial_name'])
+ self.assertEqual('', page_data['initial_summary'])
+ self.assertEqual('', page_data['initial_description'])
+ self.assertEqual([], page_data['labels'])
diff --git a/sitewide/test/projectsearch_test.py b/sitewide/test/projectsearch_test.py
new file mode 100644
index 0000000..a0d941d
--- /dev/null
+++ b/sitewide/test/projectsearch_test.py
@@ -0,0 +1,75 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for the projectsearch module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+
+from framework import profiler
+from proto import project_pb2
+from services import service_manager
+from sitewide import projectsearch
+from testing import fake
+from testing import testing_helpers
+
+
+class ProjectSearchTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService())
+ self.services.project.GetVisibleLiveProjects = mock.MagicMock()
+
+ for idx, letter in enumerate('abcdefghijklmnopqrstuvwxyz'):
+ self.services.project.TestAddProject(letter, project_id=idx + 1)
+ for idx in range(27, 110):
+ self.services.project.TestAddProject(str(idx), project_id=idx)
+
+ self.addCleanup(mock.patch.stopall())
+
+ def TestPipeline(self, expected_last, expected_len):
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.can = 1
+
+ pipeline = projectsearch.ProjectSearchPipeline(mr, self.services)
+ pipeline.SearchForIDs()
+ pipeline.GetProjectsAndPaginate('fake cnxn', '/hosting/search')
+ self.assertEqual(1, pipeline.pagination.start)
+ self.assertEqual(expected_last, pipeline.pagination.last)
+ self.assertEqual(expected_len, len(pipeline.visible_results))
+
+ return pipeline
+
+ def testZeroResults(self):
+ self.services.project.GetVisibleLiveProjects.return_value = []
+
+ pipeline = self.TestPipeline(0, 0)
+
+ self.services.project.GetVisibleLiveProjects.assert_called_once()
+ self.assertListEqual([], pipeline.visible_results)
+
+ def testNonzeroResults(self):
+ self.services.project.GetVisibleLiveProjects.return_value = [1, 2, 3]
+
+ pipeline = self.TestPipeline(3, 3)
+
+ self.services.project.GetVisibleLiveProjects.assert_called_once()
+ self.assertListEqual(
+ [1, 2, 3], [p.project_id for p in pipeline.visible_results])
+
+ def testTwoPageResults(self):
+ """Test more than one pagination page of results."""
+ self.services.project.GetVisibleLiveProjects.return_value = list(
+ range(1, 106))
+
+ pipeline = self.TestPipeline(100, 100)
+
+ self.services.project.GetVisibleLiveProjects.assert_called_once()
+ self.assertEqual(
+ '/hosting/search?num=100&start=100', pipeline.pagination.next_url)
diff --git a/sitewide/test/sitewide_helpers_test.py b/sitewide/test/sitewide_helpers_test.py
new file mode 100644
index 0000000..d292b6f
--- /dev/null
+++ b/sitewide/test/sitewide_helpers_test.py
@@ -0,0 +1,170 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for the sitewide_helpers module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import project_pb2
+from services import service_manager
+from sitewide import sitewide_helpers
+from testing import fake
+
+
+REGULAR_USER_ID = 111
+ADMIN_USER_ID = 222
+OTHER_USER_ID = 333
+
+# Test project IDs
+REGULAR_OWNER_LIVE = 1001
+REGULAR_OWNER_ARCHIVED = 1002
+REGULAR_OWNER_DELETABLE = 1003
+REGULAR_COMMITTER_LIVE = 2001
+REGULAR_COMMITTER_ARCHIVED = 2002
+REGULAR_COMMITTER_DELETABLE = 2003
+OTHER_OWNER_LIVE = 3001
+OTHER_OWNER_ARCHIVED = 3002
+OTHER_OWNER_DELETABLE = 3003
+OTHER_COMMITTER_LIVE = 4001
+MEMBERS_ONLY = 5001
+
+
+class HelperFunctionsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ project_star=fake.ProjectStarService())
+ self.cnxn = 'fake cnxn'
+
+ for user_id in (ADMIN_USER_ID, REGULAR_USER_ID, OTHER_USER_ID):
+ self.services.user.TestAddUser('ignored_%s@gmail.com' % user_id, user_id)
+
+ self.regular_owner_live = self.services.project.TestAddProject(
+ 'regular-owner-live', state=project_pb2.ProjectState.LIVE,
+ owner_ids=[REGULAR_USER_ID], project_id=REGULAR_OWNER_LIVE)
+ self.regular_owner_archived = self.services.project.TestAddProject(
+ 'regular-owner-archived', state=project_pb2.ProjectState.ARCHIVED,
+ owner_ids=[REGULAR_USER_ID], project_id=REGULAR_OWNER_ARCHIVED)
+ self.regular_owner_deletable = self.services.project.TestAddProject(
+ 'regular-owner-deletable', state=project_pb2.ProjectState.DELETABLE,
+ owner_ids=[REGULAR_USER_ID], project_id=REGULAR_OWNER_DELETABLE)
+ self.regular_committer_live = self.services.project.TestAddProject(
+ 'regular-committer-live', state=project_pb2.ProjectState.LIVE,
+ committer_ids=[REGULAR_USER_ID], project_id=REGULAR_COMMITTER_LIVE)
+ self.regular_committer_archived = self.services.project.TestAddProject(
+ 'regular-committer-archived', state=project_pb2.ProjectState.ARCHIVED,
+ committer_ids=[REGULAR_USER_ID], project_id=REGULAR_COMMITTER_ARCHIVED)
+ self.regular_committer_deletable = self.services.project.TestAddProject(
+ 'regular-committer-deletable', state=project_pb2.ProjectState.DELETABLE,
+ committer_ids=[REGULAR_USER_ID], project_id=REGULAR_COMMITTER_DELETABLE)
+ self.other_owner_live = self.services.project.TestAddProject(
+ 'other-owner-live', state=project_pb2.ProjectState.LIVE,
+ owner_ids=[OTHER_USER_ID], project_id=OTHER_OWNER_LIVE)
+ self.other_owner_archived = self.services.project.TestAddProject(
+ 'other-owner-archived', state=project_pb2.ProjectState.ARCHIVED,
+ owner_ids=[OTHER_USER_ID], project_id=OTHER_OWNER_ARCHIVED)
+ self.other_owner_deletable = self.services.project.TestAddProject(
+ 'other-owner-deletable', state=project_pb2.ProjectState.DELETABLE,
+ owner_ids=[OTHER_USER_ID], project_id=OTHER_OWNER_DELETABLE)
+ self.other_committer_live = self.services.project.TestAddProject(
+ 'other-committer-live', state=project_pb2.ProjectState.LIVE,
+ committer_ids=[OTHER_USER_ID], project_id=OTHER_COMMITTER_LIVE)
+
+ self.regular_user = self.services.user.GetUser(self.cnxn, REGULAR_USER_ID)
+
+ self.admin_user = self.services.user.TestAddUser(
+ 'administrator@chromium.org', ADMIN_USER_ID)
+ self.admin_user.is_site_admin = True
+
+ self.other_user = self.services.user.GetUser(self.cnxn, OTHER_USER_ID)
+
+ self.members_only_project = self.services.project.TestAddProject(
+ 'members-only', owner_ids=[REGULAR_USER_ID], project_id=MEMBERS_ONLY)
+ self.members_only_project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
+
+ def assertProjectsAnyOrder(self, actual_projects, *expected_projects):
+ # Check names rather than Project objects so that output is easier to read.
+ actual_names = [p.project_name for p in actual_projects]
+ expected_names = [p.project_name for p in expected_projects]
+ self.assertItemsEqual(expected_names, actual_names)
+
+ def testFilterViewableProjects_CantViewArchived(self):
+ projects = list(sitewide_helpers.FilterViewableProjects(
+ list(self.services.project.test_projects.values()),
+ self.regular_user, {REGULAR_USER_ID}))
+ self.assertProjectsAnyOrder(
+ projects, self.regular_owner_live, self.regular_committer_live,
+ self.other_owner_live, self.other_committer_live,
+ self.members_only_project)
+
+ def testFilterViewableProjects_NonMemberCantViewMembersOnly(self):
+ projects = list(sitewide_helpers.FilterViewableProjects(
+ list(self.services.project.test_projects.values()),
+ self.other_user, {OTHER_USER_ID}))
+ self.assertProjectsAnyOrder(
+ projects, self.regular_owner_live, self.regular_committer_live,
+ self.other_owner_live, self.other_committer_live)
+
+ def testFilterViewableProjects_AdminCanViewAny(self):
+ projects = list(sitewide_helpers.FilterViewableProjects(
+ list(self.services.project.test_projects.values()),
+ self.admin_user, {ADMIN_USER_ID}))
+ self.assertProjectsAnyOrder(
+ projects, self.regular_owner_live, self.regular_committer_live,
+ self.other_owner_live, self.other_committer_live,
+ self.members_only_project)
+
+ def testGetStarredProjects_OnlyViewableLiveStarred(self):
+ viewed_user_id = 123
+ for p in self.services.project.test_projects.values():
+ # We go straight to the services layer because this is a test set up
+ # rather than an actual user request.
+ self.services.project_star.SetStar(
+ self.cnxn, p.project_id, viewed_user_id, True)
+
+ self.assertProjectsAnyOrder(
+ sitewide_helpers.GetViewableStarredProjects(
+ self.cnxn, self.services, viewed_user_id,
+ {REGULAR_USER_ID}, self.regular_user),
+ self.regular_owner_live, self.regular_committer_live,
+ self.other_owner_live, self.other_committer_live,
+ self.members_only_project)
+
+ def testGetStarredProjects_MembersOnly(self):
+ # Both users were able to star the project in the past. The stars do not
+ # go away even if access to the project changes.
+ self.services.project_star.SetStar(
+ self.cnxn, self.members_only_project.project_id, REGULAR_USER_ID, True)
+ self.services.project_star.SetStar(
+ self.cnxn, self.members_only_project.project_id, OTHER_USER_ID, True)
+
+ # But now, only one of them is currently a member, so only regular_user
+ # can see the starred project in the lists.
+ self.assertProjectsAnyOrder(
+ sitewide_helpers.GetViewableStarredProjects(
+ self.cnxn, self.services, REGULAR_USER_ID, {REGULAR_USER_ID},
+ self.regular_user),
+ self.members_only_project)
+ self.assertProjectsAnyOrder(
+ sitewide_helpers.GetViewableStarredProjects(
+ self.cnxn, self.services, OTHER_USER_ID, {REGULAR_USER_ID},
+ self.regular_user),
+ self.members_only_project)
+
+ # The other user cannot see the project, so they do not see it in either
+ # list of starred projects.
+ self.assertProjectsAnyOrder(
+ sitewide_helpers.GetViewableStarredProjects(
+ self.cnxn, self.services, REGULAR_USER_ID, {OTHER_USER_ID},
+ self.other_user)) # No expected projects listed.
+ self.assertProjectsAnyOrder(
+ sitewide_helpers.GetViewableStarredProjects(
+ self.cnxn, self.services, OTHER_USER_ID, {OTHER_USER_ID},
+ self.other_user)) # No expected projects listed.
diff --git a/sitewide/test/sitewide_views_test.py b/sitewide/test/sitewide_views_test.py
new file mode 100644
index 0000000..ed2515f
--- /dev/null
+++ b/sitewide/test/sitewide_views_test.py
@@ -0,0 +1,26 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unit tests for sitewide_views module."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from proto import usergroup_pb2
+from sitewide import sitewide_views
+
+
+class GroupViewTest(unittest.TestCase):
+
+ def testConstructor(self):
+ group_settings = usergroup_pb2.MakeSettings('anyone')
+ view = sitewide_views.GroupView('groupname', 123, group_settings, 999)
+
+ self.assertEqual('groupname', view.name)
+ self.assertEqual(123, view.num_members)
+ self.assertEqual('ANYONE', view.who_can_view_members)
+ self.assertEqual('/g/999/', view.detail_url)
diff --git a/sitewide/test/userprofile_test.py b/sitewide/test/userprofile_test.py
new file mode 100644
index 0000000..b830fb7
--- /dev/null
+++ b/sitewide/test/userprofile_test.py
@@ -0,0 +1,252 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the user profile page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import mock
+import unittest
+import logging
+import webapp2
+import ezt
+
+from framework import framework_helpers
+from framework import framework_views
+from framework import permissions
+from proto import project_pb2
+from proto import user_pb2
+from services import service_manager
+from sitewide import userprofile
+from testing import fake
+from testing import testing_helpers
+
+from google.appengine.ext import testbed
+
+REGULAR_USER_ID = 111
+ADMIN_USER_ID = 222
+OTHER_USER_ID = 333
+STATES = {
+ 'live': project_pb2.ProjectState.LIVE,
+ 'archived': project_pb2.ProjectState.ARCHIVED,
+}
+
+
+def MakeReqInfo(
+ user_pb, user_id, viewed_user_pb, viewed_user_id, viewed_user_name,
+ perms=permissions.USER_PERMISSIONSET):
+ mr = fake.MonorailRequest(None, perms=perms)
+ mr.auth.user_pb = user_pb
+ mr.auth.user_id = user_id
+ mr.auth.effective_ids = {user_id}
+ mr.viewed_user_auth.email = viewed_user_name
+ mr.viewed_user_auth.user_pb = viewed_user_pb
+ mr.viewed_user_auth.user_id = viewed_user_id
+ mr.viewed_user_auth.effective_ids = {viewed_user_id}
+ mr.viewed_user_auth.user_view = framework_views.UserView(viewed_user_pb)
+ mr.viewed_user_name = viewed_user_name
+ mr.request = webapp2.Request.blank("/")
+ return mr
+
+
+class UserProfileTest(unittest.TestCase):
+
+ def setUp(self):
+ self.patcher_1 = mock.patch(
+ 'framework.framework_helpers.UserSettings.GatherUnifiedSettingsPageData')
+ self.mock_guspd = self.patcher_1.start()
+ self.mock_guspd.return_value = {'unified': None}
+
+ services = service_manager.Services(
+ project=fake.ProjectService(),
+ user=fake.UserService(),
+ usergroup=fake.UserGroupService(),
+ project_star=fake.ProjectStarService(),
+ user_star=fake.UserStarService())
+ self.servlet = userprofile.UserProfile('req', 'res', services=services)
+
+ for user_id in (
+ REGULAR_USER_ID, ADMIN_USER_ID, OTHER_USER_ID):
+ services.user.TestAddUser('%s@gmail.com' % user_id, user_id)
+
+ for user in ['regular', 'other']:
+ for relation in ['owner', 'member']:
+ for state_name, state in STATES.items():
+ services.project.TestAddProject(
+ '%s-%s-%s' % (user, relation, state_name), state=state)
+
+ # Add projects
+ for state_name, state in STATES.items():
+ services.project.TestAddProject(
+ 'regular-owner-%s' % state_name, state=state,
+ owner_ids=[REGULAR_USER_ID])
+ services.project.TestAddProject(
+ 'regular-member-%s' % state_name, state=state,
+ committer_ids=[REGULAR_USER_ID])
+ services.project.TestAddProject(
+ 'other-owner-%s' % state_name, state=state,
+ owner_ids=[OTHER_USER_ID])
+ services.project.TestAddProject(
+ 'other-member-%s' % state_name, state=state,
+ committer_ids=[OTHER_USER_ID])
+
+ self.regular_user = services.user.GetUser('fake cnxn', REGULAR_USER_ID)
+ self.admin_user = services.user.GetUser('fake cnxn', ADMIN_USER_ID)
+ self.admin_user.is_site_admin = True
+ self.other_user = services.user.GetUser('fake cnxn', OTHER_USER_ID)
+
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_memcache_stub()
+ self.testbed.init_datastore_v3_stub()
+
+ def tearDown(self):
+ self.testbed.deactivate()
+ mock.patch.stopall()
+
+ def assertProjectsAnyOrder(self, value_to_test, *expected_project_names):
+ actual_project_names = [project_view.project_name
+ for project_view in value_to_test]
+ self.assertItemsEqual(expected_project_names, actual_project_names)
+
+ def testGatherPageData_RegularUserViewingOtherUserProjects(self):
+ """A user can see the other users' live projects, but not archived ones."""
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.other_user,
+ OTHER_USER_ID, 'other@xyz.com')
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertProjectsAnyOrder(page_data['owner_of_projects'],
+ 'other-owner-live')
+ self.assertProjectsAnyOrder(page_data['committer_of_projects'],
+ 'other-member-live')
+ self.assertFalse(page_data['owner_of_archived_projects'])
+ self.assertEqual('ot...@xyz.com', page_data['viewed_user_display_name'])
+ self.assertEqual(ezt.boolean(False), page_data['can_delete_user'])
+ self.mock_guspd.assert_called_once_with(
+ 111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+ None)
+
+ def testGatherPageData_RegularUserViewingOwnProjects(self):
+ """A user can see all their own projects: live or archived."""
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.regular_user,
+ REGULAR_USER_ID, 'self@xyz.com')
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertEqual('self@xyz.com', page_data['viewed_user_display_name'])
+ self.assertEqual(ezt.boolean(False), page_data['can_delete_user'])
+ self.assertProjectsAnyOrder(page_data['owner_of_projects'],
+ 'regular-owner-live')
+ self.assertProjectsAnyOrder(page_data['committer_of_projects'],
+ 'regular-member-live')
+ self.assertProjectsAnyOrder(
+ page_data['owner_of_archived_projects'],
+ 'regular-owner-archived')
+ self.mock_guspd.assert_called_once_with(
+ 111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+ None)
+
+ def testGatherPageData_RegularUserViewingStarredUsers(self):
+ """A user can see display names of other users that they starred."""
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.regular_user,
+ REGULAR_USER_ID, 'self@xyz.com')
+ self.servlet.services.user_star.SetStar(
+ 'cnxn', OTHER_USER_ID, REGULAR_USER_ID, True)
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ starred_users = page_data['starred_users']
+ self.assertEqual(1, len(starred_users))
+ self.assertEqual('333@gmail.com', starred_users[0].email)
+ self.assertEqual('["3...@gmail.com"]', page_data['starred_users_json'])
+ self.mock_guspd.assert_called_once_with(
+ 111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+ None)
+
+ def testGatherPageData_AdminViewingOtherUserAddress(self):
+ """Site admins always see full email addresses of other users."""
+ mr = MakeReqInfo(
+ self.admin_user, ADMIN_USER_ID, self.other_user,
+ OTHER_USER_ID, 'other@xyz.com',
+ perms=permissions.ADMIN_PERMISSIONSET)
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertEqual('other@xyz.com', page_data['viewed_user_display_name'])
+ self.assertEqual(ezt.boolean(True), page_data['can_delete_user'])
+ self.mock_guspd.assert_called_once_with(
+ 222, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+ mock.ANY)
+
+ def testGatherPageData_RegularUserViewingOtherUserAddressUnobscured(self):
+ """Email should be revealed to others depending on obscure_email."""
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.other_user,
+ OTHER_USER_ID, 'other@xyz.com')
+ mr.viewed_user_auth.user_view.obscure_email = False
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertEqual('other@xyz.com', page_data['viewed_user_display_name'])
+ self.mock_guspd.assert_called_once_with(
+ 111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+ None)
+
+ def testGatherPageData_RegularUserViewingOtherUserAddressObscured(self):
+ """Email should be revealed to others depending on obscure_email."""
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.other_user,
+ OTHER_USER_ID, 'other@xyz.com')
+ mr.viewed_user_auth.user_view.obscure_email = True
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertEqual('ot...@xyz.com', page_data['viewed_user_display_name'])
+ self.assertEqual(ezt.boolean(False), page_data['can_delete_user'])
+ self.mock_guspd.assert_called_once_with(
+ 111, mr.viewed_user_auth.user_view, mr.viewed_user_auth.user_pb,
+ None)
+
+ def testGatherPageData_NoLinkedAccounts(self):
+ """An account with no linked accounts should not show anything linked."""
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.other_user,
+ OTHER_USER_ID, 'other@xyz.com')
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertIsNone(page_data['linked_parent'])
+ self.assertEqual([], page_data['linked_children'])
+
+ def testGatherPageData_ParentAccounts(self):
+ """An account with a parent linked account should show it."""
+ self.other_user.linked_parent_id = REGULAR_USER_ID
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.other_user,
+ OTHER_USER_ID, 'other@xyz.com')
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertEqual('111@gmail.com', page_data['linked_parent'].email)
+ self.assertEqual([], page_data['linked_children'])
+
+ def testGatherPageData_ChildAccounts(self):
+ """An account with a child linked account should show them."""
+ self.other_user.linked_child_ids = [REGULAR_USER_ID, ADMIN_USER_ID]
+ mr = MakeReqInfo(
+ self.regular_user, REGULAR_USER_ID, self.other_user,
+ OTHER_USER_ID, 'other@xyz.com')
+
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertEqual(None, page_data['linked_parent'])
+ self.assertEqual(
+ ['111@gmail.com', '222@gmail.com'],
+ [uv.email for uv in page_data['linked_children']])
diff --git a/sitewide/test/usersettings_test.py b/sitewide/test/usersettings_test.py
new file mode 100644
index 0000000..54c14ae
--- /dev/null
+++ b/sitewide/test/usersettings_test.py
@@ -0,0 +1,66 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Tests for the user settings page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+
+from framework import framework_helpers
+from framework import permissions
+from framework import template_helpers
+from proto import user_pb2
+from services import service_manager
+from sitewide import usersettings
+from testing import fake
+from testing import testing_helpers
+
+
+class UserSettingsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.mox = mox.Mox()
+ self.services = service_manager.Services(user=fake.UserService())
+ self.servlet = usersettings.UserSettings(
+ 'req', 'res', services=self.services)
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+
+ def testAssertBasePermission(self):
+ mr = testing_helpers.MakeMonorailRequest()
+ mr.auth.user_id = 111
+
+ # The following should return without exception.
+ self.servlet.AssertBasePermission(mr)
+
+ # No logged in user means anonymous access, should raise error.
+ mr.auth.user_id = 0
+ self.assertRaises(permissions.PermissionException,
+ self.servlet.AssertBasePermission, mr)
+
+ def testGatherPageData(self):
+ self.mox.StubOutWithMock(
+ framework_helpers.UserSettings, 'GatherUnifiedSettingsPageData')
+ framework_helpers.UserSettings.GatherUnifiedSettingsPageData(
+ 0, None, mox.IsA(user_pb2.User), mox.IsA(user_pb2.UserPrefs)
+ ).AndReturn({'unified': None})
+ self.mox.ReplayAll()
+
+ mr = testing_helpers.MakeMonorailRequest()
+ page_data = self.servlet.GatherPageData(mr)
+
+ self.assertItemsEqual(
+ ['logged_in_user_pb', 'unified', 'user_tab_mode',
+ 'viewed_user', 'offer_saved_queries_subtab', 'viewing_self'],
+ list(page_data.keys()))
+ self.assertEqual(template_helpers.PBProxy(mr.auth.user_pb),
+ page_data['logged_in_user_pb'])
+
+ self.mox.VerifyAll()
diff --git a/sitewide/test/userupdates_test.py b/sitewide/test/userupdates_test.py
new file mode 100644
index 0000000..efae9bc
--- /dev/null
+++ b/sitewide/test/userupdates_test.py
@@ -0,0 +1,115 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Unittests for monorail.sitewide.userupdates."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+import mox
+
+from features import activities
+from services import service_manager
+from sitewide import sitewide_helpers
+from sitewide import userupdates
+from testing import fake
+from testing import testing_helpers
+
+
+class ProjectUpdatesTest(unittest.TestCase):
+
+ def setUp(self):
+ self.services = service_manager.Services(
+ project=fake.ProjectService(),
+ user_star=fake.UserStarService())
+
+ self.user_id = 2
+ self.project_id = 987
+ self.project = self.services.project.TestAddProject(
+ 'proj', project_id=self.project_id)
+
+ self.mr = testing_helpers.MakeMonorailRequest(
+ services=self.services, project=self.project)
+ self.mr.cnxn = 'fake cnxn'
+ self.mr.viewed_user_auth.user_id = 100
+
+ self.mox = mox.Mox()
+
+ def tearDown(self):
+ self.mox.UnsetStubs()
+ self.mox.ResetAll()
+
+ def testUserUpdatesProjects(self):
+ uup = userupdates.UserUpdatesProjects(None, None, self.services)
+
+ self.mox.StubOutWithMock(sitewide_helpers, 'GetViewableStarredProjects')
+ sitewide_helpers.GetViewableStarredProjects(
+ self.mr.cnxn, self.services, self.mr.viewed_user_auth.user_id,
+ self.mr.auth.effective_ids, self.mr.auth.user_pb).AndReturn(
+ [self.project])
+
+ self.mox.StubOutWithMock(activities, 'GatherUpdatesData')
+ activities.GatherUpdatesData(
+ self.services, self.mr, user_ids=None,
+ project_ids=[self.project_id],
+ ending=uup._ENDING,
+ updates_page_url=uup._UPDATES_PAGE_URL,
+ highlight=uup._HIGHLIGHT).AndReturn({})
+
+ self.mox.ReplayAll()
+
+ page_data = uup.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+ self.assertEqual(3, len(page_data))
+ self.assertEqual('st5', page_data['user_tab_mode'])
+ self.assertEqual('yes', page_data['viewing_user_page'])
+ self.assertEqual(uup._TAB_MODE, page_data['user_updates_tab_mode'])
+
+ def testUserUpdatesDevelopers(self):
+ uud = userupdates.UserUpdatesDevelopers(None, None, self.services)
+
+ self.mox.StubOutWithMock(self.services.user_star, 'LookupStarredItemIDs')
+ self.services.user_star.LookupStarredItemIDs(
+ self.mr.cnxn, self.mr.viewed_user_auth.user_id).AndReturn(
+ [self.user_id])
+
+ self.mox.StubOutWithMock(activities, 'GatherUpdatesData')
+ activities.GatherUpdatesData(
+ self.services, self.mr, user_ids=[self.user_id],
+ project_ids=None, ending=uud._ENDING,
+ updates_page_url=uud._UPDATES_PAGE_URL,
+ highlight=uud._HIGHLIGHT).AndReturn({})
+
+ self.mox.ReplayAll()
+
+ page_data = uud.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+ self.assertEqual(3, len(page_data))
+ self.assertEqual('st5', page_data['user_tab_mode'])
+ self.assertEqual('yes', page_data['viewing_user_page'])
+ self.assertEqual(uud._TAB_MODE, page_data['user_updates_tab_mode'])
+
+ def testUserUpdatesIndividual(self):
+ uui = userupdates.UserUpdatesIndividual(None, None, self.services)
+
+ self.mox.StubOutWithMock(activities, 'GatherUpdatesData')
+ activities.GatherUpdatesData(
+ self.services, self.mr,
+ user_ids=[self.mr.viewed_user_auth.user_id],
+ project_ids=None, ending=uui._ENDING,
+ updates_page_url=uui._UPDATES_PAGE_URL,
+ highlight=uui._HIGHLIGHT).AndReturn({})
+
+ self.mox.ReplayAll()
+
+ page_data = uui.GatherPageData(self.mr)
+ self.mox.VerifyAll()
+ self.assertEqual(3, len(page_data))
+ self.assertEqual('st5', page_data['user_tab_mode'])
+ self.assertEqual('yes', page_data['viewing_user_page'])
+ self.assertEqual(uui._TAB_MODE, page_data['user_updates_tab_mode'])
+
diff --git a/sitewide/userclearbouncing.py b/sitewide/userclearbouncing.py
new file mode 100644
index 0000000..3decdf4
--- /dev/null
+++ b/sitewide/userclearbouncing.py
@@ -0,0 +1,62 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Class to show a servlet to clear a user's bouncing email timestamp."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+from framework import framework_helpers
+from framework import permissions
+from framework import servlet
+from framework import timestr
+
+
+class UserClearBouncing(servlet.Servlet):
+ """Shows a page that can clear a user's bouncing email timestamp."""
+
+ _PAGE_TEMPLATE = 'sitewide/user-clear-bouncing-page.ezt'
+
+ def AssertBasePermission(self, mr):
+ """Check whether the user has any permission to visit this page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ """
+ super(UserClearBouncing, self).AssertBasePermission(mr)
+ if mr.auth.user_id == mr.viewed_user_auth.user_id:
+ return
+ if mr.perms.HasPerm(permissions.EDIT_OTHER_USERS, None, None):
+ return
+ raise permissions.PermissionException('You cannot edit this user.')
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ viewed_user = mr.viewed_user_auth.user_pb
+ if viewed_user.email_bounce_timestamp:
+ last_bounce_str = timestr.FormatRelativeDate(
+ viewed_user.email_bounce_timestamp, days_only=True)
+ last_bounce_str = last_bounce_str or 'Less than 2 days ago'
+ else:
+ last_bounce_str = None
+
+ page_data = {
+ 'user_tab_mode': 'st2',
+ 'last_bounce_str': last_bounce_str,
+ }
+ return page_data
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ viewed_user = mr.viewed_user_auth.user_pb
+ viewed_user.email_bounce_timestamp = None
+ self.services.user.UpdateUser(
+ mr.cnxn, viewed_user.user_id, viewed_user)
+ return framework_helpers.FormatAbsoluteURL(
+ mr, mr.viewed_user_auth.user_view.profile_url, include_project=False,
+ saved=1, ts=int(time.time()))
diff --git a/sitewide/userprofile.py b/sitewide/userprofile.py
new file mode 100644
index 0000000..bf68c5f
--- /dev/null
+++ b/sitewide/userprofile.py
@@ -0,0 +1,271 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Classes for the user profile page ("my page")."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+import json
+
+import ezt
+
+import settings
+from businesslogic import work_env
+from framework import framework_helpers
+from framework import framework_views
+from framework import permissions
+from framework import servlet
+from framework import timestr
+from framework import xsrf
+from project import project_views
+from sitewide import sitewide_helpers
+
+
+class UserProfile(servlet.Servlet):
+ """Shows a page of information about a user."""
+
+ _PAGE_TEMPLATE = 'sitewide/user-profile-page.ezt'
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ viewed_user = mr.viewed_user_auth.user_pb
+ if self.services.usergroup.GetGroupSettings(
+ mr.cnxn, mr.viewed_user_auth.user_id):
+ url = framework_helpers.FormatAbsoluteURL(
+ mr, '/g/%s/' % viewed_user.email, include_project=False)
+ self.redirect(url, abort=True) # Show group page instead.
+
+ with work_env.WorkEnv(mr, self.services) as we:
+ project_lists = we.GetUserProjects(mr.viewed_user_auth.effective_ids)
+
+ (visible_ownership, visible_archived, visible_membership,
+ visible_contrib) = project_lists
+
+ with mr.profiler.Phase('Getting user groups'):
+ group_settings = self.services.usergroup.GetAllGroupSettings(
+ mr.cnxn, mr.viewed_user_auth.effective_ids)
+ member_ids, owner_ids = self.services.usergroup.LookupAllMembers(
+ mr.cnxn, list(group_settings.keys()))
+ friend_project_ids = [] # TODO(issue 4202): implement this.
+ visible_group_ids = []
+ for group_id in group_settings:
+ if permissions.CanViewGroupMembers(
+ mr.perms, mr.auth.effective_ids, group_settings[group_id],
+ member_ids[group_id], owner_ids[group_id], friend_project_ids):
+ visible_group_ids.append(group_id)
+
+ user_group_views = framework_views.MakeAllUserViews(
+ mr.cnxn, self.services.user, visible_group_ids)
+ user_group_views = sorted(
+ list(user_group_views.values()), key=lambda ugv: ugv.email)
+
+ with mr.profiler.Phase('Getting linked accounts'):
+ linked_parent = None
+ linked_children = []
+ linked_views = framework_views.MakeAllUserViews(
+ mr.cnxn, self.services.user,
+ [viewed_user.linked_parent_id],
+ viewed_user.linked_child_ids)
+ if viewed_user.linked_parent_id:
+ linked_parent = linked_views[viewed_user.linked_parent_id]
+ if viewed_user.linked_child_ids:
+ linked_children = [
+ linked_views[child_id] for child_id in viewed_user.linked_child_ids]
+ offer_unlink = (mr.auth.user_id == viewed_user.user_id or
+ mr.auth.user_id in linked_views)
+
+ incoming_invite_users = []
+ outgoing_invite_users = []
+ possible_parent_accounts = []
+ can_edit_invites = mr.auth.user_id == mr.viewed_user_auth.user_id
+ display_link_invites = can_edit_invites or mr.auth.user_pb.is_site_admin
+ # TODO(jrobbins): allow site admin to edit invites for other users.
+ if display_link_invites:
+ with work_env.WorkEnv(mr, self.services, phase='Getting link invites'):
+ incoming_invite_ids, outgoing_invite_ids = we.GetPendingLinkedInvites(
+ user_id=viewed_user.user_id)
+ invite_views = framework_views.MakeAllUserViews(
+ mr.cnxn, self.services.user, incoming_invite_ids, outgoing_invite_ids)
+ incoming_invite_users = [
+ invite_views[uid] for uid in incoming_invite_ids]
+ outgoing_invite_users = [
+ invite_views[uid] for uid in outgoing_invite_ids]
+ possible_parent_accounts = _ComputePossibleParentAccounts(
+ we, mr.viewed_user_auth.user_view, linked_parent, linked_children)
+
+ viewed_user_display_name = framework_views.GetViewedUserDisplayName(mr)
+
+ with work_env.WorkEnv(mr, self.services) as we:
+ starred_projects = we.ListStarredProjects(
+ viewed_user_id=mr.viewed_user_auth.user_id)
+ logged_in_starred = we.ListStarredProjects()
+ logged_in_starred_pids = {p.project_id for p in logged_in_starred}
+
+ starred_user_ids = self.services.user_star.LookupStarredItemIDs(
+ mr.cnxn, mr.viewed_user_auth.user_id)
+ starred_user_dict = framework_views.MakeAllUserViews(
+ mr.cnxn, self.services.user, starred_user_ids)
+ starred_users = list(starred_user_dict.values())
+ starred_users_json = json.dumps(
+ [uv.display_name for uv in starred_users])
+
+ is_user_starred = self._IsUserStarred(
+ mr.cnxn, mr.auth.user_id, mr.viewed_user_auth.user_id)
+
+ if viewed_user.last_visit_timestamp:
+ last_visit_str = timestr.FormatRelativeDate(
+ viewed_user.last_visit_timestamp, days_only=True)
+ last_visit_str = last_visit_str or 'Less than 2 days ago'
+ else:
+ last_visit_str = 'Never'
+
+ if viewed_user.email_bounce_timestamp:
+ last_bounce_str = timestr.FormatRelativeDate(
+ viewed_user.email_bounce_timestamp, days_only=True)
+ last_bounce_str = last_bounce_str or 'Less than 2 days ago'
+ else:
+ last_bounce_str = None
+
+ can_ban = permissions.CanBan(mr, self.services)
+ viewed_user_is_spammer = viewed_user.banned.lower() == 'spam'
+ viewed_user_may_be_spammer = not viewed_user_is_spammer
+ all_projects = self.services.project.GetAllProjects(mr.cnxn)
+ for project_id in all_projects:
+ project = all_projects[project_id]
+ viewed_user_perms = permissions.GetPermissions(viewed_user,
+ mr.viewed_user_auth.effective_ids, project)
+ if (viewed_user_perms != permissions.EMPTY_PERMISSIONSET and
+ viewed_user_perms != permissions.USER_PERMISSIONSET):
+ viewed_user_may_be_spammer = False
+
+ ban_token = None
+ ban_spammer_token = None
+ if mr.auth.user_id and can_ban:
+ form_token_path = mr.request.path + 'ban.do'
+ ban_token = xsrf.GenerateToken(mr.auth.user_id, form_token_path)
+ form_token_path = mr.request.path + 'banSpammer.do'
+ ban_spammer_token = xsrf.GenerateToken(mr.auth.user_id, form_token_path)
+
+ can_delete_user = permissions.CanExpungeUsers(mr)
+
+ page_data = {
+ 'user_tab_mode': 'st2',
+ 'viewed_user_display_name': viewed_user_display_name,
+ 'viewed_user_may_be_spammer': ezt.boolean(viewed_user_may_be_spammer),
+ 'viewed_user_is_spammer': ezt.boolean(viewed_user_is_spammer),
+ 'viewed_user_is_banned': ezt.boolean(viewed_user.banned),
+ 'owner_of_projects': [
+ project_views.ProjectView(
+ p, starred=p.project_id in logged_in_starred_pids)
+ for p in visible_ownership],
+ 'committer_of_projects': [
+ project_views.ProjectView(
+ p, starred=p.project_id in logged_in_starred_pids)
+ for p in visible_membership],
+ 'contributor_to_projects': [
+ project_views.ProjectView(
+ p, starred=p.project_id in logged_in_starred_pids)
+ for p in visible_contrib],
+ 'owner_of_archived_projects': [
+ project_views.ProjectView(p) for p in visible_archived],
+ 'starred_projects': [
+ project_views.ProjectView(
+ p, starred=p.project_id in logged_in_starred_pids)
+ for p in starred_projects],
+ 'starred_users': starred_users,
+ 'starred_users_json': starred_users_json,
+ 'is_user_starred': ezt.boolean(is_user_starred),
+ 'viewing_user_page': ezt.boolean(True),
+ 'last_visit_str': last_visit_str,
+ 'last_bounce_str': last_bounce_str,
+ 'vacation_message': viewed_user.vacation_message,
+ 'can_ban': ezt.boolean(can_ban),
+ 'ban_token': ban_token,
+ 'ban_spammer_token': ban_spammer_token,
+ 'user_groups': user_group_views,
+ 'linked_parent': linked_parent,
+ 'linked_children': linked_children,
+ 'incoming_invite_users': incoming_invite_users,
+ 'outgoing_invite_users': outgoing_invite_users,
+ 'possible_parent_accounts': possible_parent_accounts,
+ 'can_edit_invites': ezt.boolean(can_edit_invites),
+ 'offer_unlink': ezt.boolean(offer_unlink),
+ 'can_delete_user': ezt.boolean(can_delete_user),
+ }
+
+ viewed_user_prefs = None
+ if mr.perms.HasPerm(permissions.EDIT_OTHER_USERS, None, None):
+ with work_env.WorkEnv(mr, self.services) as we:
+ viewed_user_prefs = we.GetUserPrefs(mr.viewed_user_auth.user_id)
+
+ user_settings = (
+ framework_helpers.UserSettings.GatherUnifiedSettingsPageData(
+ mr.auth.user_id, mr.viewed_user_auth.user_view, viewed_user,
+ viewed_user_prefs))
+ page_data.update(user_settings)
+
+ return page_data
+
+ def _IsUserStarred(self, cnxn, logged_in_user_id, viewed_user_id):
+ """Return whether the logged in user starred the viewed user."""
+ if logged_in_user_id:
+ return self.services.user_star.IsItemStarredBy(
+ cnxn, viewed_user_id, logged_in_user_id)
+ return False
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ has_admin_perm = mr.perms.HasPerm(permissions.EDIT_OTHER_USERS, None, None)
+ with work_env.WorkEnv(mr, self.services) as we:
+ framework_helpers.UserSettings.ProcessSettingsForm(
+ we, post_data, mr.viewed_user_auth.user_pb, admin=has_admin_perm)
+
+ # TODO(jrobbins): Check all calls to FormatAbsoluteURL for include_project.
+ return framework_helpers.FormatAbsoluteURL(
+ mr, mr.viewed_user_auth.user_view.profile_url, include_project=False,
+ saved=1, ts=int(time.time()))
+
+
+def _ComputePossibleParentAccounts(
+ we, user_view, linked_parent, linked_children):
+ """Return a list of email addresses of possible parent accounts."""
+ if not user_view:
+ return [] # Anon user cannot link to any account.
+ if linked_parent or linked_children:
+ return [] # If account is already linked in any way, don't offer.
+ possible_domains = settings.linkable_domains.get(user_view.domain, [])
+ possible_emails = ['%s@%s' % (user_view.username, domain)
+ for domain in possible_domains]
+ found_users, _ = we.ListReferencedUsers(possible_emails)
+ found_emails = [user.email for user in found_users]
+ return found_emails
+
+
+class UserProfilePolymer(UserProfile):
+ """New Polymer version of user profiles in Monorail."""
+
+ _PAGE_TEMPLATE = 'sitewide/user-profile-page-polymer.ezt'
+
+
+class BanUser(servlet.Servlet):
+ """Bans or un-bans a user."""
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ if not permissions.CanBan(mr, self.services):
+ raise permissions.PermissionException(
+ "You do not have permission to ban users.")
+
+ framework_helpers.UserSettings.ProcessBanForm(
+ mr.cnxn, self.services.user, post_data, mr.viewed_user_auth.user_id,
+ mr.viewed_user_auth.user_pb)
+
+ # TODO(jrobbins): Check all calls to FormatAbsoluteURL for include_project.
+ return framework_helpers.FormatAbsoluteURL(
+ mr, mr.viewed_user_auth.user_view.profile_url, include_project=False,
+ saved=1, ts=int(time.time()))
diff --git a/sitewide/usersettings.py b/sitewide/usersettings.py
new file mode 100644
index 0000000..bb65ddd
--- /dev/null
+++ b/sitewide/usersettings.py
@@ -0,0 +1,65 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Classes for the user settings (preferences) page."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import time
+import urllib
+
+import ezt
+
+from businesslogic import work_env
+from framework import framework_helpers
+from framework import permissions
+from framework import servlet
+from framework import template_helpers
+from framework import urls
+
+
+class UserSettings(servlet.Servlet):
+ """Shows a page with a simple form to edit user preferences."""
+
+ _PAGE_TEMPLATE = 'sitewide/user-settings-page.ezt'
+
+ def AssertBasePermission(self, mr):
+ """Assert that the user has the permissions needed to view this page."""
+ super(UserSettings, self).AssertBasePermission(mr)
+
+ if not mr.auth.user_id:
+ raise permissions.PermissionException(
+ 'Anonymous users are not allowed to edit user settings')
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ page_data = {
+ 'user_tab_mode': 'st3',
+ 'logged_in_user_pb': template_helpers.PBProxy(mr.auth.user_pb),
+ # When on /hosting/settings, the logged-in user is the viewed user.
+ 'viewed_user': mr.auth.user_view,
+ 'offer_saved_queries_subtab': ezt.boolean(True),
+ 'viewing_self': ezt.boolean(True),
+ }
+ with work_env.WorkEnv(mr, self.services) as we:
+ settings_user_prefs = we.GetUserPrefs(mr.auth.user_id)
+ page_data.update(
+ framework_helpers.UserSettings.GatherUnifiedSettingsPageData(
+ mr.auth.user_id, mr.auth.user_view, mr.auth.user_pb,
+ settings_user_prefs))
+ return page_data
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted form."""
+ with work_env.WorkEnv(mr, self.services) as we:
+ framework_helpers.UserSettings.ProcessSettingsForm(
+ we, post_data, mr.auth.user_pb)
+
+ url = framework_helpers.FormatAbsoluteURL(
+ mr, urls.USER_SETTINGS, include_project=False,
+ saved=1, ts=int(time.time()))
+
+ return url
diff --git a/sitewide/userupdates.py b/sitewide/userupdates.py
new file mode 100644
index 0000000..ac44c0f
--- /dev/null
+++ b/sitewide/userupdates.py
@@ -0,0 +1,118 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Classes for user updates pages.
+
+ AbstractUserUpdatesPage: Base class for all user updates pages
+ UserUpdatesProjects: Handles displaying starred projects
+ UserUpdatesDevelopers: Handles displaying starred developers
+ UserUpdatesIndividual: Handles displaying activities by the viewed user
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+
+import logging
+
+import ezt
+
+from businesslogic import work_env
+from features import activities
+from framework import servlet
+from framework import urls
+from sitewide import sitewide_helpers
+
+
+class AbstractUserUpdatesPage(servlet.Servlet):
+ """Base class for user updates pages."""
+
+ _PAGE_TEMPLATE = 'sitewide/user-updates-page.ezt'
+
+ # Subclasses should override these constants.
+ _UPDATES_PAGE_URL = None
+ # What to highlight in the middle column on user updates pages - 'project',
+ # 'user', or None
+ _HIGHLIGHT = None
+ # What the ending phrase for activity titles should be - 'by_user',
+ # 'in_project', or None
+ _ENDING = None
+ _TAB_MODE = None
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page."""
+ # TODO(jrobbins): re-implement
+ # if self.CheckRevelationCaptcha(mr, mr.errors):
+ # mr.viewed_user_auth.user_view.RevealEmail()
+
+ page_data = {
+ 'user_tab_mode': 'st5',
+ 'viewing_user_page': ezt.boolean(True),
+ 'user_updates_tab_mode': self._TAB_MODE,
+ }
+
+ user_ids = self._GetUserIDsForUpdates(mr)
+ project_ids = self._GetProjectIDsForUpdates(mr)
+ page_data.update(activities.GatherUpdatesData(
+ self.services, mr, user_ids=user_ids,
+ project_ids=project_ids, ending=self._ENDING,
+ updates_page_url=self._UPDATES_PAGE_URL, highlight=self._HIGHLIGHT))
+
+ return page_data
+
+ def _GetUserIDsForUpdates(self, _mr):
+ """Returns a list of user IDs to retrieve activities from."""
+ return None # Means any.
+
+ def _GetProjectIDsForUpdates(self, _mr):
+ """Returns a list of project IDs to retrieve activities from."""
+ return None # Means any.
+
+
+class UserUpdatesProjects(AbstractUserUpdatesPage):
+ """Shows a page of updates from projects starred by a user."""
+
+ _UPDATES_FEED_URL = urls.USER_UPDATES_PROJECTS
+ _UPDATES_PAGE_URL = urls.USER_UPDATES_PROJECTS
+ _HIGHLIGHT = 'project'
+ _ENDING = 'by_user'
+ _TAB_MODE = 'st2'
+
+ def _GetProjectIDsForUpdates(self, mr):
+ """Returns a list of project IDs whom to retrieve activities from."""
+ with work_env.WorkEnv(mr, self.services) as we:
+ starred_projects = we.ListStarredProjects(
+ viewed_user_id=mr.viewed_user_auth.user_id)
+ return [project.project_id for project in starred_projects]
+
+
+class UserUpdatesDevelopers(AbstractUserUpdatesPage):
+ """Shows a page of updates from developers starred by a user."""
+
+ _UPDATES_FEED_URL = urls.USER_UPDATES_DEVELOPERS
+ _UPDATES_PAGE_URL = urls.USER_UPDATES_DEVELOPERS
+ _HIGHLIGHT = 'user'
+ _ENDING = 'in_project'
+ _TAB_MODE = 'st3'
+
+ def _GetUserIDsForUpdates(self, mr):
+ """Returns a list of user IDs whom to retrieve activities from."""
+ user_ids = self.services.user_star.LookupStarredItemIDs(
+ mr.cnxn, mr.viewed_user_auth.user_id)
+ logging.debug('StarredUsers: %r', user_ids)
+ return user_ids
+
+
+class UserUpdatesIndividual(AbstractUserUpdatesPage):
+ """Shows a page of updates initiated by a user."""
+
+ _UPDATES_FEED_URL = urls.USER_UPDATES_MINE + '/user'
+ _UPDATES_PAGE_URL = urls.USER_UPDATES_MINE
+ _HIGHLIGHT = 'project'
+ _TAB_MODE = 'st1'
+
+ def _GetUserIDsForUpdates(self, mr):
+ """Returns a list of user IDs whom to retrieve activities from."""
+ return [mr.viewed_user_auth.user_id]