From 543494120cc67f3e8503a2c1795796afa9256c40 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Wed, 25 Jun 2025 11:23:33 -0500 Subject: [PATCH] Refactor entity synchronization logic in Area, Brand, Item, RoomFunction, and Room models; improve ID handling and streamline foreign key resolution in settings --- models/areas.py | 55 +++++++++++++-------- models/brands.py | 34 +++++++++---- models/items.py | 37 +++++++++----- models/room_functions.py | 35 ++++++++----- models/rooms.py | 42 ++++++---------- routes.py | 19 ++++--- static/js/widget.js | 2 +- templates/fragments/_combobox_fragment.html | 13 ++++- templates/settings.html | 29 ++++++++--- 9 files changed, 170 insertions(+), 96 deletions(-) diff --git a/models/areas.py b/models/areas.py index d59d23d..d95c394 100644 --- a/models/areas.py +++ b/models/areas.py @@ -38,45 +38,60 @@ class Area(db.Model): submitted_clean = [] seen_ids = set() temp_id_map = {} - + for item in submitted_items: if not isinstance(item, dict): continue name = str(item.get("name", "")).strip() + raw_id = item.get("id") if not name: continue - submitted_clean.append({"id": item.get("id"), "name": name}) - if isinstance(item.get("id"), int) and item["id"] >= 0: - seen_ids.add(item["id"]) - + submitted_clean.append({"id": raw_id, "name": name}) + + # Record real (non-temp) IDs + try: + parsed_id = int(raw_id) + if parsed_id >= 0: + seen_ids.add(parsed_id) + except (ValueError, TypeError): + pass + existing_query = db.session.query(cls) existing_by_id = {area.id: area for area in existing_query.all()} existing_ids = set(existing_by_id.keys()) - + print(f"Existing area IDs: {existing_ids}") print(f"Submitted area IDs: {seen_ids}") - + for entry in submitted_clean: - submitted_id = entry.get("id") + submitted_id_raw = entry.get("id") submitted_name = entry["name"] - - if is_temp_id(submitted_id): + + if is_temp_id(submitted_id_raw): new_area = cls(name=submitted_name) db.session.add(new_area) db.session.flush() # Get the real ID - temp_id_map[submitted_id] = new_area.id + temp_id_map[submitted_id_raw] = new_area.id print(f"➕ Adding area: {submitted_name}") - elif submitted_id in existing_by_id: - area = existing_by_id[submitted_id] - if area.name != submitted_name: - print(f"✏️ Updating area {area.id}: '{area.name}' → '{submitted_name}'") - area.name = submitted_name - + else: + try: + submitted_id = int(submitted_id_raw) + except (ValueError, TypeError): + continue # Skip malformed ID + + if submitted_id in existing_by_id: + area = existing_by_id[submitted_id] + if area.name != submitted_name: + print(f"✏️ Updating area {area.id}: '{area.name}' → '{submitted_name}'") + area.name = submitted_name + for existing_id in existing_ids - seen_ids: area = existing_by_id[existing_id] db.session.delete(area) print(f"🗑️ Removing area: {area.name}") - - return temp_id_map - + id_map = { + **{str(i): i for i in seen_ids}, # "1" → 1 + **{str(temp): real for temp, real in temp_id_map.items()} # "temp-1" → 5 + } + return id_map diff --git a/models/brands.py b/models/brands.py index 62344d9..f58ee79 100644 --- a/models/brands.py +++ b/models/brands.py @@ -38,12 +38,18 @@ class Brand(db.Model): if not isinstance(item, dict): continue name = str(item.get("name", "")).strip() + raw_id = item.get("id") if not name: continue - submitted_clean.append({"id": item.get("id"), "name": name}) - if isinstance(item.get("id"), int) and item["id"] >= 0: - seen_ids.add(item["id"]) + submitted_clean.append({"id": raw_id, "name": name}) + try: + parsed_id = int(raw_id) + if parsed_id >= 0: + seen_ids.add(parsed_id) + except (ValueError, TypeError): + pass + existing_by_id = {b.id: b for b in db.session.query(cls).all()} existing_ids = set(existing_by_id.keys()) @@ -60,16 +66,26 @@ class Brand(db.Model): db.session.flush() temp_id_map[submitted_id] = obj.id print(f"➕ Adding brand: {name}") - elif submitted_id in existing_by_id: - obj = existing_by_id[submitted_id] - if obj.name != name: - print(f"✏️ Updating brand {obj.id}: '{obj.name}' → '{name}'") - obj.name = name + else: + try: + parsed_id = int(submitted_id) + except (ValueError, TypeError): + continue + + if parsed_id in existing_by_id: + obj = existing_by_id[parsed_id] + if obj.name != name: + print(f"✏️ Updating brand {obj.id}: '{obj.name}' → '{name}'") + obj.name = name for id_to_remove in existing_ids - seen_ids: db.session.delete(existing_by_id[id_to_remove]) print(f"🗑️ Removing brand ID {id_to_remove}") - return temp_id_map + id_map = { + **{str(i): i for i in seen_ids}, # "1" → 1 + **{str(temp): real for temp, real in temp_id_map.items()} # "temp-1" → 5 + } + return id_map diff --git a/models/items.py b/models/items.py index 5775b58..d86d974 100644 --- a/models/items.py +++ b/models/items.py @@ -40,12 +40,20 @@ class Item(db.Model): for item in submitted_items: if not isinstance(item, dict): continue - description = str(item.get("name", "")).strip() - if not description: + name = str(item.get("name", "")).strip() + raw_id = item.get("id") + + if not name: continue - submitted_clean.append({"id": item.get("id"), "description": description}) - if isinstance(item.get("id"), int) and item["id"] >= 0: - seen_ids.add(item["id"]) + + try: + parsed_id = int(raw_id) + if parsed_id >= 0: + seen_ids.add(parsed_id) + except (ValueError, TypeError): + pass + + submitted_clean.append({"id": raw_id, "description": name}) existing_by_id = {t.id: t for t in db.session.query(cls).all()} existing_ids = set(existing_by_id.keys()) @@ -54,7 +62,7 @@ class Item(db.Model): print(f"Submitted item IDs: {seen_ids}") for entry in submitted_clean: - submitted_id = entry.get("id") + submitted_id = entry["id"] description = entry["description"] if is_temp_id(submitted_id): @@ -63,15 +71,20 @@ class Item(db.Model): db.session.flush() temp_id_map[submitted_id] = obj.id print(f"➕ Adding type: {description}") - elif submitted_id in existing_by_id: - obj = existing_by_id[submitted_id] - if obj.description != description: + elif isinstance(submitted_id, int) or submitted_id.isdigit(): + submitted_id_int = int(submitted_id) + obj = existing_by_id.get(submitted_id_int) + if obj and obj.description != description: print(f"✏️ Updating type {obj.id}: '{obj.description}' → '{description}'") obj.description = description for id_to_remove in existing_ids - seen_ids: - db.session.delete(existing_by_id[id_to_remove]) + obj = existing_by_id[id_to_remove] + db.session.delete(obj) print(f"🗑️ Removing type ID {id_to_remove}") - return temp_id_map - + id_map = { + **{str(i): i for i in seen_ids}, + **{str(temp): real for temp, real in temp_id_map.items()} + } + return id_map diff --git a/models/room_functions.py b/models/room_functions.py index 1d69ac2..02685f6 100644 --- a/models/room_functions.py +++ b/models/room_functions.py @@ -37,12 +37,20 @@ class RoomFunction(db.Model): for item in submitted_items: if not isinstance(item, dict): continue - description = str(item.get("name", "")).strip() - if not description: + name = str(item.get("name", "")).strip() + raw_id = item.get("id") + + if not name: continue - submitted_clean.append({"id": item.get("id"), "description": description}) - if isinstance(item.get("id"), int) and item["id"] >= 0: - seen_ids.add(item["id"]) + + try: + parsed_id = int(raw_id) + if parsed_id >= 0: + seen_ids.add(parsed_id) + except (ValueError, TypeError): + pass + + submitted_clean.append({"id": raw_id, "description": name}) existing_by_id = {f.id: f for f in db.session.query(cls).all()} existing_ids = set(existing_by_id.keys()) @@ -60,15 +68,20 @@ class RoomFunction(db.Model): db.session.flush() temp_id_map[submitted_id] = obj.id print(f"➕ Adding function: {description}") - elif submitted_id in existing_by_id: - obj = existing_by_id[submitted_id] - if obj.description != description: + elif isinstance(submitted_id, int) or submitted_id.isdigit(): + submitted_id_int = int(submitted_id) + obj = existing_by_id.get(submitted_id_int) + if obj and obj.description != description: print(f"✏️ Updating function {obj.id}: '{obj.description}' → '{description}'") obj.description = description for id_to_remove in existing_ids - seen_ids: - db.session.delete(existing_by_id[id_to_remove]) + obj = existing_by_id[id_to_remove] + db.session.delete(obj) print(f"🗑️ Removing function ID {id_to_remove}") - return temp_id_map - + id_map = { + **{str(i): i for i in seen_ids}, + **{str(temp): real for temp, real in temp_id_map.items()} + } + return id_map diff --git a/models/rooms.py b/models/rooms.py index afbafd8..3badbd3 100644 --- a/models/rooms.py +++ b/models/rooms.py @@ -49,36 +49,25 @@ class Room(db.Model): def sync_from_state( cls, submitted_rooms: list[dict], - section_map: dict, - function_map: dict, - section_fallbacks: list, - function_fallbacks: list + section_map: dict[int, int], + function_map: dict[int, int] ): """ Syncs the Rooms table with the submitted room list. Resolves foreign keys using section_map and function_map. Supports add, update, and delete. """ - - def resolve_fk(raw_id, fallback_list, id_map, label): - if not raw_id: + def resolve_fk(key, fk_map, label): + # Print the fucking map so we can see what we're working with + print(f"Resolving {label} ID: {key} using map: {fk_map}") + if key is None: return None - - # 🛠 Handle SQLAlchemy model objects and dicts both - name_entry = next( - (getattr(item, "name", None) or item.get("name") - for item in fallback_list - if str(getattr(item, "id", None) or item.get("id")) == str(raw_id)), - None - ) - - if name_entry is None: - raise ValueError(f"Unable to resolve {label} ID: {raw_id}") - - resolved_id = id_map.get(name_entry) - if resolved_id is None: - raise ValueError(f"{label.capitalize()} '{name_entry}' not found in ID map.") - return resolved_id + key = str(key) + if key.startswith("temp") or not key.isdigit(): + if key in fk_map: + return fk_map[key] + raise ValueError(f"Unable to resolve {label} ID: {key}") + return int(key) # It's already a real ID submitted_clean = [] seen_ids = set() @@ -105,7 +94,7 @@ class Room(db.Model): try: seen_ids.add(int(rid)) except ValueError: - pass # It's an invalid non-temp string + pass # Not valid? Not seen. existing_query = db.session.query(cls) existing_by_id = {room.id: room for room in existing_query.all()} @@ -118,8 +107,8 @@ class Room(db.Model): rid = entry.get("id") name = entry["name"] - resolved_section_id = resolve_fk(entry.get("section_id"), section_fallbacks, section_map, "section") - resolved_function_id = resolve_fk(entry.get("function_id"), function_fallbacks, function_map, "function") + resolved_section_id = resolve_fk(entry.get("section_id"), section_map, "section") + resolved_function_id = resolve_fk(entry.get("function_id"), function_map, "function") if not rid or str(rid).startswith("room-"): new_room = cls(name=name, area_id=resolved_section_id, function_id=resolved_function_id) @@ -158,4 +147,3 @@ class Room(db.Model): db.session.delete(room) print(f"🗑️ Removing room: {room.name}") - diff --git a/routes.py b/routes.py index 6345525..d97a8e8 100644 --- a/routes.py +++ b/routes.py @@ -424,18 +424,23 @@ def settings(): room = dict(room) # shallow copy sid = room.get("section_id") fid = room.get("function_id") - if sid in section_map: - room["section_id"] = section_map[sid] - if fid in function_map: - room["function_id"] = function_map[fid] + + if sid is not None: + sid_key = str(sid) + if sid_key in section_map: + room["section_id"] = section_map[sid_key] + + if fid is not None: + fid_key = str(fid) + if fid_key in function_map: + room["function_id"] = function_map[fid_key] + submitted_rooms.append(room) Room.sync_from_state( submitted_rooms=submitted_rooms, section_map=section_map, - function_map=function_map, - section_fallbacks=[{"id": v, "name": k} for k, v in section_map.items()], - function_fallbacks=[{"id": v, "name": k} for k, v in function_map.items()] + function_map=function_map ) print("✅ COMMIT executed.") diff --git a/static/js/widget.js b/static/js/widget.js index ff719e2..2c418ed 100644 --- a/static/js/widget.js +++ b/static/js/widget.js @@ -74,7 +74,7 @@ const ComboBoxWidget = (() => { removeBtn.disabled = selected.length === 0; if (selected.length === 1) { - input.value = selected[0].textContent; + input.value = selected[0].textContent.trim(); currentlyEditing = selected[0]; addBtn.disabled = input.value.trim() === ''; } else { diff --git a/templates/fragments/_combobox_fragment.html b/templates/fragments/_combobox_fragment.html index c878f2b..f6ce00a 100644 --- a/templates/fragments/_combobox_fragment.html +++ b/templates/fragments/_combobox_fragment.html @@ -1,6 +1,6 @@ {% import "fragments/_icon_fragment.html" as icons %} -{% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none) %} +{% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none, data_attributes=none) %} {% if label %} @@ -18,7 +18,16 @@ diff --git a/templates/settings.html b/templates/settings.html index 4b45a60..73692c1 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -84,7 +84,8 @@ submit_button=True label='Rooms', placeholder='Add a new room', onAdd=room_editor, - onEdit=room_editor + onEdit=room_editor, + data_attributes={'area_id': 'section-id', 'function_id': 'function-id'} ) }} @@ -154,17 +155,23 @@ submit_button=True })); } + function sanitizeFk(val) { + return val && val !== "null" && val !== "" ? val : null; + } + const roomOptions = Array.from(document.getElementById('room-list').options); const rooms = roomOptions.map(opt => { const data = opt.dataset; + return { id: opt.value || undefined, name: opt.textContent.trim(), - section_id: data.sectionId ?? null, - function_id: data.functionId ?? null, + section_id: sanitizeFk(data.sectionId), + function_id: sanitizeFk(data.functionId), }; }); + return { brands: extractOptions("brand"), types: extractOptions("type"), @@ -181,8 +188,16 @@ submit_button=True const form = document.getElementById('settingsForm'); // Replace the whole submission logic with just JSON - form.addEventListener('submit', () => { - document.getElementById('formStateField').value = JSON.stringify(buildFormState()); + form.addEventListener('submit', (event) => { + event.preventDefault(); // 🚨 Stop form from leaving the building + try { + const state = buildFormState(); + document.getElementById('formStateField').value = JSON.stringify(state); + form.submit(); // 🟢 Now it can go + } catch (err) { + alert("Form submission failed: " + err.message); + console.error("Failed to build form state:", err); + } }); // Modal populates dropdowns fresh from the page every time it opens @@ -232,8 +247,8 @@ submit_button=True existingOption.textContent = name; existingOption.value = idRaw; - existingOption.dataset.sectionId = sectionVal; - existingOption.dataset.functionId = funcVal; + existingOption.dataset.sectionId = sectionVal || ""; + existingOption.dataset.functionId = funcVal || ""; ComboBoxWidget.sortOptions(roomList);