Refactor model imports and add nullable indices for improved database performance and clarity

This commit is contained in:
Yaro Kasear 2025-07-17 15:29:14 -05:00
parent 704638d07a
commit dab23009c1
5 changed files with 43 additions and 43 deletions

View file

@ -2,9 +2,9 @@ from typing import Any, List, Optional, TYPE_CHECKING
from .image import Image from .image import Image
if TYPE_CHECKING: if TYPE_CHECKING:
from .brands import Brand from .brands import Brand
from .items import Item from .items import Item
from .work_log import WorkLog from .work_log import WorkLog
from .rooms import Room from .rooms import Room
from .image import Image from .image import Image
@ -20,21 +20,21 @@ class Inventory(db.Model, ImageAttachable):
__table_args__ = ( __table_args__ = (
Index('Inventory$Barcode', 'barcode'), Index('Inventory$Barcode', 'barcode'),
) )
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True) id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
timestamp: Mapped[datetime.datetime] = mapped_column(DateTime) timestamp: Mapped[datetime.datetime] = mapped_column(DateTime)
condition: Mapped[str] = mapped_column(Unicode(255)) condition: Mapped[str] = mapped_column(Unicode(255))
type_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("item.id"), nullable=True) type_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("item.id"), nullable=True, index=True)
name: Mapped[Optional[str]] = mapped_column(Unicode(255)) name: Mapped[Optional[str]] = mapped_column(Unicode(255))
serial: Mapped[Optional[str]] = mapped_column(Unicode(255)) serial: Mapped[Optional[str]] = mapped_column(Unicode(255))
model: Mapped[Optional[str]] = mapped_column(Unicode(255)) model: Mapped[Optional[str]] = mapped_column(Unicode(255))
notes: Mapped[Optional[str]] = mapped_column(Unicode(255)) notes: Mapped[Optional[str]] = mapped_column(Unicode(255))
owner_id = mapped_column(Integer, ForeignKey('users.id')) owner_id = mapped_column(Integer, ForeignKey('users.id'), nullable=True, index=True)
brand_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("brand.id")) brand_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("brand.id"), nullable=True, index=True)
location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("rooms.id")) location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("rooms.id"), nullable=True, index=True)
barcode: Mapped[Optional[str]] = mapped_column(Unicode(255)) barcode: Mapped[Optional[str]] = mapped_column(Unicode(255))
shared: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))')) shared: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id', ondelete='SET NULL'), nullable=True) image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id', ondelete='SET NULL'), nullable=True, index=True)
location: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory') location: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory')
owner = relationship('User', back_populates='inventory') owner = relationship('User', back_populates='inventory')
@ -91,7 +91,7 @@ class Inventory(db.Model, ImageAttachable):
return f"Serial: {self.serial}" return f"Serial: {self.serial}"
else: else:
return f"ID: {self.id}" return f"ID: {self.id}"
def serialize(self) -> dict[str, Any]: def serialize(self) -> dict[str, Any]:
return { return {
'id': self.id, 'id': self.id,
@ -108,11 +108,11 @@ class Inventory(db.Model, ImageAttachable):
'barcode': self.barcode, 'barcode': self.barcode,
'shared': self.shared 'shared': self.shared
} }
@classmethod @classmethod
def from_dict(cls, data: dict[str, Any]) -> "Inventory": def from_dict(cls, data: dict[str, Any]) -> "Inventory":
timestamp_str = data.get("timestamp") timestamp_str = data.get("timestamp")
return cls( return cls(
timestamp = datetime.datetime.fromisoformat(str(timestamp_str)) if timestamp_str else datetime.datetime.now(), timestamp = datetime.datetime.fromisoformat(str(timestamp_str)) if timestamp_str else datetime.datetime.now(),
condition=data.get("condition", "Unverified"), condition=data.get("condition", "Unverified"),

View file

@ -1,6 +1,6 @@
from typing import Optional, TYPE_CHECKING, List from typing import Optional, TYPE_CHECKING, List
if TYPE_CHECKING: if TYPE_CHECKING:
from .areas import Area from .areas import Area
from .room_functions import RoomFunction from .room_functions import RoomFunction
from .inventory import Inventory from .inventory import Inventory
from .users import User from .users import User
@ -17,9 +17,9 @@ class Room(ValidatableMixin, db.Model):
VALIDATION_LABEL = "Room" VALIDATION_LABEL = "Room"
id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True) id: Mapped[int] = mapped_column(Integer, Identity(start=1, increment=1), primary_key=True)
name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True) name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
area_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("area.id")) area_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("area.id"), nullable=True, index=True)
function_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("room_function.id")) function_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("room_function.id"), nullable=True, index=True)
area: Mapped[Optional['Area']] = relationship('Area', back_populates='rooms') area: Mapped[Optional['Area']] = relationship('Area', back_populates='rooms')
room_function: Mapped[Optional['RoomFunction']] = relationship('RoomFunction', back_populates='rooms') room_function: Mapped[Optional['RoomFunction']] = relationship('RoomFunction', back_populates='rooms')
@ -33,13 +33,13 @@ class Room(ValidatableMixin, db.Model):
def __repr__(self): def __repr__(self):
return f"<Room(id={self.id}, room={repr(self.name)}, area_id={self.area_id}, function_id={self.function_id})>" return f"<Room(id={self.id}, room={repr(self.name)}, area_id={self.area_id}, function_id={self.function_id})>"
@property @property
def full_name(self): def full_name(self):
name = self.name or "" name = self.name or ""
func = self.room_function.description if self.room_function else "" func = self.room_function.description if self.room_function else ""
return f"{name} - {func}".strip(" -") return f"{name} - {func}".strip(" -")
def serialize(self): def serialize(self):
return { return {
'id': self.id, 'id': self.id,
@ -146,18 +146,18 @@ class Room(ValidatableMixin, db.Model):
@classmethod @classmethod
def validate_state(cls, submitted_items: list[dict]) -> list[str]: def validate_state(cls, submitted_items: list[dict]) -> list[str]:
errors = [] errors = []
for index, item in enumerate(submitted_items): for index, item in enumerate(submitted_items):
label = f"Room #{index + 1}" label = f"Room #{index + 1}"
if not isinstance(item, dict): if not isinstance(item, dict):
errors.append(f"{label} is not a valid object.") errors.append(f"{label} is not a valid object.")
continue continue
name = item.get("name") name = item.get("name")
if not name or not str(name).strip(): if not name or not str(name).strip():
errors.append(f"{label} is missing a name.") errors.append(f"{label} is missing a name.")
raw_id = item.get("id") raw_id = item.get("id")
if raw_id is not None: if raw_id is not None:
try: try:
@ -165,19 +165,19 @@ class Room(ValidatableMixin, db.Model):
except (ValueError, TypeError): except (ValueError, TypeError):
if not str(raw_id).startswith("room-"): if not str(raw_id).startswith("room-"):
errors.append(f"{label} has an invalid ID: {raw_id}") errors.append(f"{label} has an invalid ID: {raw_id}")
# These fields are FK IDs, so we're just checking for valid formats here. # 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")]: for fk_field, fk_label in [("section_id", "Section"), ("function_id", "Function")]:
fk_val = item.get(fk_field) fk_val = item.get(fk_field)
if fk_val is None: if fk_val is None:
continue # Let the DB enforce nullability continue # Let the DB enforce nullability
try: try:
_ = int(fk_val) _ = int(fk_val)
except (ValueError, TypeError): except (ValueError, TypeError):
fk_val_str = str(fk_val) fk_val_str = str(fk_val)
if not fk_val_str.startswith("temp-"): if not fk_val_str.startswith("temp-"):
errors.append(f"{label} has invalid {fk_label} ID: {fk_val}") errors.append(f"{label} has invalid {fk_label} ID: {fk_val}")
return errors return errors

View file

@ -1,6 +1,6 @@
from typing import Any, List, Optional, TYPE_CHECKING from typing import Any, List, Optional, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from .inventory import Inventory from .inventory import Inventory
from .rooms import Room from .rooms import Room
from .work_log import WorkLog from .work_log import WorkLog
from .image import Image from .image import Image
@ -19,9 +19,9 @@ class User(db.Model, ImageAttachable):
active: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))')) active: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
last_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True) last_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
first_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True) first_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
location_id: Mapped[Optional[int]] = mapped_column(ForeignKey("rooms.id"), nullable=True) location_id: Mapped[Optional[int]] = mapped_column(ForeignKey("rooms.id"), nullable=True, index=True)
supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id")) supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True)
image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id', ondelete='SET NULL'), nullable=True) image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id', ondelete='SET NULL'), nullable=True, index=True)
supervisor: Mapped[Optional['User']] = relationship('User', remote_side='User.id', back_populates='subordinates') supervisor: Mapped[Optional['User']] = relationship('User', remote_side='User.id', back_populates='subordinates')
subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor') subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor')
@ -33,7 +33,7 @@ class User(db.Model, ImageAttachable):
@property @property
def full_name(self) -> str: def full_name(self) -> str:
return f"{self.first_name or ''} {self.last_name or ''}".strip() return f"{self.first_name or ''} {self.last_name or ''}".strip()
def __init__(self, first_name: Optional[str] = None, last_name: Optional[str] = None, def __init__(self, first_name: Optional[str] = None, last_name: Optional[str] = None,
location_id: Optional[int] = None, supervisor_id: Optional[int] = None, location_id: Optional[int] = None, supervisor_id: Optional[int] = None,
staff: Optional[bool] = False, active: Optional[bool] = False): staff: Optional[bool] = False, active: Optional[bool] = False):
@ -47,7 +47,7 @@ class User(db.Model, ImageAttachable):
def __repr__(self): def __repr__(self):
return f"<User(id={self.id}, first_name={repr(self.first_name)}, last_name={repr(self.last_name)}, " \ return f"<User(id={self.id}, first_name={repr(self.first_name)}, last_name={repr(self.last_name)}, " \
f"location={repr(self.location)}, staff={self.staff}, active={self.active})>" f"location={repr(self.location)}, staff={self.staff}, active={self.active})>"
def serialize(self): def serialize(self):
return { return {
'id': self.id, 'id': self.id,

View file

@ -1,6 +1,6 @@
from typing import Optional, Any, List, TYPE_CHECKING from typing import Optional, Any, List, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from .inventory import Inventory from .inventory import Inventory
from .image import Image from .image import Image
from .users import User from .users import User
from .work_note import WorkNote from .work_note import WorkNote
@ -23,15 +23,15 @@ class WorkLog(db.Model, ImageAttachable):
notes: Mapped[Optional[str]] = mapped_column(Unicode()) notes: Mapped[Optional[str]] = mapped_column(Unicode())
complete: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))')) complete: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
followup: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))')) followup: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
contact_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id")) contact_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True)
analysis: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))')) analysis: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
work_item_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("inventory.id")) work_item_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("inventory.id"), nullable=True, index=True)
work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs') work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs')
contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs') contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs')
updates: Mapped[List['WorkNote']] = relationship( updates: Mapped[List['WorkNote']] = relationship(
'WorkNote', 'WorkNote',
back_populates='work_log', back_populates='work_log',
cascade='all, delete-orphan', cascade='all, delete-orphan',
order_by='WorkNote.timestamp' order_by='WorkNote.timestamp'
) )
@ -58,7 +58,7 @@ class WorkLog(db.Model, ImageAttachable):
self.analysis = analysis self.analysis = analysis
self.work_item_id = work_item_id self.work_item_id = work_item_id
self.updates = updates or [] self.updates = updates or []
def __repr__(self): def __repr__(self):
return f"<WorkLog(id={self.id}, start_time={self.start_time}, end_time={self.end_time}, " \ return f"<WorkLog(id={self.id}, start_time={self.start_time}, end_time={self.end_time}, " \
f"notes={repr(self.notes)}, complete={self.complete}, followup={self.followup}, " \ f"notes={repr(self.notes)}, complete={self.complete}, followup={self.followup}, " \
@ -77,24 +77,24 @@ class WorkLog(db.Model, ImageAttachable):
'analysis': self.analysis, 'analysis': self.analysis,
'work_item_id': self.work_item_id 'work_item_id': self.work_item_id
} }
@classmethod @classmethod
def from_dict(cls, data: dict[str, Any]) -> "WorkLog": def from_dict(cls, data: dict[str, Any]) -> "WorkLog":
start_time_str = data.get("start_time") start_time_str = data.get("start_time")
end_time_str = data.get("end_time") end_time_str = data.get("end_time")
updates_raw = data.get("updates", []) updates_raw = data.get("updates", [])
updates: list[WorkNote] = [] updates: list[WorkNote] = []
for u in updates_raw: for u in updates_raw:
if isinstance(u, dict): if isinstance(u, dict):
content = u.get("content", "").strip() content = u.get("content", "").strip()
else: else:
content = str(u).strip() content = str(u).strip()
if content: if content:
updates.append(WorkNote(content=content)) updates.append(WorkNote(content=content))
return cls( return cls(
start_time=datetime.datetime.fromisoformat(str(start_time_str)) if start_time_str else datetime.datetime.now(), start_time=datetime.datetime.fromisoformat(str(start_time_str)) if start_time_str else datetime.datetime.now(),
end_time=datetime.datetime.fromisoformat(str(end_time_str)) if end_time_str else None, end_time=datetime.datetime.fromisoformat(str(end_time_str)) if end_time_str else None,

View file

@ -12,7 +12,7 @@ class WorkNote(db.Model):
) )
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
work_log_id: Mapped[int] = mapped_column(ForeignKey('work_log.id', ondelete='CASCADE'), nullable=False) work_log_id: Mapped[int] = mapped_column(ForeignKey('work_log.id', ondelete='CASCADE'), nullable=False, index=True)
timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, default=func.now(), server_default=func.now()) timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, default=func.now(), server_default=func.now())
content: Mapped[str] = mapped_column(UnicodeText, nullable=False) content: Mapped[str] = mapped_column(UnicodeText, nullable=False)
@ -25,7 +25,7 @@ class WorkNote(db.Model):
def __repr__(self) -> str: def __repr__(self) -> str:
preview = self.content[:30].replace("\n", " ") + "..." if len(self.content) > 30 else self.content preview = self.content[:30].replace("\n", " ") + "..." if len(self.content) > 30 else self.content
return f"<WorkNote(id={self.id}), log_id={self.work_log_id}, ts={self.timestamp}, content={preview!r}>" return f"<WorkNote(id={self.id}), log_id={self.work_log_id}, ts={self.timestamp}, content={preview!r}>"
def serialize(self) -> dict: def serialize(self) -> dict:
return { return {
'id': self.id, 'id': self.id,