Browse Source

feat(teams) Add team member count

We'd like to show the number of members (active & inactive) in the team
list page.

Refs APP-75
Mark Story 6 years ago
parent
commit
91cdda82d2

+ 45 - 27
src/sentry/api/serializers/models/team.py

@@ -3,6 +3,7 @@ from __future__ import absolute_import
 import six
 
 from collections import defaultdict
+from django.db.models import Count
 from six.moves import zip
 
 from sentry import roles
@@ -16,55 +17,70 @@ from sentry.models import (
 
 
 def get_team_memberships(team_list, user):
+    """Get memberships the user has in the provided team list"""
     if user.is_authenticated():
-        memberships = frozenset(
-            OrganizationMemberTeam.objects.filter(
-                organizationmember__user=user,
-                team__in=team_list,
-            ).values_list('team', flat=True)
-        )
-    else:
-        memberships = frozenset()
+        return OrganizationMemberTeam.objects.filter(
+            organizationmember__user=user,
+            team__in=team_list,
+        ).values_list('team', flat=True)
+    return []
 
-    return memberships
+
+def get_member_totals(team_list, user):
+    """Get the total number of members in each team"""
+    if user.is_authenticated():
+        query = Team.objects.filter(
+            id__in=[t.pk for t in team_list]
+        ).annotate(
+            member_count=Count('organizationmemberteam')
+        ).values('id', 'member_count')
+        return {item['id']: item['member_count'] for item in query}
+    return {}
 
 
 def get_org_roles(org_ids, user):
+    """Get the role the user has in each org"""
     if user.is_authenticated():
         # map of org id to role
-        org_roles = {
-            om.organization_id: om.role for om in
+        return {
+            om['organization_id']: om['role'] for om in
             OrganizationMember.objects.filter(
                 user=user,
                 organization__in=set(org_ids),
-            )}
-    else:
-        org_roles = {}
+            ).values('role', 'organization_id')
+        }
+    return {}
 
-    return org_roles
+
+def get_access_requests(item_list, user):
+    if user.is_authenticated():
+        return frozenset(
+            OrganizationAccessRequest.objects.filter(
+                team__in=item_list,
+                member__user=user,
+            ).values_list('team', flat=True)
+        )
+    return frozenset()
 
 
 @register(Team)
 class TeamSerializer(Serializer):
     def get_attrs(self, item_list, user):
         request = env.request
-        memberships = get_team_memberships(item_list, user)
+        org_ids = set([t.organization_id for t in item_list])
 
-        if user.is_authenticated():
-            access_requests = frozenset(
-                OrganizationAccessRequest.objects.filter(
-                    team__in=item_list,
-                    member__user=user,
-                ).values_list('team', flat=True)
-            )
-        else:
-            access_requests = frozenset()
+        org_roles = get_org_roles(org_ids, user)
+
+        member_totals = get_member_totals(item_list, user)
+        memberships = get_team_memberships(item_list, user)
+        access_requests = get_access_requests(item_list, user)
 
-        org_roles = get_org_roles([t.organization_id for t in item_list], user)
-        avatars = {a.team_id: a for a in TeamAvatar.objects.filter(team__in=item_list)}
+        avatars = {a.team_id: a
+                   for a in TeamAvatar.objects.filter(team__in=item_list)}
 
         is_superuser = (request and is_active_superuser(request) and request.user == user)
         result = {}
+
         for team in item_list:
             is_member = team.id in memberships
             org_role = org_roles.get(team.organization_id)
@@ -83,6 +99,7 @@ class TeamSerializer(Serializer):
                 'is_member': is_member,
                 'has_access': has_access,
                 'avatar': avatars.get(team.id),
+                'member_count': member_totals.get(team.id, 0),
             }
         return result
 
@@ -102,6 +119,7 @@ class TeamSerializer(Serializer):
             'isMember': attrs['is_member'],
             'hasAccess': attrs['has_access'],
             'isPending': attrs['pending_request'],
+            'memberCount': attrs['member_count'],
             'avatar': avatar,
         }
 

+ 4 - 1
src/sentry/static/sentry/app/views/settings/organizationTeams/allTeamsRow.jsx

@@ -6,7 +6,7 @@ import createReactClass from 'create-react-class';
 
 import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator';
 import {joinTeam, leaveTeam} from 'app/actionCreators/teams';
-import {t, tct} from 'app/locale';
+import {t, tct, tn} from 'app/locale';
 import ApiMixin from 'app/mixins/apiMixin';
 import {PanelItem} from 'app/components/panels';
 import IdBadge from 'app/components/idBadge';
@@ -126,6 +126,9 @@ const AllTeamsRow = createReactClass({
             display
           )}
         </Box>
