Implements parents_as_ids query param

This change implements the parents_as_ids query param. It gives the
possibility to retrieve the parents from a project in a structured way
by using nested dictionaries.

It reuses the already implemented list_projects_parents() method and
builds the dictionaries hierarchy using the returned list.

bp hierarchical-multitenancy-improvements

Change-Id: I5fb560a934881721cb233cc1549324a5171f470c
This commit is contained in:
Rodrigo Duarte Sousa 2015-01-20 10:50:49 -03:00
parent 08afed58d3
commit 0ee272c17f
3 changed files with 115 additions and 7 deletions

View File

@ -416,18 +416,33 @@ class ProjectV3(controller.V3Controller):
return ProjectV3.wrap_collection(context, refs, hints=hints)
def _expand_project_ref(self, context, ref):
params = context['query_string']
parents_as_list = 'parents_as_list' in params and (
self.query_filter_is_true(params['parents_as_list']))
parents_as_ids = 'parents_as_ids' in params and (
self.query_filter_is_true(params['parents_as_ids']))
subtree_as_list = 'subtree_as_list' in params and (
self.query_filter_is_true(params['subtree_as_list']))
# parents_as_list and parents_as_ids are mutually exclusive
if parents_as_list and parents_as_ids:
msg = _('Cannot use parents_as_list and parents_as_ids query '
'params at the same time.')
raise exception.ValidationError(msg)
user_id = self.get_auth_context(context).get('user_id')
if ('parents_as_list' in context['query_string'] and
self.query_filter_is_true(
context['query_string']['parents_as_list'])):
if parents_as_list:
parents = self.resource_api.list_project_parents(
ref['id'], user_id)
ref['parents'] = [ProjectV3.wrap_member(context, p)
for p in parents]
elif parents_as_ids:
ref['parents'] = self.resource_api.get_project_parents_as_ids(ref)
if ('subtree_as_list' in context['query_string'] and
self.query_filter_is_true(
context['query_string']['subtree_as_list'])):
if subtree_as_list:
subtree = self.resource_api.list_projects_in_subtree(
ref['id'], user_id)
ref['subtree'] = [ProjectV3.wrap_member(context, p)

View File

@ -230,6 +230,49 @@ class Manager(manager.Manager):
self._filter_projects_list(parents, user_id)
return parents
def _build_parents_as_ids_dict(self, project, parents_by_id):
# NOTE(rodrigods): we don't rely in the order of the projects returned
# by the list_project_parents() method. Thus, we create a project cache
# (parents_by_id) in order to access each parent in constant time and
# traverse up the hierarchy.
def traverse_parents_hierarchy(project):
parent_id = project.get('parent_id')
if not parent_id:
return None
parent = parents_by_id[parent_id]
return {parent_id: traverse_parents_hierarchy(parent)}
return traverse_parents_hierarchy(project)
def get_project_parents_as_ids(self, project):
"""Gets the IDs from the parents from a given project.
The project IDs are returned as a structured dictionary traversing up
the hierarchy to the top level project. For example, considering the
following project hierarchy::
A
|
+-B-+
| |
C D
If we query for project C parents, the expected return is the following
dictionary::
'parents': {
B['id']: {
A['id']: None
}
}
"""
parents_list = self.list_project_parents(project['id'])
parents_as_ids = self._build_parents_as_ids_dict(
project, {proj['id']: proj for proj in parents_list})
return parents_as_ids
def list_projects_in_subtree(self, project_id, user_id=None):
subtree = self.driver.list_projects_in_subtree(project_id)
# If a user_id was provided, the returned list should be filtered

View File

@ -509,7 +509,46 @@ class AssignmentTestCase(test_v3.RestfulTestCase):
'project_id': self.project_id})
self.assertValidProjectResponse(r, self.project)
def test_get_project_with_parents_list(self):
def test_get_project_with_parents_as_ids(self):
"""Call ``GET /projects/{project_id}?parents_as_ids``."""
projects = self._create_projects_hierarchy(hierarchy_size=2)
# Query for projects[2] parents_as_ids
r = self.get(
'/projects/%(project_id)s?parents_as_ids' % {
'project_id': projects[2]['project']['id']})
self.assertValidProjectResponse(r, projects[2]['project'])
parents_as_ids = r.result['project']['parents']
# Assert parents_as_ids is a structured dictionary correctly
# representing the hierarchy. The request was made using projects[2]
# id, hence its parents should be projects[1] and projects[0]. It
# should have the following structure:
# {
# projects[1]: {
# projects[0]: None
# }
# }
expected_dict = {
projects[1]['project']['id']: {
projects[0]['project']['id']: None
}
}
self.assertDictEqual(expected_dict, parents_as_ids)
# Query for projects[0] parents_as_ids
r = self.get(
'/projects/%(project_id)s?parents_as_ids' % {
'project_id': projects[0]['project']['id']})
self.assertValidProjectResponse(r, projects[0]['project'])
parents_as_ids = r.result['project']['parents']
# projects[0] has no parents, parents_as_ids must be None
self.assertIsNone(parents_as_ids)
def test_get_project_with_parents_as_list(self):
"""Call ``GET /projects/{project_id}?parents_as_list``."""
projects = self._create_projects_hierarchy(hierarchy_size=2)
@ -522,6 +561,17 @@ class AssignmentTestCase(test_v3.RestfulTestCase):
self.assertIn(projects[0], r.result['project']['parents'])
self.assertNotIn(projects[2], r.result['project']['parents'])
def test_get_project_with_parents_as_list_and_parents_as_ids(self):
"""Call ``GET /projects/{project_id}?parents_as_list&parents_as_ids``.
"""
projects = self._create_projects_hierarchy(hierarchy_size=2)
self.get(
'/projects/%(project_id)s?parents_as_list&parents_as_ids' % {
'project_id': projects[1]['project']['id']},
expected_status=400)
def test_get_project_with_subtree_list(self):
"""Call ``GET /projects/{project_id}?subtree_as_list``."""
projects = self._create_projects_hierarchy(hierarchy_size=2)