Model photo upload fixes; network devices linked to catalog models
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user