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
|
### 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
|
- Dualpath "single machine" site toggle (`dualpath_single_machine`, default
|
||||||
on). A Dualpath relationship pair is one physical dual-bay machine (single
|
on). A Dualpath relationship pair is one physical dual-bay machine (single
|
||||||
controller, bay-selector switch); when on, the machines list, dashboard and
|
controller, bay-selector switch); when on, the machines list, dashboard and
|
||||||
|
|||||||
@@ -28,6 +28,9 @@
|
|||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero-card">
|
<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-content">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<h1>{{ machine.assetnumber }}</h1>
|
<h1>{{ machine.assetnumber }}</h1>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
<template v-else-if="tool">
|
<template v-else-if="tool">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero-card">
|
<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-content">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<h1>{{ tool.assetnumber }}</h1>
|
<h1>{{ tool.assetnumber }}</h1>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="detail-page" v-if="device">
|
<div class="detail-page" v-if="device">
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<div class="hero-image">
|
<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>
|
<span class="icon"><component :is="getDeviceIcon()" :size="24" /></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
<template v-else-if="computer">
|
<template v-else-if="computer">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero-card">
|
<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-content">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<h1>{{ computer.assetnumber }}</h1>
|
<h1>{{ computer.assetnumber }}</h1>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@
|
|||||||
<template v-else-if="printer">
|
<template v-else-if="printer">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero-card">
|
<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-content">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<h1>{{ displayTitle }}</h1>
|
<h1>{{ displayTitle }}</h1>
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ class Computer(BaseModel):
|
|||||||
result['vendorname'] = self.vendor.vendor
|
result['vendorname'] = self.vendor.vendor
|
||||||
if self.model:
|
if self.model:
|
||||||
result['modelname'] = self.model.modelnumber
|
result['modelname'] = self.model.modelnumber
|
||||||
|
if self.model.imageurl:
|
||||||
|
result['imageurl'] = self.model.imageurl
|
||||||
|
|
||||||
# Names of enabled remote-access protocols (for list badges)
|
# Names of enabled remote-access protocols (for list badges)
|
||||||
result['accessprotocolnames'] = [
|
result['accessprotocolnames'] = [
|
||||||
|
|||||||
@@ -118,5 +118,7 @@ class Printer(BaseModel):
|
|||||||
result['vendorname'] = self.vendor.vendor
|
result['vendorname'] = self.vendor.vendor
|
||||||
if self.model:
|
if self.model:
|
||||||
result['modelname'] = self.model.modelnumber
|
result['modelname'] = self.model.modelnumber
|
||||||
|
if self.model.imageurl:
|
||||||
|
result['imageurl'] = self.model.imageurl
|
||||||
|
|
||||||
return result
|
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