From 890334413ac65f27e195aa209811114b62af6257 Mon Sep 17 00:00:00 2001 From: Stephen Knox Date: Mon, 27 Jul 2026 09:41:11 +0000 Subject: [PATCH 1/2] Add an update_network_appdata function for a network to make appdata easier to manage --- hydra_base/lib/network.py | 44 ++++++++++++++++++++++-- tests/test_network.py | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/hydra_base/lib/network.py b/hydra_base/lib/network.py index 9f1eb47b..b42d5256 100644 --- a/hydra_base/lib/network.py +++ b/hydra_base/lib/network.py @@ -1665,6 +1665,33 @@ def update_resource_layout(resource_type, resource_id, key, value, **kwargs): return layout +def update_network_appdata(network_id, key, value, **kwargs): + """ + Update a single key in a network's appdata, without touching any + other network fields (name, description, projection, layout, etc). + This assumes that appdata is a JSON compatible Dictionary. + """ + user_id = kwargs.get('user_id') + log.info("Updating network %s's appdata with {%s:%s}", network_id, key, value) + try: + net_i = db.DBSession.query(Network).filter(Network.id == network_id).one() + except NoResultFound: + raise ResourceNotFoundError("Network %s not found"%(network_id)) + + net_i.check_write_permission(user_id) + + if net_i.appdata is None: + appdata = dict() + else: + appdata = json.loads(net_i.appdata) + + appdata[key] = value + net_i.appdata = json.dumps(appdata) + + db.DBSession.flush() + + return appdata + def get_resource(resource_type, resource_id, **kwargs): user_id = kwargs.get('user_id') @@ -1715,7 +1742,9 @@ def get_network_extents(network_id,**kwargs): min_alt_x=None, max_alt_x=None, min_alt_y=None, - max_alt_y=None + max_alt_y=None, + has_geographic=False, + has_schematic=False ) # Compute min/max extent of the network. @@ -1752,6 +1781,15 @@ def get_network_extents(network_id,**kwargs): # Default y extent if all None values min_alt_y, max_alt_y = 0, 1 + # min/max default to fake 0/1 (or 0/100 in some callers) ranges when a + # coordinate system has no data at all, which the frontend can't tell + # apart from "genuinely spans 0 to 1" - these booleans let it reliably + # detect presence instead (see hwi's network.html: hasGeographicView/ + # hasSchematicView, which gate whether the map/schematic view - and the + # dual-view switch button - are offered at all). + has_geographic = len(x) > 0 and len(y) > 0 + has_schematic = len(alt_x) > 0 and len(alt_y) > 0 + ne = JSONObject(dict( network_id = network_id, min_x=x_min, @@ -1761,7 +1799,9 @@ def get_network_extents(network_id,**kwargs): min_alt_x=min_alt_x, max_alt_x=max_alt_x, min_alt_y=min_alt_y, - max_alt_y=max_alt_y + max_alt_y=max_alt_y, + has_geographic=has_geographic, + has_schematic=has_schematic )) return ne diff --git a/tests/test_network.py b/tests/test_network.py index ee8336e4..df43e85d 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -301,6 +301,77 @@ def test_get_extents(self, client, network_with_data): assert extents.max_x == 100 assert extents.min_y == 9 assert extents.max_y == 99 + assert extents.has_geographic is True + assert extents.has_schematic is True + + def test_get_extents_empty_network(self, client, projectmaker): + """ + A network with no nodes at all has no coordinates in either system. + """ + project = projectmaker.create('test') + + network = dict( + name = 'Network @ %s'%datetime.datetime.now(), + description = 'Test network with no nodes', + project_id = project.id, + links = [], + nodes = [], + layout = {}, + scenarios = [], + resourcegroups = [], + projection = None, + attributes = [], + ) + network = client.add_network(network) + + extents = client.get_network_extents(network.id) + + assert extents.min_x is None + assert extents.max_x is None + assert extents.min_y is None + assert extents.max_y is None + assert extents.has_geographic is False + assert extents.has_schematic is False + + def test_update_network_appdata(self, client, network_with_data): + """ + Test that a single key can be set in a network's appdata without + touching any other network fields, and that the result persists. + """ + net = network_with_data + + newappdata = client.update_network_appdata(net.id, 'dualViewEnabled', True) + + assert newappdata['dualViewEnabled'] is True + + # Setting a second key should not clobber the first. + newappdata = client.update_network_appdata(net.id, 'schematicGridSize', 25) + + assert newappdata['dualViewEnabled'] is True + assert newappdata['schematicGridSize'] == 25 + + updated_net = client.get_network(net.id) + persisted_appdata = json.loads(updated_net.appdata) + + assert persisted_appdata['dualViewEnabled'] is True + assert persisted_appdata['schematicGridSize'] == 25 + + def test_update_network_appdata_no_permission(self, client, projectmaker, networkmaker): + """ + A user with no write access to a network's project must not be able + to update its appdata. + """ + # 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.update_network_appdata(net.id, 'dualViewEnabled', True) + finally: + client.login('root', '') def test_update_network(self, client, network_with_data): From 917af05cc1e320480a8355296a5cc555cbd9fe57 Mon Sep 17 00:00:00 2001 From: Stephen Knox Date: Mon, 27 Jul 2026 09:44:41 +0000 Subject: [PATCH 2/2] Add an update_project_appdata function and test --- hydra_base/lib/project.py | 27 ++++++++++++++++++++++++++ tests/project/test_project.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/hydra_base/lib/project.py b/hydra_base/lib/project.py index fcd19c31..43627921 100644 --- a/hydra_base/lib/project.py +++ b/hydra_base/lib/project.py @@ -172,6 +172,33 @@ def update_project(project, **kwargs): return proj_i +@required_perms('edit_project') +def update_project_appdata(project_id, key, value, **kwargs): + """ + Update a single key in a project's appdata, without touching any + other project fields (name, description, parent_id, etc). + Unlike Network.appdata (a Text column, manually JSON (de)serialized - + see update_network_appdata), Project.appdata is a native JSON column, + so no json.dumps/loads is needed here. + """ + user_id = kwargs.get('user_id') + + proj_i = _get_project(project_id, user_id, check_write=True) + + if proj_i.appdata is None: + appdata = {} + else: + appdata = proj_i.appdata.copy() + + appdata[key] = value + proj_i.appdata = appdata + + db.DBSession.flush() + + Project.clear_cache(user_id) + + return appdata + @required_perms('edit_project') def move_project(project_id, target_project_id, **kwargs): """ diff --git a/tests/project/test_project.py b/tests/project/test_project.py index 5dd3b45d..eb833427 100644 --- a/tests/project/test_project.py +++ b/tests/project/test_project.py @@ -126,6 +126,42 @@ def test_update(self, client, network_with_data): rs_to_check.dataset.value == 'just project desscriptor', \ "There is an inconsistency with the attributes." + def test_update_project_appdata(self, client, projectmaker): + """ + Test that a single key can be set in a project's appdata without + touching any other project fields, and that the result persists. + """ + proj = projectmaker.create() + + newappdata = client.update_project_appdata(proj.id, 'dualViewEnabled', True) + + assert newappdata['dualViewEnabled'] is True + + # Setting a second key should not clobber the first. + newappdata = client.update_project_appdata(proj.id, 'favouriteColour', 'blue') + + assert newappdata['dualViewEnabled'] is True + assert newappdata['favouriteColour'] == 'blue' + + updated_project = client.get_project(proj.id) + + assert updated_project.appdata['dualViewEnabled'] is True + assert updated_project.appdata['favouriteColour'] == 'blue' + + def test_update_project_appdata_no_permission(self, client, projectmaker): + """ + A user with no write access to a project must not be able to + update its appdata. + """ + proj = projectmaker.create(share=False) + + with pytest.raises(hb.exceptions.HydraError): + #check for non-admin, non-owning users + client.user_id = 5 + client.update_project_appdata(proj.id, 'dualViewEnabled', True) + #set back to admin + client.user_id = 1 + def test_load(self, client): project = JSONObject({}) project.name = 'Test Project %s'%(datetime.datetime.now())