Implement sync_from_state method for Area, Brand, Item, RoomFunction, and Room models; streamline entity management in settings

This commit is contained in:
Yaro Kasear 2025-06-24 13:09:41 -05:00
parent 87fa623cde
commit 8a5c5db9e0
11 changed files with 204 additions and 70 deletions

View file

@ -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}")

View file

@ -26,3 +26,33 @@ class Brand(db.Model):
'id': self.id,
'name': self.name
}
@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}")

View file

@ -29,3 +29,33 @@ class Item(db.Model):
'name': self.description,
'category': self.category
}
@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}")

View file

@ -26,3 +26,33 @@ class RoomFunction(db.Model):
'id': self.id,
'name': self.description
}
@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}")

View file

@ -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}")

View file

@ -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", []),

View file

@ -1,6 +1,8 @@
{% import "fragments/_icon_fragment.html" as icons %}
{% macro breadcrumb_header(breadcrumbs=[], title=None, submit_button=False) %}
<!-- Breadcrumb Fragment -->
<nav class="row d-flex mb-3 justify-content-between">
<div class="col">
<ol class="breadcrumb">

View file

@ -1,6 +1,8 @@
{% import "fragments/_icon_fragment.html" as icons %}
{% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none) %}
<!-- Combobox Widget Fragment -->
{% if label %}
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
{% endif %}

View file

@ -1,4 +1,6 @@
{% macro render_icon(icon, size=24, extra_class='') %}
<!-- Icon Fragment -->
<i class="bi bi-{{ icon }} {{ extra_class }}" style="font-size: {{ size }}px;"></i>
{% endmacro %}

View file

@ -1,6 +1,8 @@
{% import "fragments/_icon_fragment.html" as icons %}
{% macro category_link(endpoint, label, icon_html=none, arguments={}) %}
<!-- Category Link Fragment -->
<div class="col text-center">
<a href="{{ url_for('main.' + endpoint, **arguments) }}"
class="d-flex flex-column justify-content-center link-success link-underline-opacity-0">
@ -13,6 +15,12 @@
{% endmacro %}
{% macro navigation_link(endpoint, label, icon_html=none, arguments={}, active=false) %}
<!-- Navigation Link Fragment -->
{% if not active %}
{% set active = request.endpoint == 'main.' + endpoint %}
{% endif %}
<li class="nav-item">
<a href="{{ url_for('main.' + endpoint, **arguments) }}" class="nav-link{% if active %} active{% endif %}">
{% if icon_html %}
@ -24,6 +32,8 @@
{% endmacro %}
{% macro entry_link(endpoint, id) %}
<!-- Entry Link Fragment -->
<a href="{{ url_for('main.' + endpoint, id=id) }}" class="link-success link-underline-opacity-0">
{{ icons.render_icon('link', 12) }}
</a>

View file

@ -1,4 +1,6 @@
{% macro render_table(headers, rows, id, entry_route=None, title=None, per_page=15) %}
<!-- Table Fragment -->
{% if rows %}
{% if title %}
<label for="datatable-{{ id|default('table')|replace(' ', '-')|lower }}" class="form-label">{{ title }}</label>