diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9aed65dbf4..743ca1c054 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -798,6 +798,14 @@ class OrganizationSerializer(BaseSerializer): )) return res + def get_summary_fields(self, obj): + summary_dict = super(OrganizationSerializer, self).get_summary_fields(obj) + counts_dict = self.context.get('counts', None) + if counts_dict is not None and summary_dict is not None: + print 'counts_dict: ' + str(counts_dict) + summary_dict['counts'] = counts_dict[obj.id] + return summary_dict + class ProjectOptionsSerializer(BaseSerializer): diff --git a/awx/api/views.py b/awx/api/views.py index b203015770..09a14f5381 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -596,6 +596,15 @@ class OrganizationList(ListCreateAPIView): model = Organization serializer_class = OrganizationSerializer + # @paginated + # def get(self, *args, **kwargs): + # # self.paginated_params = {'limit': limit, 'offset': offset, 'ordering': ordering} + # limit = kwargs.pop('limit') + # offset = kwargs.pop('offset') + # ordering = kwargs.pop('ordering') + # # qs[offset:offset + limit] + # return (super(OrganizationList, self).get(*args, **kwargs), 5, None) + def create(self, request, *args, **kwargs): """Create a new organzation. @@ -614,6 +623,75 @@ class OrganizationList(ListCreateAPIView): # Okay, create the organization as usual. return super(OrganizationList, self).create(request, *args, **kwargs) + def get_serializer_context(self, *args, **kwargs): + full_context = super(OrganizationList, self).get_serializer_context(*args, **kwargs) + + if self.request is None: + return full_context + + db_results = {} + org_qs = self.request.user.get_queryset(self.model) + org_id_list = org_qs.values('id') + if len(org_id_list) == 0: + return full_context + + # Produce counts of Foreign Key relationships + db_results['inventories'] = self.request.user.get_queryset(Inventory)\ + .values('organization').annotate(Count('organization')).order_by('organization') + + db_results['teams'] = self.request.user.get_queryset(Team)\ + .values('organization').annotate(Count('organization')).order_by('organization') + + JT_reference = 'inventory__organization' + db_JT_results = self.request.user.get_queryset(JobTemplate)\ + .values(JT_reference).annotate(Count(JT_reference)).\ + order_by(JT_reference) + + # Produce counts of m2m relationships + project_qs = self.request.user.get_queryset(Project) + db_results['projects'] = Organization.projects.through.objects\ + .filter( + project_id__in=project_qs.values_list('pk', flat=True), + organization_id__in=org_qs.values_list('pk', flat=True))\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + # TODO: When RBAC branch merges, change these to role relation + user_qs = self.request.user.get_queryset(User) + db_results['users'] = Organization.users.through.objects\ + .filter( + user_id__in=user_qs.values_list('pk', flat=True), + organization_id__in=org_qs.values_list('pk', flat=True))\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + db_results['admins'] = Organization.admins.through.objects\ + .filter( + user_id__in=user_qs.values_list('pk', flat=True), + organization_id__in=org_qs.values_list('pk', flat=True))\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + count_context = {} + for org in org_id_list: + org_id = org['id'] + count_context[org_id] = {'inventories': 0, 'teams': 0, 'users': 0, + 'job_templates': 0, 'admins': 0, + 'projects': 0} + + for res in db_results: + for entry in db_results[res]: + org_id = entry['organization'] + count_context[org_id][res] = entry['organization__count'] + + for entry in db_JT_results: + org_id = entry[JT_reference] + count_context[org_id]['job_templates'] = entry['%s__count' % JT_reference] + + full_context['counts'] = count_context + + return full_context + class OrganizationDetail(RetrieveUpdateDestroyAPIView): model = Organization diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py new file mode 100644 index 0000000000..6a31bf1151 --- /dev/null +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -0,0 +1,54 @@ +import pytest + +from django.core.urlresolvers import reverse + +@pytest.fixture +def resourced_organization(organization, project, user): + admin_user = user('test-admin', True) + member_user = user('org-member') + + # Associate one resource of every type with the organization + organization.users.add(member_user) + organization.admins.add(admin_user) + organization.projects.add(project) + organization.teams.create(name='org-team') + inventory = organization.inventories.create(name="associated-inv") + inventory.jobtemplates.create(name="test-jt", + description="test-job-template-desc", + project=project, + playbook="test_playbook.yml") + + return organization + +@pytest.mark.django_db +def test_org_counts_admin(resourced_organization, user, get): + # Check that all types of resources are counted by a superuser + external_admin = user('admin', True) + response = get(reverse('api:organization_list', args=[]), external_admin) + counts = response.data['results'][0]['summary_fields']['counts'] + + assert counts == { + 'users': 1, + 'admins': 1, + 'job_templates': 1, + 'projects': 1, + 'inventories': 1, + 'teams': 1 + } + +@pytest.mark.django_db +def test_org_counts_member(resourced_organization, get): + # Check that a non-admin user can only see the full project and + # user count, consistent with the RBAC rules + member_user = resourced_organization.users.get(username='org-member') + response = get(reverse('api:organization_list', args=[]), member_user) + counts = response.data['results'][0]['summary_fields']['counts'] + + assert counts == { + 'users': 1, # User can see themselves + 'admins': 0, + 'job_templates': 0, + 'projects': 1, # Projects are shared with all the organization + 'inventories': 0, + 'teams': 0 + }