Let a printer driver carry the name Windows actually matches on
The driver form and the API both dropped `drivername` and `vendorid`. POST returned 201 with both null, PUT accepted them and stored nothing, and no screen offered either field - so there was no interface in the product that could set them. That is not a cosmetic gap. `drivername` is the exact string the INF declares, and Add-PrinterDriver matches on it; `name` is ours to choose and means nothing to Windows. With it null the shipped converger has nothing to match and installs nothing, and the drift check that compares an observed driver name against the expected one could never fire, because the expected value was never stored. `vendorid` is how a universal driver (HP UPD, Xerox GPD) attaches to a whole make instead of needing a near-duplicate row per model. Both are now read on create and update, and the driver form gained the two fields with hints saying what they are for - the INF name box says plainly that leaving it blank means the converger cannot install that driver. A whitespace value clears rather than stores: an empty box means "not known", and a blank string would look set and match nothing. Three tests. Verified by planting: drop the two fields from the create path again and the round-trip test fails. Scope: product
This commit is contained in:
@@ -163,10 +163,20 @@ def create_driver():
|
||||
data = request.get_json() or {}
|
||||
if not (data.get('name') and data.get('location')):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'name and location are required')
|
||||
# drivername and vendorid are accepted here because they are the two
|
||||
# fields the converger actually uses. drivername is the exact string from
|
||||
# the INF that Add-PrinterDriver matches on - `name` is ours to choose and
|
||||
# means nothing to Windows - and vendorid attaches a universal driver to a
|
||||
# whole make. Neither was read from the payload, so both came back null on
|
||||
# a 201 and there was no way to set them through any interface: the
|
||||
# converger had nothing to match and installed nothing, and driver-name
|
||||
# drift could never be detected because the expected value was never there.
|
||||
d = PrinterDriver(
|
||||
name=data['name'],
|
||||
location=data['location'],
|
||||
description=data.get('description'),
|
||||
drivername=(data.get('drivername') or '').strip() or None,
|
||||
vendorid=data.get('vendorid') or None,
|
||||
modelnumberid=data.get('modelnumberid') or None,
|
||||
isactive=data.get('isactive', True),
|
||||
)
|
||||
@@ -185,8 +195,11 @@ def update_driver(driver_id):
|
||||
for key in ('name', 'location', 'description', 'isactive'):
|
||||
if key in data:
|
||||
setattr(d, key, data[key])
|
||||
if 'modelnumberid' in data:
|
||||
d.modelnumberid = data['modelnumberid'] or None
|
||||
if 'drivername' in data:
|
||||
d.drivername = (data['drivername'] or '').strip() or None
|
||||
for key in ('vendorid', 'modelnumberid'):
|
||||
if key in data:
|
||||
setattr(d, key, data[key] or None)
|
||||
db.session.commit()
|
||||
return success_response(d.to_dict(), message='Driver updated')
|
||||
|
||||
|
||||
@@ -61,6 +61,26 @@
|
||||
<label>Name *</label>
|
||||
<input v-model="form.name" type="text" class="form-control" maxlength="150" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
INF Driver Name
|
||||
<span class="hint">(the exact string the INF declares - what
|
||||
Add-PrinterDriver matches on. Blank means the converger
|
||||
cannot install this driver.)</span>
|
||||
</label>
|
||||
<input v-model="form.drivername" type="text" class="form-control"
|
||||
maxlength="255"
|
||||
placeholder="HP Universal Printing PCL 6" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Vendor <span class="hint">(a universal driver serves every printer of that make; a model below wins over it)</span></label>
|
||||
<select v-model="form.vendorid" class="form-control">
|
||||
<option :value="null">-- none --</option>
|
||||
<option v-for="v in vendors" :key="v.vendorid" :value="v.vendorid">
|
||||
{{ v.vendor }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Printer Model <span class="hint">(links driver to a model so it shows on matching printers)</span></label>
|
||||
<select v-model="form.modelnumberid" class="form-control">
|
||||
@@ -96,13 +116,14 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { printersApi } from '@/api'
|
||||
import { printersApi, vendorsApi } from '@/api'
|
||||
import { useToast } from '@/composables/toast'
|
||||
import { apiError } from '@/utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const items = ref([])
|
||||
const models = ref([])
|
||||
const vendors = ref([])
|
||||
const showInactive = ref(false)
|
||||
const visibleItems = computed(() => showInactive.value ? items.value : items.value.filter(x => x.isactive !== false))
|
||||
const loading = ref(true)
|
||||
@@ -110,13 +131,13 @@ const showModal = ref(false)
|
||||
const editing = ref(null)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
const form = ref({ name: '', location: '', description: '', modelnumberid: null, isactive: true })
|
||||
const form = ref({ name: '', location: '', description: '', drivername: '', vendorid: null, modelnumberid: null, isactive: true })
|
||||
|
||||
function isHttp(loc) {
|
||||
return typeof loc === 'string' && /^https?:\/\//i.test(loc)
|
||||
}
|
||||
|
||||
onMounted(() => { loadData(); loadModels() })
|
||||
onMounted(() => { loadData(); loadModels(); loadVendors() })
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
@@ -139,11 +160,23 @@ async function loadModels() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVendors() {
|
||||
try {
|
||||
// A universal driver (HP UPD, Xerox GPD) attaches to the whole make rather
|
||||
// than one model, which is what vendorid is for. listAll, because the
|
||||
// picker must show every vendor - perpage is clamped at 100.
|
||||
const response = await vendorsApi.list({ perpage: 100 })
|
||||
vendors.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading vendors:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(item = null) {
|
||||
editing.value = item
|
||||
form.value = item
|
||||
? { name: item.name || '', location: item.location || '', description: item.description || '', modelnumberid: item.modelnumberid || null, isactive: item.isactive !== false }
|
||||
: { name: '', location: '', description: '', modelnumberid: null, isactive: true }
|
||||
? { name: item.name || '', location: item.location || '', description: item.description || '', drivername: item.drivername || '', vendorid: item.vendorid || null, modelnumberid: item.modelnumberid || null, isactive: item.isactive !== false }
|
||||
: { name: '', location: '', description: '', drivername: '', vendorid: null, modelnumberid: null, isactive: true }
|
||||
error.value = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
@@ -118,3 +118,60 @@ def test_a_row_that_names_a_vendor_is_not_matched_by_its_text(db, fleet):
|
||||
printer = Printer(vendorid=brother.vendorid, modelnumberid=None)
|
||||
printer.vendor = brother
|
||||
assert _printer_driver(printer, _universal()) is None
|
||||
|
||||
|
||||
# --- the two fields the converger actually needs -----------------------------
|
||||
#
|
||||
# drivername is the exact string the INF declares, and it is what
|
||||
# Add-PrinterDriver matches on - `name` is ours to choose and means nothing to
|
||||
# Windows. vendorid attaches a universal driver (HP UPD, Xerox GPD) to a whole
|
||||
# make. Neither was read from the request body, so both came back null on a
|
||||
# 201, and there was no interface that could set them: the shipped converger
|
||||
# had nothing to match and installed nothing, and driver-name drift could never
|
||||
# be detected because the expected value was never stored.
|
||||
|
||||
def test_a_driver_keeps_the_inf_name_and_vendor_it_was_given(client, db,
|
||||
auth_headers):
|
||||
from plugins.printers.models import PrinterDriver
|
||||
|
||||
created = client.post('/api/printers/drivers', headers=auth_headers, json={
|
||||
'name': 'HP universal',
|
||||
'location': r'\\server\share\hpupd',
|
||||
'drivername': 'HP Universal Printing PCL 6',
|
||||
'vendorid': 1,
|
||||
})
|
||||
assert created.status_code == 201, created.get_json()
|
||||
body = created.get_json()['data']
|
||||
assert body['drivername'] == 'HP Universal Printing PCL 6'
|
||||
assert body['vendorid'] == 1
|
||||
|
||||
stored = db.session.get(PrinterDriver, body['driverid'])
|
||||
assert stored.drivername == 'HP Universal Printing PCL 6'
|
||||
assert stored.vendorid == 1
|
||||
|
||||
|
||||
def test_the_inf_name_and_vendor_can_be_changed_afterwards(client, db,
|
||||
auth_headers):
|
||||
created = client.post('/api/printers/drivers', headers=auth_headers, json={
|
||||
'name': 'Xerox universal', 'location': r'\\server\share\xerox'})
|
||||
driverid = created.get_json()['data']['driverid']
|
||||
assert created.get_json()['data']['drivername'] is None
|
||||
|
||||
updated = client.put('/api/printers/drivers/%d' % driverid,
|
||||
headers=auth_headers,
|
||||
json={'drivername': 'Xerox Global Print Driver PCL6',
|
||||
'vendorid': 2})
|
||||
assert updated.status_code == 200, updated.get_json()
|
||||
assert updated.get_json()['data']['drivername'] == \
|
||||
'Xerox Global Print Driver PCL6'
|
||||
assert updated.get_json()['data']['vendorid'] == 2
|
||||
|
||||
|
||||
def test_a_blank_inf_name_clears_it_rather_than_storing_whitespace(
|
||||
client, db, auth_headers):
|
||||
"""An empty box means "not known", which the converger reads as "cannot
|
||||
install this". A whitespace string would look set and match nothing."""
|
||||
created = client.post('/api/printers/drivers', headers=auth_headers, json={
|
||||
'name': 'Blank test', 'location': r'\\server\share\blank',
|
||||
'drivername': ' '})
|
||||
assert created.get_json()['data']['drivername'] is None
|
||||
|
||||
Reference in New Issue
Block a user