+        <Box flex="1" p={2}>
+          {tn('%d Member', '%d Members', team.memberCount)}
+        </Box>
         <Box p={2}>
           {this.state.loading ? (
             <a className="btn btn-default btn-sm btn-loading btn-disabled">...</a>

+ 1 - 0
tests/js/fixtures/team.js

@@ -4,6 +4,7 @@ export function Team(params) {
     slug: 'team-slug',
     name: 'Team Name',
     isMember: true,
+    memberCount: 0,
     ...params,
   };
 }

+ 3 - 0
tests/js/spec/views/__snapshots__/organizationTeamProjects.spec.jsx.snap

@@ -279,6 +279,7 @@ exports[`OrganizationTeamProjects Should render 1`] = `
                           Object {
                             "id": "1",
                             "isMember": true,
+                            "memberCount": 0,
                             "name": "Team Name",
                             "slug": "team-slug",
                           },
@@ -334,6 +335,7 @@ exports[`OrganizationTeamProjects Should render 1`] = `
                                     Object {
                                       "id": "1",
                                       "isMember": true,
+                                      "memberCount": 0,
                                       "name": "Team Name",
                                       "slug": "team-slug",
                                     },
@@ -355,6 +357,7 @@ exports[`OrganizationTeamProjects Should render 1`] = `
                                       Object {
                                         "id": "1",
                                         "isMember": true,
+                                        "memberCount": 0,
                                         "name": "Team Name",
                                         "slug": "team-slug",
                                       },

+ 1 - 0
tests/js/spec/views/__snapshots__/projectTeams.spec.jsx.snap

@@ -94,6 +94,7 @@ exports[`ProjectTeams renders 1`] = `
             Object {
               "id": "1",
               "isMember": true,
+              "memberCount": 0,
               "name": "Team Name",
               "slug": "team-slug",
             }

+ 8 - 0
tests/js/spec/views/__snapshots__/ruleBuilder.spec.jsx.snap

@@ -56,6 +56,7 @@ exports[`RuleBuilder renders 1`] = `
         Object {
           "id": "3",
           "isMember": true,
+          "memberCount": 0,
           "name": "COOL TEAM",
           "slug": "cool-team",
         },
@@ -517,6 +518,7 @@ exports[`RuleBuilder renders 1`] = `
                     Object {
                       "id": "3",
                       "isMember": true,
+                      "memberCount": 0,
                       "name": "COOL TEAM",
                       "slug": "cool-team",
                     },
@@ -710,6 +712,7 @@ exports[`RuleBuilder renders 1`] = `
                                     Object {
                                       "id": "3",
                                       "isMember": true,
+                                      "memberCount": 0,
                                       "name": "COOL TEAM",
                                       "slug": "cool-team",
                                     }
@@ -743,6 +746,7 @@ exports[`RuleBuilder renders 1`] = `
                                           Object {
                                             "id": "4",
                                             "isMember": true,
+                                            "memberCount": 0,
                                             "name": "TEAM NOT IN PROJECT",
                                             "slug": "team-not-in-project",
                                           }
@@ -1079,6 +1083,7 @@ exports[`RuleBuilder renders with suggestions 1`] = `
         Object {
           "id": "3",
           "isMember": true,
+          "memberCount": 0,
           "name": "COOL TEAM",
           "slug": "cool-team",
         },
@@ -1756,6 +1761,7 @@ exports[`RuleBuilder renders with suggestions 1`] = `
                     Object {
                       "id": "3",
                       "isMember": true,
+                      "memberCount": 0,
                       "name": "COOL TEAM",
                       "slug": "cool-team",
                     },
@@ -2177,6 +2183,7 @@ exports[`RuleBuilder renders with suggestions 1`] = `
                                     Object {
                                       "id": "3",
                                       "isMember": true,
+                                      "memberCount": 0,
                                       "name": "COOL TEAM",
                                       "slug": "cool-team",
                                     }
@@ -2210,6 +2217,7 @@ exports[`RuleBuilder renders with suggestions 1`] = `
                                           Object {
                                             "id": "4",
                                             "isMember": true,
+                                            "memberCount": 0,
                                             "name": "TEAM NOT IN PROJECT",
                                             "slug": "team-not-in-project",
                                           }

+ 7 - 1
tests/sentry/api/endpoints/test_organization_details.py

@@ -27,6 +27,11 @@ from sentry.testutils import APITestCase, TwoFactorAPITestCase
 class OrganizationDetailsTest(APITestCase):
     def test_simple(self):
         org = self.create_organization(owner=self.user)
+        self.create_team(
+            name='appy',
+            organization=org,
+            members=[self.user])
+
         self.login_as(user=self.user)
         url = reverse(
             'sentry-api-0-organization-details', kwargs={
@@ -37,6 +42,7 @@ class OrganizationDetailsTest(APITestCase):
         assert response.data['onboardingTasks'] == []
         assert response.status_code == 200, response.content
         assert response.data['id'] == six.text_type(org.id)
+        assert len(response.data['teams']) == 1
 
         for i in range(5):
             self.create_project(organization=org)
@@ -48,7 +54,7 @@ class OrganizationDetailsTest(APITestCase):
         )
         # TODO(dcramer): we need to pare this down -- lots of duplicate queries
         # for membership data
-        with self.assertNumQueries(28, using='default'):
+        with self.assertNumQueries(33, using='default'):
             from django.db import connections
             response = self.client.get(url, format='json')
             pprint(connections['default'].queries)

+ 14 - 0
tests/sentry/api/serializers/test_team.py

@@ -29,8 +29,21 @@ class TeamSerializerTest(TestCase):
                 'avatarType': 'letter_avatar',
                 'avatarUuid': None,
             },
+            'memberCount': 0,
         }
 
+    def test_member_count(self):
+        user = self.create_user(username='foo')
+        other_user = self.create_user(username='bar')
+        third_user = self.create_user(username='baz')
+
+        organization = self.create_organization(owner=user)
+        team = self.create_team(organization=organization,
+                                members=[user, other_user, third_user])
+
+        result = serialize(team, user)
+        assert 3 == result['memberCount']
+
     def test_member_access(self):
         user = self.create_user(username='foo')
         organization = self.create_organization()
@@ -163,4 +176,5 @@ class TeamWithProjectsSerializerTest(TestCase):
                 'avatarType': 'letter_avatar',
                 'avatarUuid': None,
             },
+            'memberCount': 0
         }