displays: single display type with IP-driven role (dashboard/lobby/kiosk)
One 'display' image resolves what it shows from its own IP, like the existing
visitor-location BU mapping. Extend DashboardDefault with displayrole
(dashboard|lobby|partskiosk; migration 7d28, businessunitid now nullable since
only the dashboard role needs one) + a role->path map. New unauthenticated
GET /api/dashboarddefaults/display-role returns {role, path, businessunitid}
for the caller IP. Settings UI gains a Display selector, showing the business
unit only for the dashboard role.
This commit is contained in:
@@ -9,6 +9,7 @@ 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.utils.responses import success_response, error_response, ErrorCodes
|
||||
from shopdb.utils.authz import require_role
|
||||
|
||||
@@ -27,6 +28,7 @@ def _serialize(default):
|
||||
data = default.to_dict()
|
||||
data['businessunit'] = default.businessunit.businessunit \
|
||||
if default.businessunit else None
|
||||
data['displaypath'] = default.displaypath
|
||||
return data
|
||||
|
||||
|
||||
@@ -54,6 +56,35 @@ def visitor_location():
|
||||
})
|
||||
|
||||
|
||||
@dashboarddefaults_bp.route('/display-role', methods=['GET'])
|
||||
def display_role():
|
||||
"""Resolve what a single 'display' PC should show, from its own IP.
|
||||
|
||||
Unauthenticated: the display launcher calls this at boot. Returns the role
|
||||
(dashboard / lobby / partskiosk), the frontend path it maps to, and the
|
||||
business unit for the dashboard role. Unmapped IP -> null role.
|
||||
"""
|
||||
ipaddress = request.args.get('ipaddress') or _request_ip()
|
||||
default = DashboardDefault.query.filter_by(
|
||||
ipaddress=ipaddress, isactive=True).first()
|
||||
if not default:
|
||||
return success_response({
|
||||
'ipaddress': ipaddress,
|
||||
'role': None,
|
||||
'path': None,
|
||||
'businessunitid': None,
|
||||
'businessunit': None,
|
||||
})
|
||||
return success_response({
|
||||
'ipaddress': ipaddress,
|
||||
'role': default.displayrole,
|
||||
'path': default.displaypath,
|
||||
'businessunitid': default.businessunitid,
|
||||
'businessunit': default.businessunit.businessunit
|
||||
if default.businessunit else None,
|
||||
})
|
||||
|
||||
|
||||
@dashboarddefaults_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def list_defaults():
|
||||
@@ -70,20 +101,30 @@ def create_default():
|
||||
"""Create a visitor-IP -> business-unit mapping."""
|
||||
data = request.get_json() or {}
|
||||
ipaddress = (data.get('ipaddress') or '').strip()
|
||||
role = (data.get('displayrole') or 'dashboard').strip()
|
||||
businessunitid = data.get('businessunitid')
|
||||
|
||||
if not ipaddress:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'ipaddress is required')
|
||||
if not businessunitid:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'businessunitid is required')
|
||||
if not db.session.get(BusinessUnit, businessunitid):
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Business unit not found',
|
||||
http_code=404)
|
||||
if role not in DISPLAY_ROLES:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
f'displayrole must be one of {", ".join(DISPLAY_ROLES)}')
|
||||
# Only the dashboard role needs a business unit.
|
||||
if role == 'dashboard':
|
||||
if not businessunitid:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'businessunitid is required for the dashboard role')
|
||||
if not db.session.get(BusinessUnit, businessunitid):
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Business unit not found',
|
||||
http_code=404)
|
||||
else:
|
||||
businessunitid = None
|
||||
if DashboardDefault.query.filter_by(ipaddress=ipaddress, isactive=True).first():
|
||||
return error_response(ErrorCodes.CONFLICT,
|
||||
f"IP {ipaddress} is already mapped", http_code=409)
|
||||
|
||||
default = DashboardDefault(ipaddress=ipaddress, businessunitid=businessunitid,
|
||||
default = DashboardDefault(ipaddress=ipaddress, displayrole=role,
|
||||
businessunitid=businessunitid,
|
||||
description=data.get('description'))
|
||||
db.session.add(default)
|
||||
db.session.flush()
|
||||
@@ -104,8 +145,14 @@ def update_default(default_id):
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Mapping not found', http_code=404)
|
||||
|
||||
data = request.get_json() or {}
|
||||
if 'displayrole' in data:
|
||||
role = (data.get('displayrole') or '').strip()
|
||||
if role not in DISPLAY_ROLES:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
f'displayrole must be one of {", ".join(DISPLAY_ROLES)}')
|
||||
default.displayrole = role
|
||||
if 'businessunitid' in data:
|
||||
if not db.session.get(BusinessUnit, data['businessunitid']):
|
||||
if data['businessunitid'] and not db.session.get(BusinessUnit, data['businessunitid']):
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Business unit not found',
|
||||
http_code=404)
|
||||
default.businessunitid = data['businessunitid']
|
||||
@@ -114,6 +161,13 @@ def update_default(default_id):
|
||||
if 'description' in data:
|
||||
default.description = data['description']
|
||||
|
||||
# Non-dashboard roles carry no business unit; dashboard needs one.
|
||||
if default.displayrole != 'dashboard':
|
||||
default.businessunitid = None
|
||||
elif not default.businessunitid:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
'businessunitid is required for the dashboard role')
|
||||
|
||||
db.session.commit()
|
||||
return success_response(_serialize(default), message='Mapping updated')
|
||||
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
"""Dashboard default model: visitor-IP -> business-unit mapping.
|
||||
"""Dashboard default model: display-PC-IP -> display role (+ business unit).
|
||||
|
||||
A shopfloor / lobby kiosk display resolves which business unit to show by the
|
||||
display PC's IP address. Powers the visitor-location lookup the shopfloor and
|
||||
TV dashboards call when no explicit business unit is given.
|
||||
A single "display" PC image resolves what it should show from its own IP: the
|
||||
role (shopfloor dashboard, lobby slideshow, or 3D-parts kiosk) and, for the
|
||||
dashboard role, which business unit. Powers the visitor-location + display-role
|
||||
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_PATHS = {
|
||||
'dashboard': '/shopfloor',
|
||||
'lobby': '/tv',
|
||||
'partskiosk': '/parts-kiosk',
|
||||
}
|
||||
DISPLAY_ROLES = tuple(DISPLAY_ROLE_PATHS.keys())
|
||||
|
||||
|
||||
class DashboardDefault(BaseModel):
|
||||
"""Maps a display PC IP address to the business unit it should show."""
|
||||
"""Maps a display PC IP address to its display role (+ business unit)."""
|
||||
__tablename__ = 'dashboarddefaults'
|
||||
|
||||
dashboarddefaultid = db.Column(db.Integer, primary_key=True)
|
||||
ipaddress = db.Column(db.String(50), unique=True, nullable=False)
|
||||
# Which display the IP 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'),
|
||||
nullable=False
|
||||
nullable=True
|
||||
)
|
||||
description = db.Column(db.String(255))
|
||||
|
||||
businessunit = db.relationship('BusinessUnit')
|
||||
|
||||
@property
|
||||
def displaypath(self):
|
||||
return DISPLAY_ROLE_PATHS.get(self.displayrole)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DashboardDefault {self.ipaddress} -> {self.businessunitid}>"
|
||||
return f"<DashboardDefault {self.ipaddress} -> {self.displayrole}>"
|
||||
|
||||
Reference in New Issue
Block a user