Model photo upload fixes; network devices linked to catalog models

This commit is contained in:
2026-08-05 09:09:19 -04:00
parent d8e3280dfc
commit c51e86e686
6 changed files with 169 additions and 22 deletions

View File

@@ -346,7 +346,7 @@ def create_network_device():
Optional fields:
- name, serialnumber, statusid, locationid, businessunitid
- networkdevicetypeid, vendorid, hostname
- networkdevicetypeid, vendorid, modelnumberid, hostname
- firmwareversion, portcount, ispoe, ismanaged, rackunit
- mapx, mapy, notes
"""
@@ -408,6 +408,7 @@ def create_network_device():
assetid=asset.assetid,
networkdevicetypeid=data.get('networkdevicetypeid'),
vendorid=data.get('vendorid'),
modelnumberid=data.get('modelnumberid'),
hostname=data.get('hostname'),
firmwareversion=data.get('firmwareversion'),
portcount=data.get('portcount'),
@@ -495,7 +496,7 @@ def update_network_device(device_id: int):
setattr(asset, key, data[key])
# Update network device fields
netdev_fields = ['networkdevicetypeid', 'vendorid', 'hostname',
netdev_fields = ['networkdevicetypeid', 'vendorid', 'modelnumberid', 'hostname',
'firmwareversion', 'portcount', 'ispoe', 'ismanaged', 'rackunit']
for key in netdev_fields:
if key in data:

View File

@@ -133,6 +133,23 @@
</option>
</select>
</div>
<div class="form-group">
<!--
The catalog model is where the device photo comes from: the
detail page's hero image reads the model's imageurl, exactly as
machines, PCs and printers do.
-->
<label for="modelnumberid">Model</label>
<select id="modelnumberid" v-model="form.modelnumberid" class="form-control">
<option value="">Select Model</option>
<option v-for="m in models" :key="m.modelnumberid" :value="m.modelnumberid">
{{ m.modelnumber }}
</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="firmwareversion">Firmware Version</label>
<input
@@ -251,6 +268,7 @@ import { useRoute, useRouter } from 'vue-router'
import {
networkApi,
vendorsApi,
modelsApi,
locationsApi,
assetsApi,
businessunitsApi
@@ -284,6 +302,7 @@ const form = ref({
hostname: '',
networkdevicetypeid: '',
vendorid: '',
modelnumberid: '',
firmwareversion: '',
portcount: null,
rackunit: '',
@@ -296,6 +315,7 @@ const form = ref({
const deviceTypes = ref([])
const vendors = ref([])
const models = ref([])
const locations = ref([])
const statuses = ref([])
const businessUnits = ref([])
@@ -329,6 +349,8 @@ async function loadVendors() {
try {
const response = await vendorsApi.list({ perpage: 100 })
vendors.value = response.data.data || []
const modelResponse = await modelsApi.list({ perpage: 500 })
models.value = modelResponse.data.data || []
} catch (err) {
console.error('Error loading vendors:', err)
}
@@ -385,6 +407,7 @@ async function loadDevice() {
form.value.hostname = data.networkdevice.hostname || ''
form.value.networkdevicetypeid = data.networkdevice.networkdevicetypeid || ''
form.value.vendorid = data.networkdevice.vendorid || ''
form.value.modelnumberid = data.networkdevice.modelnumberid || ''
form.value.firmwareversion = data.networkdevice.firmwareversion || ''
form.value.portcount = data.networkdevice.portcount
form.value.rackunit = data.networkdevice.rackunit || ''
@@ -414,6 +437,7 @@ async function submitForm() {
hostname: form.value.hostname || null,
networkdevicetypeid: form.value.networkdevicetypeid || null,
vendorid: form.value.vendorid || null,
modelnumberid: form.value.modelnumberid || null,
firmwareversion: form.value.firmwareversion || null,
portcount: form.value.portcount || null,
rackunit: form.value.rackunit || null,

View File

@@ -0,0 +1,58 @@
"""Link a network device to a catalog model.
Machines, PCs and printers all carry modelnumberid, which is what lets an asset
detail page show the photo uploaded against its vendor model. Network devices
carried only vendorid, so NetworkDeviceDetail.vue rendered a hero image bound to
`networkdevice.imageurl` that nothing could ever populate - a feature that
looked present and could not work.
Nullable, with no backfill: a device whose model is not recorded is normal, and
guessing one from the vendor would put wrong photos on real hardware.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'network0002model'
down_revision = 'network0001anchor'
branch_labels = None
depends_on = None
def upgrade():
# Guarded exactly like employees0002photo. On a FRESH database the tables
# are built from the SQLAlchemy models, which already declare this column,
# so an unconditional add fails with "duplicate column name". On an existing
# server the column really is missing and gets added.
bind = op.get_bind()
insp = sa.inspect(bind)
if 'networkdevices' not in insp.get_table_names():
return
cols = {c['name'] for c in insp.get_columns('networkdevices')}
if 'modelnumberid' not in cols:
op.add_column('networkdevices', sa.Column('modelnumberid', sa.Integer(), nullable=True))
# Only where it is possible. SQLite has no ALTER TABLE ADD CONSTRAINT,
# and routing this through batch_alter_table - which rebuilds the table -
# made Alembic's column sort raise "Circular dependency detected". The
# column is what the relationship needs; the constraint is integrity on
# the real database.
if bind.dialect.name != 'sqlite':
op.create_foreign_key(
'fk_networkdevices_modelnumberid',
'networkdevices', 'models',
['modelnumberid'], ['modelnumberid'],
)
def downgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
if 'networkdevices' not in insp.get_table_names():
return
cols = {c['name'] for c in insp.get_columns('networkdevices')}
if 'modelnumberid' in cols:
if bind.dialect.name != 'sqlite':
op.drop_constraint('fk_networkdevices_modelnumberid', 'networkdevices',
type_='foreignkey')
op.drop_column('networkdevices', 'modelnumberid')

View File

@@ -49,6 +49,13 @@ class NetworkDevice(BaseModel):
)
# Vendor
modelnumberid = db.Column(
db.Integer,
db.ForeignKey('models.modelnumberid'),
nullable=True,
comment='Catalog model, which is where the device photo comes from'
)
vendorid = db.Column(
db.Integer,
db.ForeignKey('vendors.vendorid'),
@@ -97,6 +104,7 @@ class NetworkDevice(BaseModel):
)
networkdevicetype = db.relationship('NetworkDeviceType', backref='networkdevices')
vendor = db.relationship('Vendor', backref='network_devices')
model = db.relationship('Model', backref='network_devices')
__table_args__ = (
db.Index('idx_netdev_type', 'networkdevicetypeid'),
@@ -116,5 +124,11 @@ class NetworkDevice(BaseModel):
result['networkdevicetypename'] = self.networkdevicetype.networkdevicetype
if self.vendor:
result['vendorname'] = self.vendor.vendor
# Same shape as machines, PCs and printers: the detail page's hero image
# binds to imageurl, and it comes from the catalog model, not the device.
if self.model:
result['modelname'] = self.model.modelnumber
if self.model.imageurl:
result['imageurl'] = self.model.imageurl
return result