From 8a5c5db9e043e872f9a242cb390f55a4a201b2e1 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Tue, 24 Jun 2025 13:09:41 -0500 Subject: [PATCH] Implement sync_from_state method for Area, Brand, Item, RoomFunction, and Room models; streamline entity management in settings --- models/areas.py | 30 ++++++++ models/brands.py | 32 +++++++- models/items.py | 32 +++++++- models/room_functions.py | 32 +++++++- models/rooms.py | 57 +++++++++++++++ routes.py | 73 ++----------------- templates/fragments/_breadcrumb_fragment.html | 2 + templates/fragments/_combobox_fragment.html | 2 + templates/fragments/_icon_fragment.html | 2 + templates/fragments/_link_fragment.html | 10 +++ templates/fragments/_table_fragment.html | 2 + 11 files changed, 204 insertions(+), 70 deletions(-) diff --git a/models/areas.py b/models/areas.py index 40ef67a..9576068 100644 --- a/models/areas.py +++ b/models/areas.py @@ -26,3 +26,33 @@ class Area(db.Model): 'id': self.id, 'name': self.name } + + @classmethod + def sync_from_state(cls, submitted_items: list[dict]): + """ + Syncs the Area table with the provided list of dictionaries. + Adds new areas and removes areas that are not in the list. + """ + submitted = { + str(item.get("name", "")).strip() + for item in submitted_items + if isinstance(item, dict) and str(item.get("name", "")).strip() + } + + existing_query = db.session.query(cls) + existing = {area.name: area for area in existing_query.all()} + + existing_names = set(existing.keys()) + + print(f"Existing areas: {existing_names}") + print(f"Submitted areas: {submitted}") + print(f"Areas to add: {submitted - existing_names}") + print(f"Areas to remove: {existing_names - submitted}") + + for name in submitted - existing_names: + db.session.add(cls(name=name)) + print(f"➕ Adding area: {name}") + + for name in existing_names - submitted: + db.session.delete(existing[name]) + print(f"🗑️ Removing area: {name}") diff --git a/models/brands.py b/models/brands.py index 6363982..82f913c 100644 --- a/models/brands.py +++ b/models/brands.py @@ -25,4 +25,34 @@ class Brand(db.Model): return { 'id': self.id, 'name': self.name - } \ No newline at end of file + } + + @classmethod + def sync_from_state(cls, submitted_items: list[dict]): + """ + Syncs the Brand table with the provided list of dictionaries. + Adds new brands and removes brands that are not in the list. + """ + submitted = { + str(item.get("name", "")).strip() + for item in submitted_items + if isinstance(item, dict) and str(item.get("name", "")).strip() + } + + existing_query = db.session.query(cls) + existing = {brand.name: brand for brand in existing_query.all()} + + existing_names = set(existing.keys()) + + print(f"Existing brands: {existing_names}") + print(f"Submitted brands: {submitted}") + print(f"Brands to add: {submitted - existing_names}") + print(f"Brands to remove: {existing_names - submitted}") + + for name in submitted - existing_names: + db.session.add(cls(name=name)) + print(f"➕ Adding brand: {name}") + + for name in existing_names - submitted: + db.session.delete(existing[name]) + print(f"🗑️ Removing brand: {name}") diff --git a/models/items.py b/models/items.py index 104c831..95aac57 100644 --- a/models/items.py +++ b/models/items.py @@ -28,4 +28,34 @@ class Item(db.Model): 'id': self.id, 'name': self.description, 'category': self.category - } \ No newline at end of file + } + + @classmethod + def sync_from_state(cls, submitted_items: list[dict]): + """ + Syncs the Items table with the provided list of dictionaries. + Adds new items and removes items that are not in the list. + """ + submitted = { + str(item.get("name", "")).strip() + for item in submitted_items + if isinstance(item, dict) and str(item.get("name", "")).strip() + } + + existing_query = db.session.query(cls) + existing = {item.description: item for item in existing_query.all()} + + existing_descriptions = set(existing.keys()) + + print(f"Existing items: {existing_descriptions}") + print(f"Submitted items: {submitted}") + print(f"items to add: {submitted - existing_descriptions}") + print(f"items to remove: {existing_descriptions - submitted}") + + for description in submitted - existing_descriptions: + db.session.add(cls(description=description)) + print(f"➕ Adding item: {description}") + + for description in existing_descriptions - submitted: + db.session.delete(existing[description]) + print(f"🗑️ Removing item: {description}") \ No newline at end of file diff --git a/models/room_functions.py b/models/room_functions.py index 4a1bc06..455fbbe 100644 --- a/models/room_functions.py +++ b/models/room_functions.py @@ -25,4 +25,34 @@ class RoomFunction(db.Model): return { 'id': self.id, 'name': self.description - } \ No newline at end of file + } + + @classmethod + def sync_from_state(cls, submitted_items: list[dict]): + """ + Syncs the functions table with the provided list of dictionaries. + Adds new functions and removes functions that are not in the list. + """ + submitted = { + str(item.get("name", "")).strip() + for item in submitted_items + if isinstance(item, dict) and str(item.get("name", "")).strip() + } + + existing_query = db.session.query(cls) + existing = {item.description: item for item in existing_query.all()} + + existing_descriptions = set(existing.keys()) + + print(f"Existing functions: {existing_descriptions}") + print(f"Submitted functions: {submitted}") + print(f"Functions to add: {submitted - existing_descriptions}") + print(f"Functions to remove: {existing_descriptions - submitted}") + + for description in submitted - existing_descriptions: + db.session.add(cls(description=description)) + print(f"➕ Adding item: {description}") + + for description in existing_descriptions - submitted: + db.session.delete(existing[description]) + print(f"🗑️ Removing item: {description}") \ No newline at end of file diff --git a/models/rooms.py b/models/rooms.py index 04676f4..02dc3fc 100644 --- a/models/rooms.py +++ b/models/rooms.py @@ -45,3 +45,60 @@ class Room(db.Model): 'function_id': self.function_id } + @classmethod + def sync_from_state(cls, submitted_rooms: list[dict], section_map: dict, function_map: dict, section_fallbacks: list, function_fallbacks: list): + """ + Syncs the Rooms table with the submitted room list. + Resolves foreign keys using section_map and function_map. + """ + + def resolve_id(raw_id, fallback_list, id_map, label): + try: + resolved = int(raw_id) + if resolved >= 0: + if resolved in id_map.values(): + return resolved + raise ValueError(f"ID {resolved} not found in {label}.") + except Exception: + pass + + index = abs(resolved + 1) + try: + entry = fallback_list[index] + key = entry.get("name") if isinstance(entry, dict) else str(entry).strip() + final_id = id_map.get(key) + if final_id is None: + raise ValueError(f"ID for {key} not found in {label}.") + return final_id + except Exception as e: + raise ValueError(f"Failed to resolve {label} ID: {e}") from e + + existing_query = db.session.query(cls) + existing = {room.name: room for room in existing_query.all()} + + submitted = { + str(room.get("name", "")).strip(): room + for room in submitted_rooms + if isinstance(room, dict) and str(room.get("name", "")).strip() + } + + existing_names = set(existing.keys()) + submitted_names = set(submitted.keys()) + + print(f"Existing rooms: {existing_names}") + print(f"Submitted rooms: {submitted_names}") + print(f"Rooms to add: {submitted_names - existing_names}") + print(f"Rooms to remove: {existing_names - submitted_names}") + + for name in submitted_names - existing_names: + room_data = submitted[name] + area_id = resolve_id(room_data.get("section_id", -1), section_fallbacks, section_map, "section") if room_data.get("section_id") is not None else None + function_id = resolve_id(room_data.get("function_id", -1), function_fallbacks, function_map, "function") if room_data.get("function_id") is not None else None + new_room = cls(name=name, area_id=area_id, function_id=function_id) + db.session.add(new_room) + print(f"➕ Adding room: {new_room}") + + for name in existing_names - submitted_names: + room_to_remove = existing[name] + db.session.delete(room_to_remove) + print(f"🗑️ Removing room: {room_to_remove}") diff --git a/routes.py b/routes.py index 61e65be..ba4bf43 100644 --- a/routes.py +++ b/routes.py @@ -398,67 +398,6 @@ def search(): @main.route('/settings', methods=['GET', 'POST']) def settings(): - def process_entities(entity_list, model, attr, key_name="name"): - """Upserts and deletes based on entity name field.""" - existing = {getattr(e, attr): e.id for e in db.session.query(model).all()} - submitted = { - str(e.get(key_name, "")).strip() - for e in entity_list if isinstance(e, dict) and e.get(key_name) - } - - print(f"🔍 Existing: {existing}") - print(f"🆕 Submitted: {submitted}") - print(f"➕ To add: {submitted - set(existing)}") - print(f"➖ To delete: {set(existing) - submitted}") - - for name in submitted - set(existing): - db.session.add(model(**{attr: name})) - - for name in set(existing) - submitted: - db.session.execute(delete(model).where(getattr(model, attr) == name)) - - return submitted # Might be useful for mapping fallback - - def handle_rooms(rooms, section_map, function_map, section_fallbacks, function_fallbacks): - existing_rooms = {r.name: r.id for r in db.session.query(Room).all()} - submitted_rooms = { - str(r.get("name", "")).strip(): r for r in rooms if r.get("name") - } - - print(f"🔍 Rooms in DB: {list(existing_rooms.keys())}") - print(f"🆕 Rooms submitted: {list(submitted_rooms.keys())}") - print(f"➖ To delete: {set(existing_rooms) - set(submitted_rooms)}") - - for name, data in submitted_rooms.items(): - if name not in existing_rooms: - section_id = resolve_id(data.get("section_id"), section_fallbacks, section_map, "section") if data.get("section_id") is not None else None - function_id = resolve_id(data.get("function_id"), function_fallbacks, function_map, "function") if data.get("function_id") is not None else None - db.session.add(Room(name=name, area_id=section_id, function_id=function_id)) - - for name in set(existing_rooms) - set(submitted_rooms): - db.session.execute(delete(Room).where(Room.name == name)) - - def resolve_id(raw_id, fallback_list, id_map, label): - try: - resolved = int(raw_id) - if resolved >= 0: - if resolved in id_map.values(): - return resolved - raise ValueError(f"{label.title()} ID {resolved} not found.") - except Exception: - pass # Continue to fallback logic - - index = abs(resolved + 1) - try: - entry = fallback_list[index] - key = entry.get("name") if isinstance(entry, dict) else str(entry).strip() - final_id = id_map.get(key) - if final_id is None: - raise ValueError(f"Unresolved {label}: {key}") - return final_id - except Exception as e: - raise ValueError(f"Failed resolving {label} ID {raw_id}: {e}") from e - if request.method == 'POST': print("⚠️⚠️⚠️ POST /settings reached! ⚠️⚠️⚠️") form = request.form @@ -473,17 +412,17 @@ def settings(): try: with db.session.begin(): - brand_names = process_entities(state.get("brands", []), Brand, "name") - type_names = process_entities(state.get("types", []), Item, "description", "name") - section_names = process_entities(state.get("sections", []), Area, "name") - func_names = process_entities(state.get("functions", []), RoomFunction, "description") + Brand.sync_from_state(state.get("brands", [])) + Item.sync_from_state(state.get("types", [])) + Area.sync_from_state(state.get("sections", [])) + RoomFunction.sync_from_state(state.get("functions", [])) # Refresh maps after inserts section_map = {a.name: a.id for a in db.session.query(Area).all()} function_map = {f.description: f.id for f in db.session.query(RoomFunction).all()} - handle_rooms( - rooms=state.get("rooms", []), + Room.sync_from_state( + submitted_rooms=state.get("rooms", []), section_map=section_map, function_map=function_map, section_fallbacks=state.get("sections", []), diff --git a/templates/fragments/_breadcrumb_fragment.html b/templates/fragments/_breadcrumb_fragment.html index dbb9922..c197511 100644 --- a/templates/fragments/_breadcrumb_fragment.html +++ b/templates/fragments/_breadcrumb_fragment.html @@ -1,6 +1,8 @@ {% import "fragments/_icon_fragment.html" as icons %} {% macro breadcrumb_header(breadcrumbs=[], title=None, submit_button=False) %} + +