Implement sync_from_state methods for Area, Brand, Item, RoomFunction, and Room models; enhance entity management and foreign key resolution in settings

This commit is contained in:
Yaro Kasear 2025-06-25 09:31:05 -05:00
parent 8a5c5db9e0
commit 7833c4828b
9 changed files with 359 additions and 186 deletions

View file

@ -6,6 +6,7 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
from ..temp import is_temp_id
class Area(db.Model):
__tablename__ = 'Areas'
@ -28,31 +29,54 @@ class Area(db.Model):
}
@classmethod
def sync_from_state(cls, submitted_items: list[dict]):
def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
"""
Syncs the Area table with the provided list of dictionaries.
Adds new areas and removes areas that are not in the list.
Syncs the Area table (aka 'sections') with the submitted list.
Supports add, update, and delete.
Also returns a mapping of temp IDs to real IDs for resolution.
"""
submitted = {
str(item.get("name", "")).strip()
for item in submitted_items
if isinstance(item, dict) and str(item.get("name", "")).strip()
}
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()
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"])
existing_query = db.session.query(cls)
existing = {area.name: area for area in existing_query.all()}
existing_by_id = {area.id: area for area in existing_query.all()}
existing_ids = set(existing_by_id.keys())
existing_names = set(existing.keys())
print(f"Existing area IDs: {existing_ids}")
print(f"Submitted area IDs: {seen_ids}")
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 entry in submitted_clean:
submitted_id = entry.get("id")
submitted_name = entry["name"]
if is_temp_id(submitted_id):
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
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
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
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

@ -6,6 +6,7 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
from ..temp import is_temp_id
class Brand(db.Model):
__tablename__ = 'Brands'
@ -28,31 +29,47 @@ class Brand(db.Model):
}
@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()
}
def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
submitted_clean = []
seen_ids = set()
temp_id_map = {}
existing_query = db.session.query(cls)
existing = {brand.name: brand for brand in existing_query.all()}
for item in submitted_items:
if not isinstance(item, dict):
continue
name = str(item.get("name", "")).strip()
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"])
existing_names = set(existing.keys())
existing_by_id = {b.id: b for b in db.session.query(cls).all()}
existing_ids = set(existing_by_id.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}")
print(f"Existing brand IDs: {existing_ids}")
print(f"Submitted brand IDs: {seen_ids}")
for name in submitted - existing_names:
db.session.add(cls(name=name))
for entry in submitted_clean:
submitted_id = entry.get("id")
name = entry["name"]
if is_temp_id(submitted_id):
obj = cls(name=name)
db.session.add(obj)
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
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
for name in existing_names - submitted:
db.session.delete(existing[name])
print(f"🗑️ Removing brand: {name}")

View file

@ -6,6 +6,7 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
from ..temp import is_temp_id
class Item(db.Model):
__tablename__ = 'Items'
@ -31,31 +32,46 @@ class Item(db.Model):
}
@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()
}
def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
submitted_clean = []
seen_ids = set()
temp_id_map = {}
existing_query = db.session.query(cls)
existing = {item.description: item for item in existing_query.all()}
for item in submitted_items:
if not isinstance(item, dict):
continue
description = str(item.get("name", "")).strip()
if not description:
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"])
existing_descriptions = set(existing.keys())
existing_by_id = {t.id: t for t in db.session.query(cls).all()}
existing_ids = set(existing_by_id.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}")
print(f"Existing item IDs: {existing_ids}")
print(f"Submitted item IDs: {seen_ids}")
for description in submitted - existing_descriptions:
db.session.add(cls(description=description))
print(f" Adding item: {description}")
for entry in submitted_clean:
submitted_id = entry.get("id")
description = entry["description"]
if is_temp_id(submitted_id):
obj = cls(description=description)
db.session.add(obj)
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:
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])
print(f"🗑️ Removing type ID {id_to_remove}")
return temp_id_map
for description in existing_descriptions - submitted:
db.session.delete(existing[description])
print(f"🗑️ Removing item: {description}")

View file

