Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
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()))