State that this is a bundled plugin whose frontend and three small core edits land in this repo, list the three deliberate divergences from the scaffold before the learner hits them, suggest a per-milestone solution branch for instructors, and point out the earliest visible win (wire the bare list page as soon as the GET endpoint works).
238 lines
12 KiB
Markdown
238 lines
12 KiB
Markdown
# Plugin lab: build the printedparts plugin yourself
|
|
|
|
A guided, milestone-based exercise: build the 3D-printed-parts storefront +
|
|
kiosk plugin specified in `docs/proposals/printedparts-plugin.md`. Each
|
|
milestone lists what to build, which existing code to imitate, and a
|
|
checkpoint that proves you are done. Read the spec first, keep it open.
|
|
|
|
Prerequisites: a working dev environment (README quick start), the three
|
|
plugin docs skimmed once - `PLUGIN-QUICKSTART.md` (mechanics),
|
|
`PLUGIN-GUIDE.md` (the measuringtools walkthrough - your narrative reference),
|
|
`PLUGIN-HOOKS.md` (hook reference). Naming rules: `CONTRIBUTING.md` - the
|
|
pre-commit hook enforces them, read it before naming anything.
|
|
|
|
Ground rules
|
|
- Import core ONLY via `shopdb.api` (+ `shopdb.plugins.base`). The contract
|
|
test fails your build otherwise.
|
|
- DB columns: lowercase concatenated (`quantityonhand`, not quantity_on_hand).
|
|
- Run `bash scripts/check-naming-and-style.sh` and the test suite at every
|
|
checkpoint.
|
|
- Commit once per milestone. Working on a branch is fine; so is a fork.
|
|
|
|
Know before you start
|
|
- You are building a BUNDLED plugin inside this repo. Plugin frontend files
|
|
live in core (`frontend/src/...`), and three core files get small edits:
|
|
`frontend/src/api/index.js` (api client), the router, and
|
|
`PLUGIN_TABLE_OWNERS` in `shopdb/plugins/alembic_template.py`. That is
|
|
normal for all 12 bundled plugins - external-plugin UI packaging does not
|
|
exist yet - so do not be confused when a "plugin" touches core.
|
|
- This plugin deliberately DIVERGES from the scaffold in three places, each
|
|
a teaching point you will hit in order: (1) it is NOT an asset type, so the
|
|
scaffold's AssetType seeding gets deleted (M1); (2) its migration is a REAL
|
|
baseline that creates tables, not a stamp-only anchor (M1); (3) its kiosk
|
|
take endpoint is the product's first UNauthenticated write - read the
|
|
decision record in the proposal before building it (M4).
|
|
- Instructor option: keep a solution branch with one commit per milestone
|
|
(tag `lab-m1`..`lab-m7`); a stuck learner can `git diff lab-m3 lab-m4` to
|
|
see exactly what a milestone changes.
|
|
|
|
---
|
|
|
|
## Milestone 1 - skeleton, models, migration (backend exists)
|
|
|
|
Build
|
|
1. `flask plugin new printedparts` - scaffolds `plugins/printedparts/`.
|
|
2. The scaffold assumes an Asset-extension plugin; ours is standalone.
|
|
In `plugin.py` strip the AssetType seeding from `on_install` (imitate
|
|
`plugins/knowledgebase/plugin.py` instead of the template).
|
|
3. Replace the scaffold model with the two spec tables: `PrintedItem`,
|
|
`PrintedItemTransaction` (`models/printeditem.py`). Use `BaseModel` +
|
|
`AuditMixin` from `shopdb.api`. Itemcode: leave generation to the API
|
|
layer (M2), column just `unique=True, index=True`.
|
|
4. Register both tables in `PLUGIN_TABLE_OWNERS`
|
|
(`shopdb/plugins/alembic_template.py`).
|
|
5. Create `plugins/printedparts/migrations/` with the 3-line `env.py` +
|
|
`script.py.mako` (copy from measuringtools) and a REAL baseline
|
|
`versions/0001_printedparts_baseline.py` - hand-written
|
|
`op.create_table(...)` for both tables (see
|
|
`plugins/measuringtools/migrations/versions/0001_measuringtools_baseline.py`).
|
|
6. Manifest: api_prefix `/api/printedparts`, `dependencies: ["employees"]`,
|
|
`default_enabled: false`.
|
|
|
|
Checkpoint
|
|
```
|
|
flask plugin install printedparts && flask plugin enable printedparts
|
|
flask plugin upgrade-all # applies your 0001
|
|
mysql> SHOW TABLES LIKE 'printed%'; -- both tables
|
|
mysql> SELECT * FROM alembic_version_printedparts; -- your revision id
|
|
pytest tests/ -q # nothing broken, contract tests green
|
|
```
|
|
|
|
## Milestone 2 - CRUD API + permissions + itemcode
|
|
|
|
Build
|
|
1. `api/routes.py`: list/detail/create/update/soft-delete per the spec table.
|
|
Imitate a clean plugin blueprint (`plugins/measuringtools/api/routes.py`)
|
|
for pagination (`perpage`, `dir`), search, and the shared
|
|
`success_response`/`error_response` helpers from `shopdb.api`.
|
|
2. Itemcode on create: `<prefix>-<id zero-padded to 4>`; prefix from Setting
|
|
`printedparts_code_prefix` (read via `self.get_setting` or Setting model
|
|
through `shopdb.api`). Two-step: insert, flush to get the id, set code.
|
|
3. `get_permissions()` on the plugin class: view/create/edit/delete/restock
|
|
(tuples, category `printedparts` - copy shape from
|
|
`plugins/usb/plugin.py`). Gate mutations with `@jwt_required()` +
|
|
`@require_permission(...)`; reads are `@jwt_required(optional=True)`.
|
|
4. Restock + adjust endpoints: both write a LEDGER row and move
|
|
`quantityonhand` in the same commit. Adjust requires `reason`, rejects a
|
|
result below zero. Both record the operator: accept `badge` in the body
|
|
and resolve it (M4 extracts the resolver - for now digits=SSO is enough).
|
|
5. Seed the 3 settings in `on_install` (Setting.set pattern - see how other
|
|
plugins seed in `on_install`; category `printedparts`).
|
|
|
|
Tip - earlier visible win: as soon as the GET list endpoint works, jump ahead
|
|
and wire just the api client + router entry + a bare `PrintedItemsList.vue`
|
|
(first two steps of M3), seed two rows by hand, and look at your parts in the
|
|
browser. Everything before that moment is invisible; seeing the table makes
|
|
the rest of the lab concrete. Then come back and finish the mutations here.
|
|
|
|
Checkpoint
|
|
```
|
|
pytest plugins/printedparts/tests/ -q # write tests as you go:
|
|
# - create mints 3DP-0001 style codes
|
|
# - restock/adjust move both ledger and cache atomically
|
|
# - adjust below zero -> 400/422
|
|
# - permission gates: anonymous create -> 401, wrong-perm user -> 403
|
|
curl -s localhost:5001/api/printedparts/items | jq # anonymous list OK
|
|
```
|
|
|
|
## Milestone 3 - management frontend + images
|
|
|
|
Build
|
|
1. The scaffold already dropped `PrintedpartsList/Detail/Form.vue` starters
|
|
and a router file; rename/build them into `PrintedItemsList/Detail/Form`
|
|
per the spec. Master templates: `PrintersList.vue` (list),
|
|
`PrinterDetail.vue` (detail). Global CSS only; CSS variables for colors
|
|
(frontend/CLAUDE.md rules).
|
|
2. Register the API client in `frontend/src/api/index.js` (paste the
|
|
generated `frontend-api-snippet.js`, extend with restock/adjust/image
|
|
calls).
|
|
3. Low-stock highlighting on the list (`quantityonhand <= lowstockthreshold`
|
|
-> danger badge). Filters: search + low-stock-only checkbox.
|
|
4. Image upload: replicate the models-image trio - upload/serve/delete -
|
|
from `shopdb/core/api/models.py` INTO the plugin blueprint
|
|
(`instance/printedpartsimages/`, public GET serve, imageurl column,
|
|
prefix-guarded delete). Wire the Form upload + Detail hero image.
|
|
5. Nav: `get_navigation_items()` -> "3D Parts" (usb plugin shape). Router
|
|
meta: list/detail plugin-gated, new/edit `requiresAuth` (see
|
|
`frontend/src/router/routes/usb.js`).
|
|
|
|
Checkpoint: create an item with a photo in the UI; thumbnail on list, hero on
|
|
detail; restock from detail updates qty + shows in history; frontend build +
|
|
`npx vitest run` green; naming hook green.
|
|
|
|
## Milestone 4 - badge resolution + kiosk
|
|
|
|
Build
|
|
1. Badge resolver in the plugin (`services/badges.py`): copy the USB contract
|
|
- all-digits -> SSO; `^0(\d+)BZ$` case-insensitive -> PayNo; resolve
|
|
display name via the employees plugin directory the way
|
|
`plugins/usb/api/selfhosted.py::_resolve_name` does (lazy import inside
|
|
the function, degrade gracefully when the plugin is absent). Policy
|
|
setting `printedparts_unknown_badge` (deny -> 422).
|
|
2. Kiosk endpoints (UNauthenticated - the notifications/employees open-read
|
|
precedent): `GET /kiosk/item/<itemcode>` and `POST /kiosk/take`
|
|
{itemcode, badge, quantity}. Take: validate active item, 1 <= qty <=
|
|
onhand, resolve badge, single-commit ledger row + decrement. Clear error
|
|
strings - the kiosk displays them verbatim.
|
|
3. Kiosk view `/parts-kiosk`: top-level route, NO requiresAuth, outside
|
|
AppLayout (register beside `/shopfloor` in `frontend/src/router/index.js`).
|
|
Three-step flow per the spec. The scanner is a keyboard wedge: hidden
|
|
always-focused input, submit on Enter, route the scan to whichever step is
|
|
active. Build `TouchKeypad.vue` (3x4 grid of big buttons, emits digits/
|
|
clear/backspace) - net-new, nothing to copy, keep it dumb.
|
|
4. Manual fallback path (typed item search + badge entry) behind a small
|
|
"no scanner?" link.
|
|
|
|
Checkpoint: full kiosk walkthrough on a touchscreen (or browser): scan/type
|
|
an itemcode -> item card; badge `0123456BZ` and plain SSO both resolve; take 3
|
|
-> success screen, qty down 3, ledger row has your name; taking more than
|
|
onhand -> friendly error; unknown badge -> denied message. Backend tests for
|
|
the resolver shapes + take validation.
|
|
|
|
## Milestone 5 - labels (1in x 0.5in)
|
|
|
|
Build
|
|
1. Public print route `/print/printedparts-labels` + view (imitate
|
|
`USBLabelBatch.vue` - USB is the precedent for a plugin OWNING its label
|
|
page instead of joining TYPE_CONFIG).
|
|
2. New stock size: `@page { size: 1in 0.5in; margin: 0 }`, one label per page
|
|
(roll-fed label printers treat each page as one label). Layout: CODE128
|
|
via JsBarcode (~0.9in wide, displayValue false), itemcode text ~7pt under
|
|
it, optional truncated name. Offer QR as a variant but default barcode.
|
|
3. Batch: multi-select items -> sequence of labels; plus a ULINE mini-grid
|
|
sheet fallback (mini72 pattern in `AssetLabelBatch.vue`).
|
|
4. Print buttons on Detail (single) and List (batch selected).
|
|
|
|
Checkpoint: print preview shows one 1x0.5 label per page; a printed (or
|
|
PDF-zoomed) barcode scans back into the kiosk and pulls up the right item.
|
|
That round trip - label printed from the catalog, scanned at the kiosk,
|
|
stock decremented with your name on it - is the demo moment; make it work
|
|
end to end before polishing.
|
|
|
|
## Milestone 6 - metrics, reports, widget
|
|
|
|
Build
|
|
1. `get_reports()` -> stock, consumption (date range), by-person; endpoints
|
|
in the plugin blueprint, `@jwt_required(optional=True)`, `?format=csv` via
|
|
the `generate_csv` helper pattern (`shopdb/core/api/reports.py` shows the
|
|
shape; a plugin report lives in the plugin and is merged into
|
|
`GET /api/reports` automatically when enabled).
|
|
2. Stock report includes the reconcile check: flag rows where cached
|
|
`quantityonhand` != SUM(ledger). Should always be empty; if not, you have
|
|
a non-atomic write path - find it.
|
|
3. OPTIONAL/deferred: `get_dashboard_widgets()` -> low-stock count. Caveat:
|
|
this hook predates the ADR-010 data-only renderers - the widget names a
|
|
frontend component that must already exist in core, so a plugin widget
|
|
only renders if you also add that component. Reports are the primary
|
|
monitoring surface; skip the widget unless you want the extra credit.
|
|
4. Nice-to-have if time: burn rate (avg weekly takes over trailing 4 weeks +
|
|
weeks-to-empty). Plain SQL over the ledger.
|
|
|
|
Checkpoint: reports appear on /reports grouped under the plugin, CSV
|
|
downloads; widget renders on the dashboard; reconcile column all-clear after
|
|
a kiosk session.
|
|
|
|
## Milestone 7 - lifecycle + closeout
|
|
|
|
Build/verify
|
|
1. Disable/enable cycle: `flask plugin disable printedparts` - nav entry,
|
|
routes, reports, and grantable permissions all disappear; enable restores.
|
|
2. Fresh-database proof: point DATABASE_URL at a scratch DB, `flask db
|
|
upgrade` + `flask plugin install/enable/upgrade-all` - everything works
|
|
with zero manual SQL.
|
|
3. Full suite: backend pytest, vitest, frontend build, naming hook.
|
|
4. End checklist from `PLUGIN-GUIDE.md` section 12.
|
|
|
|
Done means: a colleague can clone the repo, enable the plugin, print a bin
|
|
label, and take a part at the kiosk with their badge - without asking you
|
|
anything.
|
|
|
|
---
|
|
|
|
## Where each pattern lives (cheat sheet)
|
|
|
|
| Need | Copy from |
|
|
|---|---|
|
|
| Standalone (non-asset) plugin shape | `plugins/knowledgebase/` |
|
|
| Checkout/ledger + badge contract | `plugins/usb/` (`api/routes.py` badge regex, `api/selfhosted.py` name resolve) |
|
|
| Real-baseline plugin migration | `plugins/measuringtools/migrations/` |
|
|
| Blueprint style, pagination, authz | `plugins/measuringtools/api/routes.py` |
|
|
| Image upload/serve/delete | `shopdb/core/api/models.py` |
|
|
| Open kiosk endpoints precedent | `plugins/employees/api/routes.py`, `plugins/notifications/api/routes.py` |
|
|
| Plugin-owned label print view | `frontend/src/views/print/USBLabelBatch.vue` |
|
|
| Barcode/QR rendering | JsBarcode usage in `AssetLabel.vue`, `qrLogo.js` |
|
|
| Kiosk route posture | `/shopfloor` in `frontend/src/router/index.js` |
|
|
| List/Detail master templates | `PrintersList.vue`, `PrinterDetail.vue` |
|
|
| Reports hook + CSV | `plugins/warranty/` report + `shopdb/core/api/reports.py` |
|
|
| Permissions declaration | `plugins/usb/plugin.py::get_permissions` |
|