@ -6,6 +6,7 @@ from sqlalchemy import Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
from ..temp import is_temp_id
class RoomFunction(db.Model):
__tablename__ = 'Room Functions'
@ -28,31 +29,46 @@ class RoomFunction(db.Model):
}
@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()
}
def sync_from_state(cls, submitted_items: list[dict]) -> dict[str, int]:
submitted_clean = []
seen_ids = set()
temp_id_map = {}
existing_query = db.session.query(cls)
existing = {item.description: item for item in existing_query.all()}
for item in submitted_items:
if not isinstance(item, dict):
continue
description = str(item.get("name", "")).strip()
if not description:
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"])
existing_descriptions = set(existing.keys())
existing_by_id = {f.id: f for f in db.session.query(cls).all()}
existing_ids = set(existing_by_id.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}")
print(f"Existing function IDs: {existing_ids}")
print(f"Submitted function IDs: {seen_ids}")
for description in submitted - existing_descriptions:
db.session.add(cls(description=description))
print(f" Adding item: {description}")
for entry in submitted_clean:
submitted_id = entry.get("id")
description = entry["description"]
if is_temp_id(submitted_id):
obj = cls(description=description)
db.session.add(obj)
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:
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])
print(f"🗑️ Removing function ID {id_to_remove}")
return temp_id_map
for description in existing_descriptions - submitted:
db.session.delete(existing[description])
print(f"🗑️ Removing item: {description}")

View file

