Implement sync_from_state method for Area, Brand, Item, RoomFunction, and Room models; streamline entity management in settings
This commit is contained in:
parent
87fa623cde
commit
8a5c5db9e0
11 changed files with 204 additions and 70 deletions
|
@ -26,3 +26,33 @@ class Area(db.Model):
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'name': self.name
|
'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}")
|
||||||
|
|
|
@ -26,3 +26,33 @@ class Brand(db.Model):
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'name': self.name
|
'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}")
|
||||||
|
|
|
@ -29,3 +29,33 @@ class Item(db.Model):
|
||||||
'name': self.description,
|
'name': self.description,
|
||||||
'category': self.category
|
'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}")
|
|
@ -26,3 +26,33 @@ class RoomFunction(db.Model):
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'name': self.description
|
'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}")
|
|
@ -45,3 +45,60 @@ class Room(db.Model):
|
||||||
'function_id': self.function_id
|
'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}")
|
||||||
|
|
73
routes.py
73
routes.py
|
@ -398,67 +398,6 @@ def search():
|
||||||
|
|
||||||
@main.route('/settings', methods=['GET', 'POST'])
|
@main.route('/settings', methods=['GET', 'POST'])
|
||||||
def settings():
|
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':
|
if request.method == 'POST':
|
||||||
print("⚠️⚠️⚠️ POST /settings reached! ⚠️⚠️⚠️")
|
print("⚠️⚠️⚠️ POST /settings reached! ⚠️⚠️⚠️")
|
||||||
form = request.form
|
form = request.form
|
||||||
|
@ -473,17 +412,17 @@ def settings():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db.session.begin():
|
with db.session.begin():
|
||||||
brand_names = process_entities(state.get("brands", []), Brand, "name")
|
Brand.sync_from_state(state.get("brands", []))
|
||||||
type_names = process_entities(state.get("types", []), Item, "description", "name")
|
Item.sync_from_state(state.get("types", []))
|
||||||
section_names = process_entities(state.get("sections", []), Area, "name")
|
Area.sync_from_state(state.get("sections", []))
|
||||||
func_names = process_entities(state.get("functions", []), RoomFunction, "description")
|
RoomFunction.sync_from_state(state.get("functions", []))
|
||||||
|
|
||||||
# Refresh maps after inserts
|
# Refresh maps after inserts
|
||||||
section_map = {a.name: a.id for a in db.session.query(Area).all()}
|
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()}
|
function_map = {f.description: f.id for f in db.session.query(RoomFunction).all()}
|
||||||
|
|
||||||
handle_rooms(
|
Room.sync_from_state(
|
||||||
rooms=state.get("rooms", []),
|
submitted_rooms=state.get("rooms", []),
|
||||||
section_map=section_map,
|
section_map=section_map,
|
||||||
function_map=function_map,
|
function_map=function_map,
|
||||||
section_fallbacks=state.get("sections", []),
|
section_fallbacks=state.get("sections", []),
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{% import "fragments/_icon_fragment.html" as icons %}
|
{% import "fragments/_icon_fragment.html" as icons %}
|
||||||
|
|
||||||
{% macro breadcrumb_header(breadcrumbs=[], title=None, submit_button=False) %}
|
{% macro breadcrumb_header(breadcrumbs=[], title=None, submit_button=False) %}
|
||||||
|
<!-- Breadcrumb Fragment -->
|
||||||
|
|
||||||
<nav class="row d-flex mb-3 justify-content-between">
|
<nav class="row d-flex mb-3 justify-content-between">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{% import "fragments/_icon_fragment.html" as icons %}
|
{% 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) %}
|
||||||
|
<!-- Combobox Widget Fragment -->
|
||||||
|
|
||||||
{% if label %}
|
{% if label %}
|
||||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
{% macro render_icon(icon, size=24, extra_class='') %}
|
{% macro render_icon(icon, size=24, extra_class='') %}
|
||||||
|
<!-- Icon Fragment -->
|
||||||
|
|
||||||
<i class="bi bi-{{ icon }} {{ extra_class }}" style="font-size: {{ size }}px;"></i>
|
<i class="bi bi-{{ icon }} {{ extra_class }}" style="font-size: {{ size }}px;"></i>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{% import "fragments/_icon_fragment.html" as icons %}
|
{% import "fragments/_icon_fragment.html" as icons %}
|
||||||
|
|
||||||
{% macro category_link(endpoint, label, icon_html=none, arguments={}) %}
|
{% macro category_link(endpoint, label, icon_html=none, arguments={}) %}
|
||||||
|
<!-- Category Link Fragment -->
|
||||||
|
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<a href="{{ url_for('main.' + endpoint, **arguments) }}"
|
<a href="{{ url_for('main.' + endpoint, **arguments) }}"
|
||||||
class="d-flex flex-column justify-content-center link-success link-underline-opacity-0">
|
class="d-flex flex-column justify-content-center link-success link-underline-opacity-0">
|
||||||
|
@ -13,6 +15,12 @@
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro navigation_link(endpoint, label, icon_html=none, arguments={}, active=false) %}
|
{% 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">
|
<li class="nav-item">
|
||||||
<a href="{{ url_for('main.' + endpoint, **arguments) }}" class="nav-link{% if active %} active{% endif %}">
|
<a href="{{ url_for('main.' + endpoint, **arguments) }}" class="nav-link{% if active %} active{% endif %}">
|
||||||
{% if icon_html %}
|
{% if icon_html %}
|
||||||
|
@ -24,6 +32,8 @@
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro entry_link(endpoint, id) %}
|
{% macro entry_link(endpoint, id) %}
|
||||||
|
<!-- Entry Link Fragment -->
|
||||||
|
|
||||||
<a href="{{ url_for('main.' + endpoint, id=id) }}" class="link-success link-underline-opacity-0">
|
<a href="{{ url_for('main.' + endpoint, id=id) }}" class="link-success link-underline-opacity-0">
|
||||||
{{ icons.render_icon('link', 12) }}
|
{{ icons.render_icon('link', 12) }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
{% macro render_table(headers, rows, id, entry_route=None, title=None, per_page=15) %}
|
{% macro render_table(headers, rows, id, entry_route=None, title=None, per_page=15) %}
|
||||||
|
<!-- Table Fragment -->
|
||||||
|
|
||||||
{% if rows %}
|
{% if rows %}
|
||||||
{% if title %}
|
{% if title %}
|
||||||
<label for="datatable-{{ id|default('table')|replace(' ', '-')|lower }}" class="form-label">{{ title }}</label>
|
<label for="datatable-{{ id|default('table')|replace(' ', '-')|lower }}" class="form-label">{{ title }}</label>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue