Display role vocabulary, migration commit fix, stale enforcement reports
This commit is contained in:
@@ -9,7 +9,8 @@ from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import DashboardDefault, BusinessUnit, AuditLog
|
||||
from shopdb.core.models.dashboarddefault import DISPLAY_ROLES, DISPLAY_ROLE_PATHS
|
||||
from shopdb.core.models.dashboarddefault import (
|
||||
DISPLAY_ROLES, DISPLAY_ROLE_PATHS, normalize_display_role)
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
from shopdb.utils.authz import require_role
|
||||
|
||||
@@ -117,17 +118,19 @@ def create_default():
|
||||
data = request.get_json() or {}
|
||||
fqdn = (data.get('fqdn') or '').strip().lower() or None
|
||||
ipaddress = (data.get('ipaddress') or '').strip() or None
|
||||
role = (data.get('displayrole') or 'dashboard').strip()
|
||||
# Accept any casing and the retired spellings, store the canonical one, so a
|
||||
# caller matching the kiosk's own file is never turned away over a capital.
|
||||
role = normalize_display_role(data.get('displayrole') or 'Dashboard')
|
||||
businessunitid = data.get('businessunitid')
|
||||
|
||||
if not fqdn and not ipaddress:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'fqdn or ipaddress is required')
|
||||
if role not in DISPLAY_ROLES:
|
||||
if not role:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
f'displayrole must be one of {", ".join(DISPLAY_ROLES)}')
|
||||
# Only the dashboard role needs a business unit (location).
|
||||
if role == 'dashboard':
|
||||
# Only the Dashboard role needs a business unit (location).
|
||||
if role == 'Dashboard':
|
||||
if not businessunitid:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'a location is required for the dashboard role')
|
||||
@@ -167,8 +170,8 @@ def update_default(default_id):
|
||||
|
||||
data = request.get_json() or {}
|
||||
if 'displayrole' in data:
|
||||
role = (data.get('displayrole') or '').strip()
|
||||
if role not in DISPLAY_ROLES:
|
||||
role = normalize_display_role(data.get('displayrole'))
|
||||
if not role:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
f'displayrole must be one of {", ".join(DISPLAY_ROLES)}')
|
||||
default.displayrole = role
|
||||
@@ -187,8 +190,8 @@ def update_default(default_id):
|
||||
if not default.fqdn and not default.ipaddress:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'fqdn or ipaddress is required')
|
||||
# Non-dashboard roles carry no location; dashboard needs one.
|
||||
if default.displayrole != 'dashboard':
|
||||
# Non-Dashboard roles carry no location; Dashboard needs one.
|
||||
if normalize_display_role(default.displayrole) != 'Dashboard':
|
||||
default.businessunitid = None
|
||||
elif not default.businessunitid:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
|
||||
@@ -9,15 +9,46 @@ lookups the kiosks call at launch.
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
# Display role -> the frontend kiosk path it maps to. Kept here so the API and
|
||||
# any consumer resolve a role to a URL the same way.
|
||||
# Display role -> the frontend kiosk path it maps to. THE one place a role is
|
||||
# named, and the reason the names look like this: these are the literal values a
|
||||
# person types into C:\Enrollment\display-type.txt on the kiosk itself, which the
|
||||
# GE-Enforce dispatcher reads to pick a target. Two vocabularies used to exist -
|
||||
# core said 'partskiosk' where the kiosk said '3DPrintRoom' - so a display could
|
||||
# report a role core could not store and core could store a role no kiosk would
|
||||
# ever match. The machine's own file wins, because that is what a person edits.
|
||||
DISPLAY_ROLE_PATHS = {
|
||||
'dashboard': '/shopfloor',
|
||||
'lobby': '/tv',
|
||||
'partskiosk': '/parts-kiosk',
|
||||
'Dashboard': '/shopfloor',
|
||||
'Lobby': '/tv',
|
||||
'3DPrintRoom': '/parts-kiosk',
|
||||
}
|
||||
DISPLAY_ROLES = tuple(DISPLAY_ROLE_PATHS.keys())
|
||||
|
||||
# Retired spellings -> canonical. 'partskiosk' is what core called the 3D print
|
||||
# room before the vocabularies were merged; rows and API callers still carry it.
|
||||
LEGACY_DISPLAY_ROLES = {
|
||||
'partskiosk': '3DPrintRoom',
|
||||
}
|
||||
|
||||
|
||||
def normalize_display_role(value):
|
||||
"""Canonical role for any spelling, or None if it names no role.
|
||||
|
||||
Case-insensitive on purpose. The authoritative value is one line of a text
|
||||
file edited by hand on the kiosk, so 'lobby', 'Lobby' and 'LOBBY' all arrive
|
||||
in practice; the dispatcher already matches its own map case-insensitively
|
||||
(-ieq) and this keeps the server agreeing with it. Retired spellings map
|
||||
forward. Returns None rather than guessing, so an unrecognised value can be
|
||||
stored verbatim and SEEN rather than silently rewritten to a wrong role.
|
||||
"""
|
||||
text = (value or '').strip()
|
||||
if not text:
|
||||
return None
|
||||
lowered = text.lower()
|
||||
for role in DISPLAY_ROLES:
|
||||
if role.lower() == lowered:
|
||||
return role
|
||||
return LEGACY_DISPLAY_ROLES.get(lowered)
|
||||
|
||||
# GE device naming: a PC's DNS name is 'F' + its BIOS serial under the device
|
||||
# domain, e.g. FABC1234.device.geaerospace.net. The domain is a setting so other
|
||||
# sites can point elsewhere; the collector already reports the serial, so the
|
||||
@@ -52,8 +83,8 @@ class DashboardDefault(BaseModel):
|
||||
# not depend on innodb_large_prefix; FQDNs (F<serial>.<domain>) fit easily.
|
||||
fqdn = db.Column(db.String(191), unique=True, nullable=True, index=True)
|
||||
ipaddress = db.Column(db.String(50), unique=True, nullable=True)
|
||||
# Which display the mapping drives. Only the dashboard role uses businessunitid.
|
||||
displayrole = db.Column(db.String(20), nullable=False, default='dashboard')
|
||||
# Which display the mapping drives. Only the Dashboard role uses businessunitid.
|
||||
displayrole = db.Column(db.String(20), nullable=False, default='Dashboard')
|
||||
businessunitid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('businessunits.businessunitid'),
|
||||
@@ -65,7 +96,9 @@ class DashboardDefault(BaseModel):
|
||||
|
||||
@property
|
||||
def displaypath(self):
|
||||
return DISPLAY_ROLE_PATHS.get(self.displayrole)
|
||||
# Resolve through the normalizer so a legacy or oddly-cased stored value
|
||||
# still finds its path instead of silently returning None.
|
||||
return DISPLAY_ROLE_PATHS.get(normalize_display_role(self.displayrole))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DashboardDefault {self.fqdn or self.ipaddress} -> {self.displayrole}>"
|
||||
|
||||
Reference in New Issue
Block a user