diff --git a/hydra_base/lib/attributes.py b/hydra_base/lib/attributes.py index 62ae63a0..15ef1744 100644 --- a/hydra_base/lib/attributes.py +++ b/hydra_base/lib/attributes.py @@ -134,20 +134,18 @@ def get_attributes_by_id(attr_ids, **kwargs): def get_template_attributes(template_id, **kwargs): """ - Get a specific attribute by its ID. + Get all attributes linked to a template via template types. """ + import hydra_base.lib.template as templatelib + template = templatelib.get_template(template_id, **kwargs) + attr_id_map = {} - try: - attrs_i = db.DBSession.query(Attr).filter( - TemplateType.template_id == template_id).filter( - TypeAttr.type_id == TemplateType.id).filter( - Attr.id == TypeAttr.id).all() - - log.debug(attrs_i) - return attrs_i - except NoResultFound: - return None - + for tt in template.templatetypes: + for ta in tt.typeattrs: + attr_id_map[ta.attr_id] = ta.attr + attrs = list(attr_id_map.values()) + log.info("Attributes linked to template %s: %s", template_id, len(attrs)) + return [JSONObject(a) for a in attrs] def get_attribute_by_name_and_dimension(name, dimension_id=None, network_id=None, project_id=None, **kwargs): """ @@ -1244,11 +1242,12 @@ def get_all_resource_attributes(ref_key, network_id, template_id=None, **kwargs) """ Get all the resource attributes for a given resource type in the network. That includes all the resource attributes for a given type within the network. - For example, if the ref_key is 'NODE', then it will return all the attirbutes + For example, if the ref_key is 'NODE', then it will return all the attributes of all nodes in the network. This function allows a front end to pre-load an entire network's resource attribute information to reduce on function calls. - If type_id is specified, only + If template_id is specified, only return the resource attributes within the type. + NOTE: This uses discrete queries per resource type for performance reasons. """ user_id = kwargs.get('user_id') @@ -1256,39 +1255,76 @@ def get_all_resource_attributes(ref_key, network_id, template_id=None, **kwargs) net = _get_network(network_id) net.check_read_permission(user_id, do_raise=True) - resource_attr_qry = db.DBSession.query(ResourceAttr).\ - outerjoin(Node, Node.id == ResourceAttr.node_id).\ - outerjoin(Link, Link.id == ResourceAttr.link_id).\ - outerjoin(ResourceGroup, ResourceGroup.id == ResourceAttr.group_id).filter( - ResourceAttr.ref_key == ref_key, - or_( - and_(ResourceAttr.node_id != None, - ResourceAttr.node_id == Node.id, - Node.network_id == network_id), - - and_(ResourceAttr.link_id != None, - ResourceAttr.link_id == Link.id, - Link.network_id == network_id), - - and_(ResourceAttr.group_id != None, - ResourceAttr.group_id == ResourceGroup.id, - ResourceGroup.network_id == network_id) - )) + resource_attrs = [] - if template_id is not None: - attr_ids = [] - rs = db.DBSession.query(TypeAttr).join( - TemplateType, - TemplateType.id == TypeAttr.type_id).filter( - TemplateType.template_id == template_id).all() - for r in rs: - attr_ids.append(r.attr_id) - - resource_attr_qry = resource_attr_qry.filter(ResourceAttr.attr_id.in_(attr_ids)) + ref_key_norm = ref_key.upper() - resource_attrs = resource_attr_qry.all() + # If a template_id is provided, resolve attr_ids via get_template() so that + # inherited type attributes (from parent templates/types) are included. + attr_ids = None + if template_id is not None: + import hydra_base.lib.template as templatelib + template = templatelib.get_template(template_id, **kwargs) + attr_ids = list({ta.attr_id for tt in template.templatetypes for ta in tt.typeattrs}) + if not attr_ids: + return [] + + if ref_key_norm == 'NODE': + qry = db.DBSession.query(ResourceAttr, Attr.name, Attr.id, Attr.description).\ + join(Node, Node.id == ResourceAttr.node_id).\ + join(Attr, Attr.id == ResourceAttr.attr_id).\ + filter( + ResourceAttr.node_id != None, + Node.network_id == network_id, + ResourceAttr.ref_key == ref_key_norm) + if attr_ids is not None: + qry = qry.filter(ResourceAttr.attr_id.in_(attr_ids)) + resource_attrs = qry.all() + + elif ref_key_norm == 'LINK': + qry = db.DBSession.query(ResourceAttr, Attr.name, Attr.id, Attr.description).\ + join(Link, Link.id == ResourceAttr.link_id).\ + join(Attr, Attr.id == ResourceAttr.attr_id).\ + filter( + ResourceAttr.link_id != None, + Link.network_id == network_id, + ResourceAttr.ref_key == ref_key_norm) + if attr_ids is not None: + qry = qry.filter(ResourceAttr.attr_id.in_(attr_ids)) + resource_attrs = qry.all() + + elif ref_key_norm == 'GROUP': + qry = db.DBSession.query(ResourceAttr, Attr.name, Attr.id, Attr.description).\ + join(ResourceGroup, ResourceGroup.id == ResourceAttr.group_id).\ + join(Attr, Attr.id == ResourceAttr.attr_id).\ + filter( + ResourceAttr.group_id != None, + ResourceGroup.network_id == network_id, + ResourceAttr.ref_key == ref_key_norm) + if attr_ids is not None: + qry = qry.filter(ResourceAttr.attr_id.in_(attr_ids)) + resource_attrs = qry.all() + + elif ref_key_norm == 'NETWORK': + qry = db.DBSession.query(ResourceAttr, Attr.name, Attr.id, Attr.description).\ + join(Attr, Attr.id == ResourceAttr.attr_id).\ + filter( + ResourceAttr.network_id == network_id, + ResourceAttr.ref_key == ref_key_norm) + if attr_ids is not None: + qry = qry.filter(ResourceAttr.attr_id.in_(attr_ids)) + resource_attrs = qry.all() + + # Convert results to JSONObjects with attribute data included + result_objects = [] + for ra in resource_attrs: + ra_obj = JSONObject(ra[0]) # ResourceAttr object + ra_obj.attr_id = ra[2] # Attr.id + ra_obj.name = ra[1] # Attr.name + ra_obj.description = ra[3] # Attr.description + result_objects.append(ra_obj) - return resource_attrs + return result_objects def get_resource_attributes(ref_key, ref_id, type_id=None, **kwargs): """ diff --git a/hydra_base/lib/network.py b/hydra_base/lib/network.py index 10bb8f13..d978d15c 100644 --- a/hydra_base/lib/network.py +++ b/hydra_base/lib/network.py @@ -2661,6 +2661,46 @@ def get_all_resource_attributes_in_network(attr_id, network_id, include_resource return json_ra +def get_all_attributes_in_network(network_id, **kwargs): + """ + Find every attribute def (not resource attribute) + Args: + network_id (int): The ID of the network to search + Returns: + List of JSONObjects + Raises: + HydraError if the network_id does not exist + """ + + user_id = kwargs.get('user_id') + + #check the user can read the network + try: + net = db.DBSession.query(Network).filter(Network.id == network_id).one() + except NoResultFound: + raise HydraError("Network %s not found" % (network_id,)) + net.check_read_permission(user_id) + + network_attr_ids = db.DBSession.query(ResourceAttr.attr_id.label('attr_id')).filter( + ResourceAttr.network_id == network_id + ) + node_attr_ids = db.DBSession.query(ResourceAttr.attr_id.label('attr_id')).join( + Node, ResourceAttr.node_id == Node.id + ).filter(Node.network_id == network_id) + link_attr_ids = db.DBSession.query(ResourceAttr.attr_id.label('attr_id')).join( + Link, ResourceAttr.link_id == Link.id + ).filter(Link.network_id == network_id) + group_attr_ids = db.DBSession.query(ResourceAttr.attr_id.label('attr_id')).join( + ResourceGroup, ResourceAttr.group_id == ResourceGroup.id + ).filter(ResourceGroup.network_id == network_id) + + all_attr_ids = network_attr_ids.union(node_attr_ids, link_attr_ids, group_attr_ids).subquery() + + attrs = db.DBSession.query(Attr).join(all_attr_ids, Attr.id == all_attr_ids.c.attr_id).all() + + return [JSONObject(a) for a in attrs] + + def get_all_resource_data( scenario_id, include_metadata=False, diff --git a/hydra_base/lib/template/__init__.py b/hydra_base/lib/template/__init__.py index 57756468..20f66d7a 100644 --- a/hydra_base/lib/template/__init__.py +++ b/hydra_base/lib/template/__init__.py @@ -617,19 +617,19 @@ def update_template(template, auto_delete=False, **kwargs): should be deleted automatically. This flag is also used when updating the typeattrs of type. Defaults to False. """ - tmpl = db.DBSession.query(Template).filter(Template.id == template.id).one() - tmpl.name = template.name + tmpl_i = db.DBSession.query(Template).filter(Template.id == template.id).one() + tmpl_i.name = template.name if template.status is not None: - tmpl.status = template.status + tmpl_i.status = template.status if template.description: - tmpl.description = template.description + tmpl_i.description = template.description - template_types = tmpl.get_types() + template_types = tmpl_i.get_types() if template.layout: - tmpl.layout = get_json_as_string(template.layout) + tmpl_i.layout = get_json_as_string(template.layout) type_dict = dict([(t.id, t) for t in template_types]) @@ -640,9 +640,9 @@ def update_template(template, auto_delete=False, **kwargs): types = template.types if template.types is not None else template.templatetypes for templatetype in types: - if templatetype.id is not None and templatetype.template_id != tmpl.id: + if templatetype.id is not None and templatetype.template_id != tmpl_i.id: log.debug("Type %s is a part of a parent template. Ignoring.", templatetype.id) - req_templatetype_ids.append(type_i.id) + req_templatetype_ids.append(templatetype.id) continue if templatetype.id is not None: @@ -662,9 +662,9 @@ def update_template(template, auto_delete=False, **kwargs): db.DBSession.flush() - updated_templatetypes = tmpl.get_types() + updated_templatetypes = tmpl_i.get_types() - tmpl_j = JSONObject(tmpl) + tmpl_j = JSONObject(tmpl_i) tmpl_j.templatetypes = updated_templatetypes diff --git a/tests/test_network.py b/tests/test_network.py index 5953a7fc..5eb179bb 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -167,6 +167,69 @@ def test_get_all_resource_attributes_in_network(self, client, network_with_data) #Find the attribute that ALL nodes have. assert len(all_network_resource_attrs) == len(network_with_data.nodes) + def test_get_all_attributes_in_network(self, client, network_with_data): + """ + Test that get_all_attributes_in_network returns unique attribute + definitions (not resource attributes) for all resource types in the + network: network-level, nodes, links, and groups. + """ + net = network_with_data + + all_attrs = client.get_all_attributes_in_network(net.id) + + assert len(all_attrs) > 0 + + # Collect all attr_ids actually used across every resource type in the network + expected_attr_ids = set() + for ra in net.attributes: + expected_attr_ids.add(ra.attr_id) + for node in net.nodes: + for ra in node.attributes: + expected_attr_ids.add(ra.attr_id) + for link in net.links: + for ra in link.attributes: + expected_attr_ids.add(ra.attr_id) + for group in net.resourcegroups: + for ra in group.attributes: + expected_attr_ids.add(ra.attr_id) + + returned_attr_ids = {a.id for a in all_attrs} + + # Every attribute used in the network should be in the result + assert expected_attr_ids == returned_attr_ids + + def test_get_all_attributes_in_network_deduplication(self, client, network_with_data): + """ + Test that get_all_attributes_in_network de-duplicates attribute + definitions that appear on multiple resources (e.g. the same attr_id + shared by all nodes or shared between links and groups). + """ + net = network_with_data + + all_attrs = client.get_all_attributes_in_network(net.id) + + # Each attribute definition must appear exactly once + returned_ids = [a.id for a in all_attrs] + assert len(returned_ids) == len(set(returned_ids)), ( + "Duplicate attribute IDs found in get_all_attributes_in_network result" + ) + + def test_get_all_attributes_in_network_permissions(self, client, projectmaker, networkmaker): + """ + Test that a user without read permission on the network cannot call + get_all_attributes_in_network. + """ + # Create a project that is NOT shared with other users + private_proj = projectmaker.create(name=None, share=False) + net = networkmaker.create(project_id=private_proj.id) + + # UserD has not been granted access to this private network/project + client.login('UserD', 'password') + try: + with pytest.raises(hb.exceptions.HydraError): + client.get_all_attributes_in_network(net.id) + finally: + client.login('root', '') def test_get_network_1(self, client, networkmaker): """