inventory/inventory/models/rooms.py

199 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Optional, TYPE_CHECKING, List
if TYPE_CHECKING:
from .areas import Area
from .room_functions import RoomFunction
from .inventory import Inventory
from .users import User
from sqlalchemy import ForeignKey, Identity, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship
from . import db
from ..utils.validation import ValidatableMixin
class Room(ValidatableMixin, db.Model):
__tablename__ = 'Rooms'
VALIDATION_LABEL = "Room"
id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True)
name: Mapped[Optional[str]] = mapped_column("Room", Unicode(255), nullable=True)
area_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("Areas.id"))
function_id: Mapped[Optional[int]] = mapped_column("Function", Integer, ForeignKey("Room Functions.ID"))
area: Mapped[Optional['Area']] = relationship('Area', back_populates='rooms')
room_function: Mapped[Optional['RoomFunction']] = relationship('RoomFunction', back_populates='rooms')
inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='location')
users: Mapped[List['User']] = relationship('User', back_populates='location')
def __init__(self, name: Optional[str] = None, area_id: Optional[int] = None, function_id: Optional[int] = None):
self.name = name
self.area_id = area_id
self.function_id = function_id
def __repr__(self):
return f"<Room(id={self.id}, room={repr(self.name)}, area_id={self.area_id}, function_id={self.function_id})>"
@property
def full_name(self):
name = self.name or ""
func = self.room_function.description if self.room_function else ""
return f"{name} - {func}".strip(" -")
def serialize(self):
return {
'id': self.id,
'name': self.name,
'area_id': self.area_id,
'function_id': self.function_id
}
@classmethod
def sync_from_state(cls, submitted_rooms: list[dict], section_map: dict[str, int], function_map: dict[str, int]) -> None:
"""
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(key, fk_map, label):
print(f"Resolving {label} ID: {key} using map: {fk_map}")
if key is None:
return None
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()
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:
seen_ids.add(int(rid))
except ValueError:
pass # Not valid? Not seen.
existing_query = db.session.query(cls)
existing_by_id = {room.id: room for room in existing_query.all()}
existing_ids = set(existing_by_id.keys())
print(f"Existing room IDs: {existing_ids}")
print(f"Submitted room IDs: {seen_ids}")
for entry in submitted_clean:
rid = entry.get("id")
name = entry["name"]
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)
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.get(existing_id)
if not room:
continue
# Skip if a newly added room matches this one — likely duplicate
if any(
r["name"] == room.name and
resolve_fk(r["section_id"], section_map, "section") == room.area_id and
resolve_fk(r["function_id"], function_map, "function") == room.function_id
for r in submitted_clean
if r.get("id") is None or str(r.get("id")).startswith("room-")
):
print(f"⚠️ Skipping deletion of likely duplicate: {room}")
continue
db.session.delete(room)
print(f"🗑️ Removing room: {room}")
@classmethod
def validate_state(cls, submitted_items: list[dict]) -> list[str]:
print("VALIDATING")
errors = []
for index, item in enumerate(submitted_items):
label = f"Room #{index + 1}"
if not isinstance(item, dict):
errors.append(f"{label} is not a valid object.")
continue
name = item.get("name")
if not name or not str(name).strip():
errors.append(f"{label} is missing a name.")
raw_id = item.get("id")
if raw_id is not None:
try:
_ = int(raw_id)
except (ValueError, TypeError):
if not str(raw_id).startswith("room-"):
errors.append(f"{label} has an invalid ID: {raw_id}")
# These fields are FK IDs, so we're just checking for valid formats here.
for fk_field, fk_label in [("section_id", "Section"), ("function_id", "Function")]:
fk_val = item.get(fk_field)
if fk_val is None:
continue # Let the DB enforce nullability
try:
_ = int(fk_val)
except (ValueError, TypeError):
fk_val_str = str(fk_val)
if not fk_val_str.startswith("temp-"):
errors.append(f"{label} has invalid {fk_label} ID: {fk_val}")
return errors