@ -46,59 +46,116 @@ class Room(db.Model):
}
@classmethod
def sync_from_state(cls, submitted_rooms: list[dict], section_map: dict, function_map: dict, section_fallbacks: list, function_fallbacks: list):
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.
Supports add, update, and delete.
"""
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
def resolve_fk(raw_id, fallback_list, id_map, label):
if not raw_id:
return None
index = abs(resolved + 1)
# 🛠 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
submitted_clean = []
seen_ids = set()
for room in submitted_rooms:
if not isinstance(room, dict):
continue
name = str(room.get("name", "")).strip()
if not name:
continue
rid = room.get("id")
section_id = room.get("section_id")
function_id = room.get("function_id")
submitted_clean.append({
"id": rid,
"name": name,
"section_id": section_id,
"function_id": function_id
})
if rid and not str(rid).startswith("room-"):
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
seen_ids.add(int(rid))
except ValueError:
pass # It's an invalid non-temp string
existing_query = db.session.query(cls)
existing = {room.name: room for room in existing_query.all()}
existing_by_id = {room.id: room for room in existing_query.all()}
existing_ids = set(existing_by_id.keys())
submitted = {
str(room.get("name", "")).strip(): room
for room in submitted_rooms
if isinstance(room, dict) and str(room.get("name", "")).strip()
}
print(f"Existing room IDs: {existing_ids}")
print(f"Submitted room IDs: {seen_ids}")
existing_names = set(existing.keys())
submitted_names = set(submitted.keys())
for entry in submitted_clean:
rid = entry.get("id")
name = entry["name"]
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}")
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")
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)
if not rid or str(rid).startswith("room-"):
new_room = cls(name=name, area_id=resolved_section_id, function_id=resolved_function_id)
db.session.add(new_room)
print(f" Adding room: {new_room}")
else:
try:
rid_int = int(rid)
except ValueError:
print(f"⚠️ Invalid room ID format: {rid}")
continue
room = existing_by_id.get(rid_int)
if not room:
print(f"⚠️ No matching room in DB for ID: {rid_int}")
continue
updated = False
if room.name != name:
print(f"✏️ Updating room name {room.id}: '{room.name}''{name}'")
room.name = name
updated = True
if room.area_id != resolved_section_id:
print(f"✏️ Updating room area {room.id}: {room.area_id}{resolved_section_id}")
room.area_id = resolved_section_id
updated = True
if room.function_id != resolved_function_id:
print(f"✏️ Updating room function {room.id}: {room.function_id}{resolved_function_id}")
room.function_id = resolved_function_id
updated = True
if not updated:
print(f"✅ No changes to room {room.id}")
for existing_id in existing_ids - seen_ids:
room = existing_by_id[existing_id]
db.session.delete(room)
print(f"🗑️ Removing room: {room.name}")
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

@ -412,21 +412,30 @@ def settings():
try:
with db.session.begin():
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", []))
# Sync each table and grab temp ID maps
brand_map = Brand.sync_from_state(state.get("brands", []))
type_map = Item.sync_from_state(state.get("types", []))
section_map = Area.sync_from_state(state.get("sections", []))
function_map = 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()}
# Fix up room foreign keys based on real IDs
submitted_rooms = []
for room in state.get("rooms", []):
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]
submitted_rooms.append(room)
Room.sync_from_state(
submitted_rooms=state.get("rooms", []),
submitted_rooms=submitted_rooms,
section_map=section_map,
function_map=function_map,
section_fallbacks=state.get("sections", []),
function_fallbacks=state.get("functions", [])
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()]
)
print("✅ COMMIT executed.")
@ -454,3 +463,4 @@ def settings():
functions=[f.serialize() for f in functions],
rooms=[r.serialize() for r in rooms],
)

View file

@ -1,10 +1,14 @@
const ComboBoxWidget = (() => {
let tempIdCounter = -1;
let tempIdCounter = 1;
function createTempId(prefix = "temp") {
return `${prefix}-${tempIdCounter++}`;
}
function createOption(text, value = null) {
const option = document.createElement('option');
option.textContent = text;
option.value = value ?? (tempIdCounter--);
option.value = value ?? createTempId();
return option;
}
@ -144,6 +148,7 @@ const ComboBoxWidget = (() => {
initComboBox,
createOption,
sortOptions,
handleComboAdd
handleComboAdd,
createTempId
};
})();

6
temp.py Normal file
View file

@ -0,0 +1,6 @@
def is_temp_id(val):
return (
val is None or
(isinstance(val, int) and val < 0) or
(isinstance(val, str) and val.startswith("temp-"))
)

View file

@ -59,7 +59,19 @@ submit_button=True
{% set room_editor %}
const roomEditor = new bootstrap.Modal(document.getElementById('roomEditor'));
const roomNameInput = document.getElementById('roomName');
roomNameInput.value = document.getElementById('room-input').value;
const input = document.getElementById('room-input');
const name = input.value.trim();
const existingOption = Array.from(document.getElementById('room-list').options)
.find(opt => opt.textContent.trim() === name);
roomNameInput.value = name;
document.getElementById('roomId').value = existingOption?.value ?? ''; // this will be the ID or temp ID
if (existingOption?.dataset.sectionId)
document.getElementById('roomSection').value = existingOption.dataset.sectionId;
if (existingOption?.dataset.functionId)
document.getElementById('roomFunction').value = existingOption.dataset.functionId;
roomEditor.show();
@ -71,7 +83,8 @@ submit_button=True
options=rooms,
label='Rooms',
placeholder='Add a new room',
onAdd=room_editor
onAdd=room_editor,
onEdit=room_editor
) }}
</div>
</div>
@ -89,6 +102,7 @@ submit_button=True
<div class="col">
<label for="roomName" class="form-label">Room Name</label>
<input type="text" class="form-input" id="roomName" placeholder="Enter room name">
<input type="hidden" id="roomId">
</div>
</div>
<div class="row">
@ -134,16 +148,20 @@ submit_button=True
function buildFormState() {
function extractOptions(id) {
const select = document.getElementById(`${id}-list`);
return Array.from(select.options).map(opt => ({ name: opt.textContent.trim(), id: parseInt(opt.value) || undefined }));
return Array.from(select.options).map(opt => ({
name: opt.textContent.trim(),
id: opt.value || undefined
}));
}
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 ? parseInt(data.sectionId) : null,
function_id: data.functionId ? parseInt(data.functionId) : null
section_id: data.sectionId ?? null,
function_id: data.functionId ?? null,
};
});
@ -192,43 +210,47 @@ submit_button=True
const name = document.getElementById('roomName').value.trim();
const sectionVal = document.getElementById('roomSection').value;
const funcVal = document.getElementById('roomFunction').value;
const section = sectionVal !== "" ? parseInt(sectionVal) : null;
const func = funcVal !== "" ? parseInt(funcVal) : null;
let idRaw = document.getElementById('roomId').value;
if (!name) {
alert('Please enter a room name.');
return;
}
// Avoid duplicate visible names
const roomList = document.getElementById('room-list');
const exists = Array.from(roomList.options).some(opt => opt.textContent.trim() === name);
if (exists) {
alert(`Room "${name}" already exists.`);
return;
let existingOption = Array.from(roomList.options).find(opt => opt.value === idRaw);
// If it's a brand new ID, generate one (string-based!)
if (!idRaw) {
idRaw = ComboBoxWidget.createTempId("room");
}
// Add to select box visibly
const option = ComboBoxWidget.createOption(name);
if (section !== null) {
option.dataset.sectionId = section;
}
if (func !== null) {
option.dataset.functionId = func;
if (!existingOption) {
existingOption = ComboBoxWidget.createOption(name, idRaw);
roomList.appendChild(existingOption);
}
roomList.appendChild(option);
existingOption.textContent = name;
existingOption.value = idRaw;
existingOption.dataset.sectionId = sectionVal;
existingOption.dataset.functionId = funcVal;
ComboBoxWidget.sortOptions(roomList);
// Track in state object
formState.rooms.push({
// Update formState.rooms
const index = formState.rooms.findIndex(r => r.id === idRaw);
const payload = {
id: idRaw,
name,
section_id: section,
function_id: func
});
section_id: sectionVal !== "" ? sectionVal : null,
function_id: funcVal !== "" ? funcVal : null
};
if (index >= 0) {
formState.rooms[index] = payload;
} else {
formState.rooms.push(payload);
}
bootstrap.Modal.getInstance(modal).hide();
});