Files
shawn e8a918fc7b feat: add is_disney column for reliable Disney/Non-Disney filtering
- Added is_disney INTEGER DEFAULT 0 column to assets table
- Backfilled 996 Disney assets based on D- customer prefix
- Server: is_disney in AssetCreate/AssetUpdate models, INSERT/UPDATE
- Server: disney_filter query param now uses is_disney column (no JOIN needed)
- import_cantaloupe.py: sets is_disney=1 when customer starts with D-
- import_machines.py: sets is_disney=1 when customer starts with D-
- disney_classify.py: sets is_disney=1 when classifying
- Frontend: new Location Type dropdown with Disney/Non-Disney toggle
  (replaces the __disney__/__non_disney__ park dropdown pseudo-options)
2026-05-22 18:34:13 -04:00

336 lines
10 KiB
Python

"""
Disney park classification for canteen assets.
Detects which Disney park/area an asset belongs to based on:
- Asset name pattern (D-{Park} ...)
- Address keywords
- Room/building name keywords
- Customer name (already D-{Park} prefixed)
"""
import re
from typing import Optional
# ─── Disney Park definitions ─────────────────────────────────────────────────
DISNEY_PARK_NAMES = {
"magic-kingdom": "Magic Kingdom",
"epcot": "Epcot",
"hollywood-studios": "Hollywood Studios",
"animal-kingdom": "Animal Kingdom",
"disney-springs": "Disney Springs",
"resort": "Resort",
"office": "Office",
"other": "Other",
}
# Park icons / emoji for UI
DISNEY_PARK_ICONS = {
"magic-kingdom": "🏰",
"epcot": "🌍",
"hollywood-studios": "🎬",
"animal-kingdom": "🌿",
"disney-springs": "🛍️",
"resort": "🏨",
"office": "🏢",
"other": "📍",
}
DISNEY_PARK_COLORS = {
"magic-kingdom": "#9b59b6", # purple
"epcot": "#3498db", # blue
"hollywood-studios": "#e74c3c", # red
"animal-kingdom": "#2ecc71", # green
"disney-springs": "#f39c12", # orange
"resort": "#1abc9c", # teal
"office": "#95a5a6", # gray
"other": "#bdc3c7", # light gray
}
# ─── Resort name keywords ────────────────────────────────────────────────────
RESORT_KEYWORDS = [
"WILDERNESS LODGE", "Wilderness Lodge",
"FORT WILDERNESS", "Fort Wilderness",
"GRAND FLORIDIAN", "Grand Floridian", "Floridian",
"BOARDWALK", "Boardwalk",
"SARATOGA SPRINGS", "Saratoga Springs",
"CARIBBEAN BEACH", "Caribbean Beach",
"CORONADO SPRINGS", "Coronado Springs",
"GRAN DESTINO", "Gran Destino",
"PORT ORLEANS", "Port Orleans",
"POLYNESIAN", "Polynesian",
"CONTEMPORARY", "Contemporary",
"ANIMAL KINGDOM LODGE", "Animal Kingdom Lodge",
"ANIMAL KNGDM LODGE", "Animal Kngdm Lodge",
"KIDANI", "Kidani",
"BEACH CLUB", "Beach Club",
"YACHT CLUB", "Yacht Club",
"OLD KEY WEST", "Old Key West",
"RIVIERA", "Riviera",
"POP CENTURY", "Pop Century",
"ART OF ANIMATION", "Art of Animation",
"ALL STAR", "All Star",
"SHADES OF GREEN", "Shades of Green",
"BUENA VISTA PALACE", "Buena Vista Palace",
"WYNDHAM", "Wyndham",
"HILTON", "Hilton",
"IHG HOTEL", "IHG Hotel",
"DAYS INN", "Days Inn",
"B RESORT", "B Resort",
"DOUBLETREE", "DoubleTree",
"HOLIDAY INN", "Holiday Inn",
"BEST WESTERN", "Best Western",
"MAGNOLIA", "Magnolia", # Saratoga Springs area
]
# ─── Classification logic ────────────────────────────────────────────────────
def classify_asset(name: str, address: str = "", building_name: str = "",
room: str = "", customer_name: str = "") -> Optional[str]:
"""
Classify a canteen asset into a Disney park/area.
Returns a disney_park key or None if not Disney-related.
"""
text = f"{name} {address} {building_name} {room} {customer_name}"
# Quick check — is this Disney at all?
if not _is_disney_related(text):
return None
# 1. Direct name pattern: D-{Park} ...
park = _match_name_prefix(name)
if park:
return park
# 2. Address/building keywords
park = _match_address_keywords(address, building_name, room)
if park:
return park
# 3. Customer name pattern
park = _match_customer_name(customer_name)
if park:
return park
# 4. Resort keywords in any field
if _is_resort(text):
return "resort"
# 5. Default: other Disney
return "other"
def _is_disney_related(text: str) -> bool:
"""Check if this asset is Disney-related at all."""
t = text.upper()
keywords = [
"DISNEY", "WDW", "MAGIC KINGDOM", "EPCOT",
"HOLLYWOOD STUDIOS", "ANIMAL KINGDOM",
"DISNEY SPRINGS", "LAKE BUENA VISTA", "BAY LAKE",
"SEVEN SEAS", "HOTEL PLAZA BLVD",
"D-", "D_MAGIC", "D_EPCOT", "D_HOLLYWOOD", "D_ANIMAL",
"FLORIDIAN", "WILDERNESS LODGE", "BOARDWALK",
"GRAND FLORIDIAN", "SARATOGA SPRINGS",
"BUENA VISTA",
]
return any(kw in t for kw in keywords)
def _match_name_prefix(name: str) -> Optional[str]:
"""Match D-{Park} pattern in asset name."""
n = name.upper()
# D-Magic Kingdom
if "D-MAGIC KINGDOM" in n or "D_MAGIC KINGDOM" in n:
return "magic-kingdom"
# D-Epcot
if "D-EPCOT" in n or "D_EPCOT" in n:
return "epcot"
# D-Hollywood Studios
if "D-HOLLYWOOD STUDIOS" in n or "D_HOLLYWOOD STUDIOS" in n:
return "hollywood-studios"
# D-Animal Kingdom or D-AK
if "D-ANIMAL KINGDOM" in n or "D_ANIMAL KINGDOM" in n or " D-AK " in n:
return "animal-kingdom"
# D-Disney Springs
if "D-DISNEY SPRINGS" in n or "D_DISNEY SPRINGS" in n:
return "disney-springs"
# Resort detection in name prefix
if n.startswith("D-") or " - " in n:
# Extract the location part after D- and before any type indicator
for kw in RESORT_KEYWORDS:
if kw.upper() in n:
return "resort"
return None
def _match_address_keywords(address: str, building: str, room: str) -> Optional[str]:
"""Match address/building patterns to parks."""
addr = (address + " " + building + " " + room).upper()
# Magic Kingdom
if "SEVEN SEAS" in addr or "MAGIC KINGDOM" in addr or "BAY LAKE" in addr:
return "magic-kingdom"
# Epcot
if "EPCOT CENTER" in addr or "EPCOT" in addr:
return "epcot"
# Hollywood Studios
if "EAST STUDIO" in addr or "HOLLYWOOD STUDIOS" in addr:
return "hollywood-studios"
# Animal Kingdom
if "RAINFOREST" in addr or "ANIMAL KINGDOM" in addr or " AK " in addr or addr.startswith("AK "):
return "animal-kingdom"
# Disney Springs / Hotel Plaza Blvd area
if "DISNEY SPRINGS" in addr or "BUENA VISTA DR" in addr or "HOTEL PLAZA BLVD" in addr:
return "disney-springs"
# Floridian Way = Grand Floridian (resort)
if "FLORIDIAN" in addr:
return "resort"
# Epcot Resorts Blvd = Boardwalk area
if "EPCOT RESORTS" in addr:
return "resort"
return None
def _match_customer_name(customer: str) -> Optional[str]:
"""Match customer name to park classification."""
c = customer.upper()
if "D-MAGIC KINGDOM" in c:
return "magic-kingdom"
if "D-EPCOT" in c:
return "epcot"
if "D-HOLLYWOOD STUDIOS" in c:
return "hollywood-studios"
if "D-DISNEY SPRINGS" in c:
return "disney-springs"
if "D-DISNEY VENDING" in c or "D-DISNEY WORLD" in c:
return "office"
# D-{Resort} pattern
if c.startswith("D-"):
for kw in RESORT_KEYWORDS:
if kw.upper() in c:
return "resort"
# If it starts with D- and we haven't matched yet, it's some Disney thing
return "other"
return None
def _is_resort(text: str) -> bool:
"""Check if text mentions a known Disney resort."""
t = text.upper()
for kw in RESORT_KEYWORDS:
if kw.upper() in t:
return True
return False
# ─── Batch classification ────────────────────────────────────────────────────
def classify_all_assets(db_path: str) -> dict:
"""
Run classification on all Disney-related assets in the DB.
Returns a report of what was classified.
"""
import sqlite3
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
# Get all assets that are Disney-related (or have no classification yet)
rows = conn.execute("""
SELECT a.id, a.machine_id, a.name, a.address, a.building_name, a.room,
a.disney_park, c.name as customer_name
FROM assets a
LEFT JOIN customers c ON a.customer_id = c.id
WHERE a.disney_park IS NULL
""").fetchall()
results = {"classified": [], "unclassifiable": [], "total_checked": len(rows)}
counts = {}
for row in rows:
park = classify_asset(
name=row['name'] or '',
address=row['address'] or '',
building_name=row['building_name'] or '',
room=row['room'] or '',
customer_name=row['customer_name'] or '',
)
if park:
conn.execute(
"UPDATE assets SET disney_park = ?, is_disney = 1, updated_at = datetime('now') WHERE id = ?",
(park, row['id'])
)
results["classified"].append({
"id": row['id'],
"machine_id": row['machine_id'],
"name": row['name'],
"park": park,
})
counts[park] = counts.get(park, 0) + 1
else:
results["unclassifiable"].append({
"id": row['id'],
"machine_id": row['machine_id'],
"name": row['name'],
})
conn.commit()
conn.close()
results["counts"] = counts
results["total_classified"] = len(results["classified"])
return results
def get_classification_stats(db_path: str) -> dict:
"""Get statistics about current Disney classifications."""
import sqlite3
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
rows = conn.execute("""
SELECT disney_park, COUNT(*) as count
FROM assets
WHERE disney_park IS NOT NULL
GROUP BY disney_park
ORDER BY count DESC
""").fetchall()
total = conn.execute("SELECT COUNT(*) as c FROM assets").fetchone()['c']
disney = conn.execute(
"SELECT COUNT(*) as c FROM assets WHERE disney_park IS NOT NULL"
).fetchone()['c']
unclassified = conn.execute(
"SELECT COUNT(*) as c FROM assets WHERE name LIKE '%Disney%' OR name LIKE '%D-%' AND disney_park IS NULL"
).fetchone()['c']
conn.close()
return {
"total_assets": total,
"classified": disney,
"unclassified_disney": 0, # Will be accurate after classify run
"by_park": {row['disney_park']: row['count'] for row in rows},
}