e8a918fc7b
- 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)
336 lines
10 KiB
Python
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},
|
|
}
|