Document external-DB schema contracts for employees + USB plugins
Every site's HR directory and USB check-in/out databases may use a different schema, so document exactly what each plugin queries and how to adapt. - plugins/employees/README.md: required employees table columns (SSO, First_Name, Last_Name, Team, Role, Picture), the queries run, photo handling, and a CREATE VIEW recipe to map a different site schema without code changes. - plugins/usb/README.md: cmmc_usb devices / checkinoutlog / users columns, read-write ops, the employee-directory dependency, and a view recipe. - USB plugin gains get_config_schema() (cmmc_usb_db_host/name/user + password); cmmc_usb_connection reads host/name/user settings-first (env fallback), the password stays env-only - matching the employees plugin. - Config-field help points at the READMEs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
94
plugins/employees/README.md
Normal file
94
plugins/employees/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Employees plugin - directory database contract
|
||||
|
||||
Read-only lookups against a **separate** employee/HR directory database. Powers:
|
||||
|
||||
- Employee search (add people to a notification, browse the directory)
|
||||
- Single + batch SSO lookup
|
||||
- Recognition notifications (name + photo on the shopfloor / lobby displays)
|
||||
|
||||
The plugin never writes to this database. Use a read-only account.
|
||||
|
||||
## Connection
|
||||
|
||||
Credentials resolve **settings-first, then environment**, except the password
|
||||
which is env-only (never stored in the app database).
|
||||
|
||||
| Field | Setting key (editable in the setup wizard) | Env var (fallback) |
|
||||
| -------- | ------------------------------------------ | ---------------------- |
|
||||
| Host | `employee_db_host` | `EMPLOYEE_DB_HOST` |
|
||||
| Database | `employee_db_name` | `EMPLOYEE_DB_NAME` |
|
||||
| User | `employee_db_user` | `EMPLOYEE_DB_USER` |
|
||||
| Password | (not stored) | `EMPLOYEE_DB_PASSWORD` |
|
||||
|
||||
Set the password in `.env`; the setup wizard's Features step shows the exact
|
||||
line to paste. Engine is MySQL/MariaDB (pymysql).
|
||||
|
||||
## Required schema
|
||||
|
||||
The plugin runs these queries verbatim, so a site's database must expose a
|
||||
table (or **view** - see below) named `employees` with these columns:
|
||||
|
||||
| Column | Type | Notes |
|
||||
| ------------ | ------------------- | ------------------------------------------------- |
|
||||
| `SSO` | INT | Unique person id. Lookups require it to be numeric |
|
||||
| `First_Name` | VARCHAR | Displayed as the given name |
|
||||
| `Last_Name` | VARCHAR | Displayed as the surname; sort key |
|
||||
| `Team` | VARCHAR | Team / group label |
|
||||
| `Role` | VARCHAR | Job title / role |
|
||||
| `Picture` | VARCHAR | **Image filename** (see Photos) |
|
||||
|
||||
Queries actually executed:
|
||||
|
||||
```sql
|
||||
-- search
|
||||
SELECT SSO, First_Name, Last_Name, Team, Role, Picture
|
||||
FROM employees
|
||||
WHERE First_Name LIKE %s OR Last_Name LIKE %s OR CAST(SSO AS CHAR) LIKE %s
|
||||
ORDER BY Last_Name, First_Name LIMIT %s;
|
||||
|
||||
-- single / batch
|
||||
SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO = %s;
|
||||
SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO IN (...);
|
||||
```
|
||||
|
||||
Rows are returned to the API **with these exact column names**. The frontend
|
||||
(EmployeeSearch, NotificationForm, EmployeeDetail, ShopfloorDashboard) reads
|
||||
`SSO`, `First_Name`, `Last_Name`, `Team`, `Role`, `Picture` as-is - do not
|
||||
rename them in the response.
|
||||
|
||||
## Photos
|
||||
|
||||
`Picture` holds an **image filename** (e.g. `123456.jpg`), not a path or blob.
|
||||
The app renders it as `/static/employees/<Picture>`, so the image files must
|
||||
live in the app's `static/employees/` directory. Leave `Picture` empty/NULL for
|
||||
people with no photo; the UI falls back to initials.
|
||||
|
||||
## Adapting a different site schema (recommended: a view)
|
||||
|
||||
Sites whose HR/directory database uses different table or column names should
|
||||
**not** be forced to rename anything. Instead, create a read-only **view**
|
||||
named `employees` that maps local columns to the names above:
|
||||
|
||||
```sql
|
||||
CREATE VIEW employees AS
|
||||
SELECT
|
||||
person_id AS SSO,
|
||||
given_name AS First_Name,
|
||||
surname AS Last_Name,
|
||||
department AS Team,
|
||||
job_title AS Role,
|
||||
photo_filename AS Picture
|
||||
FROM hr_people
|
||||
WHERE active = 1;
|
||||
```
|
||||
|
||||
Grant the app's read-only user `SELECT` on the view. No app code changes -
|
||||
point `employee_db_*` at that database and the plugin works.
|
||||
|
||||
Notes:
|
||||
- `SSO` must be numeric (single/batch lookup validate `isdigit()`).
|
||||
- Column-name case follows your database's identifier casing; match the names
|
||||
above exactly on case-sensitive platforms.
|
||||
- If the directory is unreachable or the `employees` object is missing, lookups
|
||||
return a 500 and the rest of the app keeps working (the feature degrades, it
|
||||
does not crash the app).
|
||||
@@ -63,7 +63,10 @@ class EmployeesPlugin(BasePlugin):
|
||||
wizard can edit; the password stays in .env (emitted, not stored)."""
|
||||
return [
|
||||
{'key': 'employee_db_host', 'label': 'Employee DB host', 'type': 'text',
|
||||
'secret': False, 'default': 'localhost'},
|
||||
'secret': False, 'default': 'localhost',
|
||||
'help': 'This DB must expose an "employees" table or view with columns '
|
||||
'SSO, First_Name, Last_Name, Team, Role, Picture. '
|
||||
'See plugins/employees/README.md.'},
|
||||
{'key': 'employee_db_name', 'label': 'Employee DB name', 'type': 'text',
|
||||
'secret': False, 'default': 'wjf_employees'},
|
||||
{'key': 'employee_db_user', 'label': 'Employee DB user', 'type': 'text',
|
||||
|
||||
98
plugins/usb/README.md
Normal file
98
plugins/usb/README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# USB plugin - CMMC USB check-in/out database contract
|
||||
|
||||
The USB plugin tracks removable-media check-in/out for CMMC compliance. Device
|
||||
and log state live in a **separate, read-write** MySQL database (`cmmc_usb`),
|
||||
reached with parameterized pymysql via `cmmc_usb_connection()`. Names of people
|
||||
are resolved from the HR employee directory (see the employees plugin).
|
||||
|
||||
The plugin's own reference tables (`usbdevicetypes`, `usbdevices`,
|
||||
`usbcheckouts`) live in the main app database; only the live check-in/out data
|
||||
is in `cmmc_usb`.
|
||||
|
||||
## Connection
|
||||
|
||||
Credentials resolve **settings-first, then environment**, except the password
|
||||
which is env-only (never stored in the app database).
|
||||
|
||||
| Field | Setting key (editable in the setup wizard) | Env var (fallback) |
|
||||
| -------- | ------------------------------------------ | ----------------------- |
|
||||
| Host | `cmmc_usb_db_host` | `CMMC_USB_DB_HOST` |
|
||||
| Database | `cmmc_usb_db_name` | `CMMC_USB_DB_NAME` |
|
||||
| User | `cmmc_usb_db_user` | `CMMC_USB_DB_USER` |
|
||||
| Password | (not stored) | `CMMC_USB_DB_PASSWORD` |
|
||||
|
||||
Set the password in `.env`; the setup wizard's Features step shows the exact
|
||||
line to paste. This database is **read-write** - the app account needs
|
||||
`SELECT, INSERT, UPDATE`. Engine is MySQL/MariaDB (pymysql).
|
||||
|
||||
## Required schema
|
||||
|
||||
The plugin runs raw SQL against these tables (or **views** - see below).
|
||||
|
||||
### `devices`
|
||||
| Column | Type | Notes |
|
||||
| ----------------- | ------- | -------------------------------------- |
|
||||
| `device_id` | VARCHAR | Primary key; the device serial/tag |
|
||||
| `device_desc` | VARCHAR | Description |
|
||||
| `device_owner` | VARCHAR | Owner |
|
||||
| `status` | VARCHAR | Check-in/out state |
|
||||
| `locker_location` | VARCHAR | Where the device is stored |
|
||||
|
||||
Operations: `SELECT` (list + by id), `INSERT` (register device), `UPDATE`
|
||||
(edit fields, change status).
|
||||
|
||||
### `checkinoutlog`
|
||||
| Column | Type | Notes |
|
||||
| ----------------- | --------- | ---------------------------------- |
|
||||
| `log_id` | INT (PK) | Auto id |
|
||||
| `badge_number` | VARCHAR | Person's badge |
|
||||
| `device_id` | VARCHAR | FK to `devices.device_id` |
|
||||
| `action` | VARCHAR | check-in / check-out |
|
||||
| `timestamp` | DATETIME | When it happened |
|
||||
| `scanned_viruses` | (text/int)| Scan result |
|
||||
| `locker_location` | VARCHAR | Locker at time of event |
|
||||
| `sanitized` | (bool/int)| Sanitization flag |
|
||||
|
||||
Operations: `SELECT` (history per device), `INSERT` (log an event).
|
||||
|
||||
### `users`
|
||||
| Column | Type | Notes |
|
||||
| -------------- | ------- | ------------------------------------- |
|
||||
| `badge_number` | VARCHAR | Primary key; the scanned badge |
|
||||
| `first_name` | VARCHAR | Given name |
|
||||
| `last_name` | VARCHAR | Surname |
|
||||
|
||||
Operations: `SELECT` by badge, `INSERT` (auto-add a badge on first scan).
|
||||
|
||||
## Employee directory dependency
|
||||
|
||||
To turn a scanned badge into a name, the plugin also reads the HR `employees`
|
||||
directory (via the employees plugin's connection). A badge shaped `0<digits>BZ`
|
||||
carries a PayNo (the digits); lookups try `employees.SSO` and `employees.PayNo`.
|
||||
See `plugins/employees/README.md` for that schema and connection.
|
||||
|
||||
## Adapting a different site schema (recommended: views)
|
||||
|
||||
Sites whose USB-tracking database uses different table/column names should
|
||||
create read-only/updatable **views** named `devices`, `checkinoutlog`, and
|
||||
`users` that map local columns to the names above. Example:
|
||||
|
||||
```sql
|
||||
CREATE VIEW devices AS
|
||||
SELECT
|
||||
asset_tag AS device_id,
|
||||
description AS device_desc,
|
||||
owner AS device_owner,
|
||||
state AS status,
|
||||
storage_bay AS locker_location
|
||||
FROM usb_assets;
|
||||
```
|
||||
|
||||
Because the plugin writes to `devices`/`checkinoutlog`/`users`, either make the
|
||||
views updatable (single-table views usually are) or expose real tables with
|
||||
these column names. Grant the app account `SELECT, INSERT, UPDATE`.
|
||||
|
||||
Notes:
|
||||
- `device_id` and `badge_number` are the natural keys the plugin matches on.
|
||||
- If `cmmc_usb` is unreachable, USB endpoints return an error and the rest of
|
||||
the app keeps working (the feature degrades, it does not crash the app).
|
||||
@@ -55,7 +55,24 @@ class USBPlugin(BasePlugin):
|
||||
def get_models(self) -> List[Type]:
|
||||
"""Return list of SQLAlchemy model classes."""
|
||||
return [USBDeviceType, USBDevice, USBCheckout]
|
||||
|
||||
|
||||
def get_config_schema(self) -> List[Dict]:
|
||||
"""CMMC USB check-in/out database connection. Host/name/user are settings
|
||||
the wizard can edit; the password stays in .env (emitted, not stored)."""
|
||||
return [
|
||||
{'key': 'cmmc_usb_db_host', 'label': 'USB DB host', 'type': 'text',
|
||||
'secret': False, 'default': 'localhost',
|
||||
'help': 'This DB must expose devices / checkinoutlog / users tables '
|
||||
'(or views). See plugins/usb/README.md.'},
|
||||
{'key': 'cmmc_usb_db_name', 'label': 'USB DB name', 'type': 'text',
|
||||
'secret': False, 'default': 'cmmc_usb'},
|
||||
{'key': 'cmmc_usb_db_user', 'label': 'USB DB user', 'type': 'text',
|
||||
'secret': False},
|
||||
{'key': 'cmmc_usb_db_password', 'label': 'USB DB password', 'type': 'password',
|
||||
'secret': True, 'envvar': 'CMMC_USB_DB_PASSWORD',
|
||||
'help': 'Stored in .env, not the database. The wizard shows the line to paste.'},
|
||||
]
|
||||
|
||||
def init_app(self, app: Flask, db_instance) -> None:
|
||||
"""Initialize plugin with Flask app."""
|
||||
logger.info(f"USB plugin initialized (v{self.meta.version})")
|
||||
|
||||
@@ -11,12 +11,28 @@ import pymysql
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def _setting_or_config(setting_key, config_key):
|
||||
"""Non-secret config: a saved Setting wins, else the env-backed app config.
|
||||
|
||||
Lets the setup wizard edit host/name/user without touching .env, while the
|
||||
password stays env-only.
|
||||
"""
|
||||
from shopdb.core.models import Setting
|
||||
try:
|
||||
row = Setting.query.filter_by(key=setting_key).first()
|
||||
if row and row.value:
|
||||
return row.value
|
||||
except Exception:
|
||||
pass
|
||||
return current_app.config[config_key]
|
||||
|
||||
|
||||
def cmmc_usb_connection():
|
||||
"""Open a pymysql connection to the cmmc_usb DB."""
|
||||
return pymysql.connect(
|
||||
host=current_app.config['CMMC_USB_DB_HOST'],
|
||||
user=current_app.config['CMMC_USB_DB_USER'],
|
||||
host=_setting_or_config('cmmc_usb_db_host', 'CMMC_USB_DB_HOST'),
|
||||
user=_setting_or_config('cmmc_usb_db_user', 'CMMC_USB_DB_USER'),
|
||||
password=current_app.config['CMMC_USB_DB_PASSWORD'],
|
||||
database=current_app.config['CMMC_USB_DB_NAME'],
|
||||
database=_setting_or_config('cmmc_usb_db_name', 'CMMC_USB_DB_NAME'),
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user