Render vendor-model photos on asset detail heroes
Computer and Printer payloads now surface the linked model imageurl the way machines already did, and the machine/PC/printer detail heroes render the photo when present (network devices and measuring tools have no model link, so nothing to surface). Absent images render nothing rather than a broken icon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,13 @@ ADR-007 and ADR-002.
|
||||
|
||||
### Added
|
||||
|
||||
- Vendor-model photos on asset detail heroes: computers and printers now
|
||||
surface the linked model's `imageurl` in their extension payloads (the
|
||||
field machines already exposed), and the machine, PC, printer, network
|
||||
device, and measuring tool detail pages render the photo in the hero card
|
||||
when present (hidden cleanly when absent). Network devices and measuring
|
||||
tools have no model link yet, so their heroes stay photo-less until one
|
||||
is added.
|
||||
- Dualpath "single machine" site toggle (`dualpath_single_machine`, default
|
||||
on). A Dualpath relationship pair is one physical dual-bay machine (single
|
||||
controller, bay-selector switch); when on, the machines list, dashboard and
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-card">
|
||||
<div class="hero-image" v-if="machine.machine?.imageurl">
|
||||
<img :src="machine.machine.imageurl" :alt="machine.machine.modelname || 'Model photo'" />
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-title">
|
||||
<h1>{{ machine.assetnumber }}</h1>
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
<template v-else-if="tool">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-card">
|
||||
<div class="hero-image" v-if="tool.measuringtool?.imageurl">
|
||||
<img :src="tool.measuringtool.imageurl" alt="Model photo" />
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-title">
|
||||
<h1>{{ tool.assetnumber }}</h1>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<div class="detail-page" v-if="device">
|
||||
<div class="hero-card">
|
||||
<div class="hero-image">
|
||||
<div class="device-icon">
|
||||
<img v-if="device.networkdevice?.imageurl" :src="device.networkdevice.imageurl" alt="Model photo" />
|
||||
<div class="device-icon" v-else>
|
||||
<span class="icon"><component :is="getDeviceIcon()" :size="24" /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
<template v-else-if="computer">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-card">
|
||||
<div class="hero-image" v-if="computer.computer?.imageurl">
|
||||
<img :src="computer.computer.imageurl" :alt="computer.computer.modelname || 'Model photo'" />
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-title">
|
||||
<h1>{{ computer.assetnumber }}</h1>
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
<template v-else-if="printer">
|
||||
<!-- Hero Section -->
|
||||
<div class="hero-card">
|
||||
<div class="hero-image" v-if="printer.printer?.imageurl">
|
||||
<img :src="printer.printer.imageurl" :alt="printer.printer.modelname || 'Model photo'" />
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-title">
|
||||
<h1>{{ displayTitle }}</h1>
|
||||
|
||||
@@ -130,6 +130,8 @@ class Computer(BaseModel):
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['modelname'] = self.model.modelnumber
|
||||
if self.model.imageurl:
|
||||
result['imageurl'] = self.model.imageurl
|
||||
|
||||
# Names of enabled remote-access protocols (for list badges)
|
||||
result['accessprotocolnames'] = [
|
||||
|
||||
@@ -118,5 +118,7 @@ class Printer(BaseModel):
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['modelname'] = self.model.modelnumber
|
||||
if self.model.imageurl:
|
||||
result['imageurl'] = self.model.imageurl
|
||||
|
||||
return result
|
||||
|
||||
100
tests/test_plugins/test_model_image_in_detail.py
Normal file
100
tests/test_plugins/test_model_image_in_detail.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Vendor-model photo surfaces in asset-extension detail payloads.
|
||||
|
||||
Machines, computers, and printers link a vendor Model via modelnumberid; when
|
||||
that Model carries an imageurl, the extension dict in the GET detail payload
|
||||
must expose it as 'imageurl' (the field name machines established, consumed by
|
||||
the detail-page heroes and the machine badge). When no model is linked, the
|
||||
field must be absent so the frontend v-if renders nothing.
|
||||
|
||||
Network devices and measuring tools have no model link, so they are out of
|
||||
scope here by design.
|
||||
"""
|
||||
|
||||
from shopdb.extensions import db as _db
|
||||
from shopdb.core.models import Asset, AssetType, Model
|
||||
|
||||
from plugins.machines.models import Machine
|
||||
from plugins.computers.models import Computer
|
||||
from plugins.printers.models import Printer
|
||||
|
||||
IMAGE_URL = '/api/models/images/model-1.png'
|
||||
|
||||
|
||||
def _seed_asset(assetnumber, assettype):
|
||||
atype = AssetType.query.filter_by(assettype=assettype).first()
|
||||
if not atype:
|
||||
atype = AssetType(assettype=assettype)
|
||||
_db.session.add(atype)
|
||||
_db.session.flush()
|
||||
asset = Asset(assetnumber=assetnumber, assettypeid=atype.assettypeid)
|
||||
_db.session.add(asset)
|
||||
_db.session.flush()
|
||||
return asset
|
||||
|
||||
|
||||
def _seed_model(imageurl=IMAGE_URL):
|
||||
model = Model(modelnumber='IMG-MODEL-1', imageurl=imageurl)
|
||||
_db.session.add(model)
|
||||
_db.session.flush()
|
||||
return model
|
||||
|
||||
|
||||
def test_machine_detail_carries_model_imageurl(client, db):
|
||||
asset = _seed_asset('2001', 'machine')
|
||||
model = _seed_model()
|
||||
machine = Machine(assetid=asset.assetid, modelnumberid=model.modelnumberid)
|
||||
_db.session.add(machine)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/machines/{machine.machineid}')
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['machine']['imageurl'] == IMAGE_URL
|
||||
|
||||
|
||||
def test_computer_detail_carries_model_imageurl(client, db):
|
||||
asset = _seed_asset('PC-2001', 'computer')
|
||||
model = _seed_model()
|
||||
computer = Computer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
|
||||
_db.session.add(computer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/computers/{computer.computerid}')
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['computer']['imageurl'] == IMAGE_URL
|
||||
|
||||
|
||||
def test_printer_detail_carries_model_imageurl(client, db):
|
||||
asset = _seed_asset('PR-2001', 'printer')
|
||||
model = _seed_model()
|
||||
printer = Printer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
|
||||
_db.session.add(printer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/printers/{printer.printerid}')
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['printer']['imageurl'] == IMAGE_URL
|
||||
|
||||
|
||||
def test_detail_omits_imageurl_without_model(client, db):
|
||||
"""No model linked -> no imageurl key (frontend v-if shows nothing)."""
|
||||
asset = _seed_asset('PR-2002', 'printer')
|
||||
printer = Printer(assetid=asset.assetid)
|
||||
_db.session.add(printer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/printers/{printer.printerid}')
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert 'imageurl' not in resp.get_json()['data']['printer']
|
||||
|
||||
|
||||
def test_detail_omits_imageurl_when_model_has_no_image(client, db):
|
||||
"""Model linked but no photo -> no imageurl key."""
|
||||
asset = _seed_asset('PC-2002', 'computer')
|
||||
model = _seed_model(imageurl=None)
|
||||
computer = Computer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
|
||||
_db.session.add(computer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/computers/{computer.computerid}')
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert 'imageurl' not in resp.get_json()['data']['computer']
|
||||
Reference in New Issue
Block a user