diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9aed65dbf4..6ca73cf6a0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -798,6 +798,18 @@ 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('related_field_counts', None) + if counts_dict is not None and summary_dict is not None: + if obj.id not in counts_dict: + summary_dict['related_field_counts'] = { + 'inventories': 0, 'teams': 0, 'users': 0, + 'job_templates': 0, 'admins': 0, 'projects': 0} + else: + summary_dict['related_field_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..b50ba0497c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -614,6 +614,78 @@ 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: + if self.request.method == 'POST': + full_context['related_field_counts'] = {} + return full_context + + inv_qs = self.request.user.get_queryset(Inventory) + project_qs = self.request.user.get_queryset(Project) + user_qs = self.request.user.get_queryset(User) + + # Produce counts of Foreign Key relationships + db_results['inventories'] = inv_qs\ + .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') + + # TODO: When RBAC branch merges, change this to project relationship + JT_reference = 'inventory__organization' + # Extra filter is applied on the inventory, because this catches + # the case of deleted (and purged) inventory + db_results['job_templates'] = self.request.user.get_queryset(JobTemplate)\ + .filter(inventory__in=inv_qs)\ + .values(JT_reference).annotate(Count(JT_reference))\ + .order_by(JT_reference) + + # Produce counts of m2m relationships + db_results['projects'] = Organization.projects.through.objects\ + .filter(project__in=project_qs, organization__in=org_qs)\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + # TODO: When RBAC branch merges, change these to role relation + db_results['users'] = Organization.users.through.objects\ + .filter(user__in=user_qs, organization__in=org_qs)\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + db_results['admins'] = Organization.admins.through.objects\ + .filter(user__in=user_qs, organization__in=org_qs)\ + .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: + if res == 'job_templates': + org_reference = JT_reference + else: + org_reference = 'organization' + for entry in db_results[res]: + org_id = entry[org_reference] + if org_id in count_context: + count_context[org_id][res] = entry['%s__count' % org_reference] + + full_context['related_field_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..8d881fe8a0 --- /dev/null +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -0,0 +1,144 @@ +import pytest + +from django.core.urlresolvers import reverse + +@pytest.fixture +def resourced_organization(organization, project, team, inventory, 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") + project.jobtemplates.create(name="test-jt", + description="test-job-template-desc", + inventory=inventory, + 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) + assert response.status_code == 200 + + counts = response.data['results'][0]['summary_fields']['related_field_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) + assert response.status_code == 200 + + counts = response.data['results'][0]['summary_fields']['related_field_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 + } + +@pytest.mark.django_db +def test_new_org_zero_counts(user, post): + # Check that a POST to the organization list endpoint returns + # correct counts, including the new record + org_list_url = reverse('api:organization_list', args=[]) + post_response = post(url=org_list_url, data={'name': 'test organization', + 'description': ''}, user=user('admin', True)) + assert post_response.status_code == 201 + + new_org_list = post_response.render().data + counts_dict = new_org_list['summary_fields']['related_field_counts'] + assert counts_dict == { + 'users': 0, + 'admins': 0, + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 + } + +@pytest.mark.django_db +def test_two_organizations(resourced_organization, organizations, user, get): + # Check correct results for two organizations are returned + external_admin = user('admin', True) + organization_zero = organizations(1)[0] + response = get(reverse('api:organization_list', args=[]), external_admin) + assert response.status_code == 200 + + org_id_full = resourced_organization.id + org_id_zero = organization_zero.id + counts = {} + for i in range(2): + org_id = response.data['results'][i]['id'] + counts[org_id] = response.data['results'][i]['summary_fields']['related_field_counts'] + + assert counts[org_id_full] == { + 'users': 1, + 'admins': 1, + 'job_templates': 1, + 'projects': 1, + 'inventories': 1, + 'teams': 1 + } + assert counts[org_id_zero] == { + 'users': 0, + 'admins': 0, + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 + } + +@pytest.mark.django_db +def test_JT_associated_with_project(organizations, project, user, get): + # Check that adding a project to an organization gets the project's JT + # included in the organization's JT count + external_admin = user('admin', True) + two_orgs = organizations(2) + organization = two_orgs[0] + other_org = two_orgs[1] + + unrelated_inv = other_org.inventories.create(name='not-in-organization') + project.jobtemplates.create(name="test-jt", + description="test-job-template-desc", + inventory=unrelated_inv, + playbook="test_playbook.yml") + organization.projects.add(project) + + response = get(reverse('api:organization_list', args=[]), external_admin) + assert response.status_code == 200 + + org_id = organization.id + counts = {} + for i in range(2): + working_id = response.data['results'][i]['id'] + counts[working_id] = response.data['results'][i]['summary_fields']['related_field_counts'] + + assert counts[org_id] == { + 'users': 0, + 'admins': 0, + 'job_templates': 1, + 'projects': 1, + 'inventories': 0, + 'teams': 0 + } +