printedparts: badge resolution honors the employee directory mode
The resolver only read the self-hosted directory table, which is empty at sites running the external HR directory - every kiosk badge fell to the deny policy. It now branches on employee_directory_mode like the usb plugin: selfhosted looks up DirectoryEmployee by SSO; external queries the HR directory via employee_connection, resolving PayNo badges by their real PayNo column and recovering the employee's SSO.
This commit is contained in:
@@ -385,6 +385,18 @@ cannot carry a JWT header), and the reports (product-wide jwt-optional
|
|||||||
convention). Grant `printedparts.view` to the roles that should see the
|
convention). Grant `printedparts.view` to the roles that should see the
|
||||||
catalog - admins bypass as always.
|
catalog - admins bypass as always.
|
||||||
|
|
||||||
|
## Stage 16b (extension) - badge resolution honors the directory mode
|
||||||
|
|
||||||
|
Prod runs the employee directory in EXTERNAL mode (live HR database), where
|
||||||
|
the self-hosted directoryemployees table is empty - so the original
|
||||||
|
resolver's every lookup missed and the deny policy blocked the kiosk. The
|
||||||
|
resolver now branches on the same employee_directory_mode setting the
|
||||||
|
usb/employees plugins use: selfhosted reads DirectoryEmployee by SSO;
|
||||||
|
external queries the HR directory via employee_connection() - and resolves
|
||||||
|
PayNo badges by their actual PayNo column, recovering the real SSO, which
|
||||||
|
the self-hosted table cannot do. Dual-backend lesson in miniature: a plugin
|
||||||
|
that resolves people must honor the site's directory mode.
|
||||||
|
|
||||||
## Stage 16 (extension) - kiosk touch fixes from first hands-on use
|
## Stage 16 (extension) - kiosk touch fixes from first hands-on use
|
||||||
|
|
||||||
First real touchscreen session found two problems worth their own stage:
|
First real touchscreen session found two problems worth their own stage:
|
||||||
|
|||||||
@@ -8,18 +8,25 @@ cross-plugin imports break the shopdb.api-only contract):
|
|||||||
scanners emit this shape)
|
scanners emit this shape)
|
||||||
- anything else -> unresolvable
|
- anything else -> unresolvable
|
||||||
|
|
||||||
Names come from the employees plugin's self-hosted directory, looked up by
|
Name lookup honors the site's employee directory mode (the same setting the
|
||||||
SSO. The directory carries no PayNo column, so PayNo badges resolve only when
|
employees/usb plugins use):
|
||||||
the wrapped digits are themselves the SSO (true at sites whose badges encode
|
|
||||||
the SSO); otherwise they fall to the unknown-badge policy.
|
- selfhosted: the employees plugin's directoryemployees table, by SSO. The
|
||||||
|
table has no PayNo column, so a PayNo badge only resolves when the wrapped
|
||||||
|
digits are themselves the SSO.
|
||||||
|
- external: the HR directory via employee_connection() - SSO badges by SSO,
|
||||||
|
PayNo badges by PayNo (which also recovers the real SSO to record).
|
||||||
|
|
||||||
Policy (Setting printedparts_unknown_badge): 'deny' (default) rejects a badge
|
Policy (Setting printedparts_unknown_badge): 'deny' (default) rejects a badge
|
||||||
with no directory match; 'allow' records the SSO with an empty name.
|
with no directory match; 'allow' records the SSO with an empty name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from shopdb.api import Setting
|
from shopdb.api import Setting, employee_connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_PAYNO_BADGE = re.compile(r'^0(\d+)BZ$', re.IGNORECASE)
|
_PAYNO_BADGE = re.compile(r'^0(\d+)BZ$', re.IGNORECASE)
|
||||||
|
|
||||||
@@ -28,17 +35,59 @@ class BadgeError(ValueError):
|
|||||||
"""Raised when a badge cannot be accepted under the site policy."""
|
"""Raised when a badge cannot be accepted under the site policy."""
|
||||||
|
|
||||||
|
|
||||||
def _directory_name(sso):
|
def _parse_badge(badge):
|
||||||
"""Best-effort display name from the employees plugin directory."""
|
"""Return ('sso'|'payno', digits) or raise BadgeError on unknown shape."""
|
||||||
|
badge = (badge or '').strip()
|
||||||
|
if not badge:
|
||||||
|
raise BadgeError('Scan or enter a badge')
|
||||||
|
if badge.isdigit():
|
||||||
|
return 'sso', badge
|
||||||
|
match = _PAYNO_BADGE.match(badge)
|
||||||
|
if match:
|
||||||
|
return 'payno', match.group(1)
|
||||||
|
raise BadgeError('Unrecognized badge format')
|
||||||
|
|
||||||
|
|
||||||
|
def _selfhosted_lookup(digits):
|
||||||
|
"""(sso, name) from the employees plugin directory, or None."""
|
||||||
try:
|
try:
|
||||||
from plugins.employees.models import DirectoryEmployee
|
from plugins.employees.models import DirectoryEmployee
|
||||||
from shopdb.api import db
|
from shopdb.api import db
|
||||||
if sso and str(sso).isdigit():
|
employee = db.session.get(DirectoryEmployee, int(digits))
|
||||||
employee = db.session.get(DirectoryEmployee, int(sso))
|
if employee:
|
||||||
if employee:
|
return digits, f'{employee.firstname} {employee.lastname}'.strip()
|
||||||
return f'{employee.firstname} {employee.lastname}'.strip()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
logger.exception('Self-hosted directory lookup failed for %s', digits)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _external_lookup(kind, digits):
|
||||||
|
"""(sso, name) from the HR directory, or None. PayNo badges resolve to
|
||||||
|
the employee's real SSO."""
|
||||||
|
try:
|
||||||
|
conn = employee_connection()
|
||||||
|
except Exception:
|
||||||
|
logger.exception('HR directory connection failed')
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
column = 'SSO' if kind == 'sso' else 'PayNo'
|
||||||
|
cursor.execute(
|
||||||
|
f'SELECT SSO, First_Name, Last_Name FROM employees '
|
||||||
|
f'WHERE {column} = %s', (digits,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
sso = str(row[0] if not isinstance(row, dict) else row['SSO'])
|
||||||
|
first = row[1] if not isinstance(row, dict) else row['First_Name']
|
||||||
|
last = row[2] if not isinstance(row, dict) else row['Last_Name']
|
||||||
|
return sso, f'{(first or "").strip()} {(last or "").strip()}'.strip()
|
||||||
|
except Exception:
|
||||||
|
logger.exception('HR directory lookup failed for %s %s', kind, digits)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -48,22 +97,17 @@ def resolve_badge(badge):
|
|||||||
Raises BadgeError with a kiosk-displayable message when the badge shape is
|
Raises BadgeError with a kiosk-displayable message when the badge shape is
|
||||||
unrecognized or the policy denies an unmatched badge.
|
unrecognized or the policy denies an unmatched badge.
|
||||||
"""
|
"""
|
||||||
badge = (badge or '').strip()
|
kind, digits = _parse_badge(badge)
|
||||||
if not badge:
|
|
||||||
raise BadgeError('Scan or enter a badge')
|
|
||||||
|
|
||||||
if badge.isdigit():
|
mode = (Setting.get('employee_directory_mode') or 'selfhosted').lower()
|
||||||
sso = badge
|
if mode == 'external':
|
||||||
|
resolved = _external_lookup(kind, digits)
|
||||||
else:
|
else:
|
||||||
match = _PAYNO_BADGE.match(badge)
|
resolved = _selfhosted_lookup(digits)
|
||||||
if not match:
|
|
||||||
raise BadgeError('Unrecognized badge format')
|
|
||||||
sso = match.group(1)
|
|
||||||
|
|
||||||
name = _directory_name(sso)
|
if resolved is None:
|
||||||
if name is None:
|
|
||||||
policy = (Setting.get('printedparts_unknown_badge') or 'deny').lower()
|
policy = (Setting.get('printedparts_unknown_badge') or 'deny').lower()
|
||||||
if policy != 'allow':
|
if policy != 'allow':
|
||||||
raise BadgeError('Badge not recognized - see the parts team')
|
raise BadgeError('Badge not recognized - see the parts team')
|
||||||
return sso, ''
|
return digits, ''
|
||||||
return sso, name
|
return resolved
|
||||||
|
|||||||
Reference in New Issue
Block a user