Give a network device somewhere to put its IP
The network device form had no IP field, so the one thing people look up a switch for could not be entered. The API had accepted an ipaddress since the legacy import work - create, update and every read already carried it - but nothing in the UI ever sent one, which also left the device off the "Devices on this network" list, since that matches on the IP a device does not have. The field sits next to Hostname and round-trips through the Communication row the platform keeps IPs in, the same way the PC and printer forms do. Clearing it now clears the stored address. The upsert helper returned early on a blank value despite a docstring promising it cleared - harmless while no form could submit one, wrong the moment this field existed.
This commit is contained in:
@@ -24,9 +24,13 @@ def _upsert_primary_ip(asset, ip):
|
||||
"""Create/update/clear the asset's primary IP Communication from an
|
||||
ipaddress string. No-op if the IP CommunicationType is not seeded."""
|
||||
ip = (ip or '').strip()
|
||||
if not ip:
|
||||
return
|
||||
comm = Communication.query.filter_by(assetid=asset.assetid, isprimary=True).first()
|
||||
if not ip:
|
||||
# Clearing the field clears the row's IP - it used to return early, so
|
||||
# an address could be typed but never taken back out.
|
||||
if comm:
|
||||
comm.ipaddress = None
|
||||
return
|
||||
if comm:
|
||||
comm.ipaddress = ip
|
||||
return
|
||||
|
||||
@@ -112,6 +112,16 @@
|
||||
placeholder="e.g., sw-bldg1-floor2"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ipaddress">IP Address</label>
|
||||
<input
|
||||
id="ipaddress"
|
||||
v-model="form.ipaddress"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="e.g., 192.168.1.10"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="networkdevicetypeid">Device Type</label>
|
||||
<select id="networkdevicetypeid" v-model="form.networkdevicetypeid" class="form-control">
|
||||
@@ -318,6 +328,7 @@ const form = ref({
|
||||
locationid: '',
|
||||
businessunitid: '',
|
||||
hostname: '',
|
||||
ipaddress: '',
|
||||
networkdevicetypeid: '',
|
||||
vendorid: '',
|
||||
modelnumberid: '',
|
||||
@@ -419,6 +430,9 @@ async function loadDevice() {
|
||||
form.value.mapx = data.mapx
|
||||
form.value.mapy = data.mapy
|
||||
form.value.notes = data.notes || ''
|
||||
// The IP lives in a Communication row, not on the extension table; the API
|
||||
// flattens it onto the response as ipaddress.
|
||||
form.value.ipaddress = data.ipaddress || ''
|
||||
|
||||
// Network device specific
|
||||
if (data.networkdevice) {
|
||||
@@ -453,6 +467,7 @@ async function submitForm() {
|
||||
locationid: form.value.locationid || null,
|
||||
businessunitid: form.value.businessunitid || null,
|
||||
hostname: form.value.hostname || null,
|
||||
ipaddress: form.value.ipaddress || null,
|
||||
networkdevicetypeid: form.value.networkdevicetypeid || null,
|
||||
vendorid: form.value.vendorid || null,
|
||||
modelnumberid: form.value.modelnumberid || null,
|
||||
|
||||
@@ -57,6 +57,39 @@ def test_update_network_device_ipaddress_upserts(client, db, auth_headers, netwo
|
||||
assert len(comms) == 1 and comms[0].ipaddress == '10.0.0.9'
|
||||
|
||||
|
||||
def test_clearing_the_ip_takes_it_back_out(client, db, auth_headers, network_setup):
|
||||
"""Blanking the form field must clear the stored IP.
|
||||
|
||||
The upsert helper returned early on a blank value, so an address could be
|
||||
typed and never removed - invisible until the form grew an IP field.
|
||||
"""
|
||||
from shopdb.core.models import Communication
|
||||
created = client.post('/api/network', json={
|
||||
'assetnumber': 'SW-09', 'name': 'Switch', 'ipaddress': '10.0.0.5',
|
||||
}, headers=auth_headers).get_json()['data']
|
||||
devid = created['networkdevice']['networkdeviceid']
|
||||
|
||||
cleared = client.put(f'/api/network/{devid}', json={'ipaddress': ''},
|
||||
headers=auth_headers)
|
||||
assert cleared.status_code == 200, cleared.get_json()
|
||||
assert not cleared.get_json()['data']['ipaddress']
|
||||
|
||||
comms = Communication.query.filter_by(assetid=created['assetid']).all()
|
||||
assert len(comms) == 1 and comms[0].ipaddress is None
|
||||
|
||||
|
||||
def test_omitting_ipaddress_leaves_it_alone(client, db, auth_headers, network_setup):
|
||||
"""A PUT that never mentions the field must not wipe the stored IP."""
|
||||
created = client.post('/api/network', json={
|
||||
'assetnumber': 'SW-10', 'name': 'Switch', 'ipaddress': '10.0.0.7',
|
||||
}, headers=auth_headers).get_json()['data']
|
||||
devid = created['networkdevice']['networkdeviceid']
|
||||
|
||||
updated = client.put(f'/api/network/{devid}', json={'hostname': 'sw-10'},
|
||||
headers=auth_headers)
|
||||
assert updated.get_json()['data']['ipaddress'] == '10.0.0.7'
|
||||
|
||||
|
||||
def test_seed_reference_data_creates_ip_commtype(app, db):
|
||||
"""The reference-data seed now creates the IP communication type the asset
|
||||
import depends on."""
|
||||
|
||||
Reference in New Issue
Block a user