From cf56baabe2f3e19c5727dc9d20f8c38308b3577f Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Fri, 5 Sep 2025 09:45:22 -0500 Subject: [PATCH] Fix a lot of missing relationship information, plus some upstream CRUDKit changes. --- crudkit/core/base.py | 8 +++++++- crudkit/core/service.py | 17 +++++++++++++---- inventory/__init__.py | 20 +++++++++++++++++++- inventory/models/__init__.py | 16 ++++++++++++++++ inventory/models/brand.py | 2 +- inventory/models/image.py | 4 ++-- inventory/models/inventory.py | 12 +++++++----- inventory/models/room.py | 5 ++++- inventory/models/user.py | 15 ++++++++++----- inventory/models/work_log.py | 2 +- inventory/models/work_note.py | 3 ++- 11 files changed, 82 insertions(+), 22 deletions(-) diff --git a/crudkit/core/base.py b/crudkit/core/base.py index c7de93e..0540ec0 100644 --- a/crudkit/core/base.py +++ b/crudkit/core/base.py @@ -10,7 +10,13 @@ class CRUDMixin: updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) def as_dict(self): - return {c.name: getattr(self, c.name) for c in self.__table__.columns} + # Combine all columns from all inherited tables + result = {} + for cls in self.__class__.__mro__: + if hasattr(cls, "__table__"): + for column in cls.__table__.columns: + result[column.name] = getattr(self, column.name) + return result class Version(Base): __tablename__ = "versions" diff --git a/crudkit/core/service.py b/crudkit/core/service.py index 5126a9b..a7da4e5 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -1,5 +1,5 @@ from typing import Type, TypeVar, Generic -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, with_polymorphic from crudkit.core.base import Version from crudkit.core.spec import CRUDSpec @@ -9,13 +9,22 @@ def _is_truthy(val): return str(val).lower() in ('1', 'true', 'yes', 'on') class CRUDService(Generic[T]): - def __init__(self, model: Type[T], session: Session): + def __init__(self, model: Type[T], session: Session, polymorphic: bool = False): self.model = model self.session = session + self.polymorphic = polymorphic self.supports_soft_delete = hasattr(model, 'is_deleted') + def get_query(self): + if self.polymorphic: + poly_model = with_polymorphic(self.model, '*') + return self.session.query(poly_model) + else: + base_only = with_polymorphic(self.model, [], flat=True) + return self.session.query(base_only) + def get(self, id: int, include_deleted: bool = False) -> T | None: - obj = self.session.get(self.model, id) + obj = self.get_query().filter_by(id=id).first() if obj is None: return None if self.supports_soft_delete and not include_deleted and obj.is_deleted: @@ -23,7 +32,7 @@ class CRUDService(Generic[T]): return obj def list(self, params=None) -> list[T]: - query = self.session.query(self.model) + query = self.get_query() if params: if self.supports_soft_delete: diff --git a/inventory/__init__.py b/inventory/__init__.py index cfb1f17..2f5ed3b 100644 --- a/inventory/__init__.py +++ b/inventory/__init__.py @@ -3,7 +3,10 @@ import os from flask import Flask -from .db import init_db, create_all_tables +from crudkit.api.flask_api import generate_crud_blueprint +from crudkit.core.service import CRUDService + +from .db import init_db, create_all_tables, get_session def create_app() -> Flask: app = Flask(__name__) @@ -16,6 +19,21 @@ def create_app() -> Flask: create_all_tables() + session = get_session() + + area_service = CRUDService(_models.Area, session) + brand_service = CRUDService(_models.Brand, session) + device_type_service = CRUDService(_models.DeviceType, session) + image_service = CRUDService(_models.Image, session) + inventory_service = CRUDService(_models.Inventory, session) + room_function_service = CRUDService(_models.RoomFunction, session) + room_service = CRUDService(_models.Room, session) + user_service = CRUDService(_models.User, session) + work_log_service = CRUDService(_models.WorkLog, session) + work_note_service = CRUDService(_models.WorkNote, session) + + app.register_blueprint(generate_crud_blueprint(_models.Area, area_service), url_prefix="/api/area") + @app.get("/") def index(): return {"status": "ok"} diff --git a/inventory/models/__init__.py b/inventory/models/__init__.py index f522e87..4f197bd 100644 --- a/inventory/models/__init__.py +++ b/inventory/models/__init__.py @@ -4,3 +4,19 @@ from sqlalchemy import Integer, Unicode from sqlalchemy.orm import Mapped, mapped_column from crudkit.core.base import Base, CRUDMixin + +from .area import Area +from .brand import Brand +from .device_type import DeviceType +from .image import Image +from .inventory import Inventory +from .room_function import RoomFunction +from .room import Room +from .user import User +from .work_log import WorkLog +from .work_note import WorkNote + +__all__ = [ + "Area", "Brand", "DeviceType", "Image", "Inventory", + "RoomFunction", "Room", "User", "WorkLog", "WorkNote", +] diff --git a/inventory/models/brand.py b/inventory/models/brand.py index 6d2528d..9314a13 100644 --- a/inventory/models/brand.py +++ b/inventory/models/brand.py @@ -15,4 +15,4 @@ class Brand(Base, CRUDMixin): is_deleted: Mapped[Boolean] = mapped_column(Boolean, nullable=False, default=False) def __repr__(self) -> str: - return f""4 + return f"" diff --git a/inventory/models/image.py b/inventory/models/image.py index 2c636de..be7891b 100644 --- a/inventory/models/image.py +++ b/inventory/models/image.py @@ -12,9 +12,9 @@ class Image(Base, CRUDMixin): caption: Mapped[str] = mapped_column(Unicode(255), default="") timestamp: Mapped[DateTime] = mapped_column(DateTime, default=func.now(), server_default=func.now()) - inventory: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='images') + inventory: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='image') user: Mapped[Optional['User']] = relationship('User', back_populates='image') - worklogs: Mapped[List['WorkLog']] = relationship('WorkLog', secondary=worklog_images, back_populates='images') + # worklogs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='images') def __repr__(self): return f"" diff --git a/inventory/models/inventory.py b/inventory/models/inventory.py index 43ea25c..62dd65b 100644 --- a/inventory/models/inventory.py +++ b/inventory/models/inventory.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, Unicode from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -15,11 +15,11 @@ class Inventory(Base, CRUDMixin): condition: Mapped[str] = mapped_column(Unicode(255)) model: Mapped[Optional[str]] = mapped_column(Unicode(255)) notes: Mapped[Optional[str]] = mapped_column(Unicode(255)) - shared = Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + shared: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) timestamp: Mapped[DateTime] = mapped_column(DateTime) brand: Mapped[Optional['Brand']] = relationship('Brand', back_populates='inventory') - brand_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('users.id'), nullable=True, index=True) + brand_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('brand.id'), nullable=True, index=True) device_type: Mapped[Optional['DeviceType']] = relationship('DeviceType', back_populates='inventory') device_type_id: Mapped[Optional[int]] = mapped_column('type_id', Integer, ForeignKey("item.id"), nullable=True, index=True) @@ -27,12 +27,14 @@ class Inventory(Base, CRUDMixin): image: Mapped[Optional['Image']] = relationship('Image', back_populates='inventory', passive_deletes=True) image_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('images.id', ondelete='SET NULL'), nullable=True, index=True) - location = Mapped[Optional['Room']] = relationship('Room', back_populates='inventory') - location_id = Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('rooms.id'), nullable=True, index=True) + location: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory') + location_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('rooms.id'), nullable=True, index=True) owner: Mapped[Optional['User']] = relationship('User', back_populates='inventory') owner_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True) + work_logs: Mapped[Optional[List['WorkLog']]] = relationship('WorkLog', back_populates='work_item') + def __repr__(self): parts = [f"id={self.id}"] diff --git a/inventory/models/room.py b/inventory/models/room.py index d83937a..a74f31d 100644 --- a/inventory/models/room.py +++ b/inventory/models/room.py @@ -1,10 +1,13 @@ -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from sqlalchemy import ForeignKey, Integer, Unicode from sqlalchemy.orm import Mapped, mapped_column, relationship from crudkit.core.base import Base, CRUDMixin +if TYPE_CHECKING: + from .user import User + class Room(Base, CRUDMixin): __tablename__ = 'rooms' diff --git a/inventory/models/user.py b/inventory/models/user.py index 99e6efe..cfda439 100644 --- a/inventory/models/user.py +++ b/inventory/models/user.py @@ -1,10 +1,13 @@ -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from sqlalchemy import Boolean, Integer, ForeignKey, Unicode from sqlalchemy.orm import Mapped, mapped_column, relationship from crudkit.core.base import Base, CRUDMixin +if TYPE_CHECKING: + from .room import Room + class User(Base, CRUDMixin): __tablename__ = 'users' @@ -20,12 +23,14 @@ class User(Base, CRUDMixin): inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='owner') - location: Mapped[Optional['Room']] = relationship('Room', back_populates='owner') - location_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True) + location: Mapped[Optional['Room']] = relationship('Room', back_populates='users') + location_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("rooms.id"), nullable=True, index=True) - supervisor: Mapped[Optional['User']] = relationship('User', back_populates='subordinates') - supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("user.id"), nullable=True, index=True) + supervisor: Mapped[Optional['User']] = relationship('User', back_populates='subordinates', remote_side='User.id') + supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True) subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor') + work_logs: Mapped[Optional[List['WorkLog']]] = relationship('WorkLog', back_populates='contact') + def __repr__(self): return f"" diff --git a/inventory/models/work_log.py b/inventory/models/work_log.py index d33d4cd..91b3cc2 100644 --- a/inventory/models/work_log.py +++ b/inventory/models/work_log.py @@ -14,7 +14,7 @@ class WorkLog(Base, CRUDMixin): complete: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=False, default=False) contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs') - contact_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("user.id"), nullable=True, index=True) + contact_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True) updates: Mapped[List['WorkNote']] = relationship('WorkNote', back_populates='work_log', cascade='all, delete-orphan') diff --git a/inventory/models/work_note.py b/inventory/models/work_note.py index 17609d9..cb723ef 100644 --- a/inventory/models/work_note.py +++ b/inventory/models/work_note.py @@ -1,4 +1,4 @@ -from sqlalchemy import DateTime, UnicodeText, func +from sqlalchemy import DateTime, ForeignKey, Integer, UnicodeText, func from sqlalchemy.orm import Mapped, mapped_column, relationship from crudkit.core.base import Base, CRUDMixin @@ -10,6 +10,7 @@ class WorkNote(Base, CRUDMixin): timestamp: Mapped[DateTime] = mapped_column(DateTime, default=func.now(), server_default=func.now()) work_log: Mapped['WorkLog'] = relationship('WorkLog', back_populates='updates') + work_log_id: Mapped[int] = mapped_column(Integer, ForeignKey('work_log.id')) def __repr__(self) -> str: preview = self.content[:30].replace("\n", " ") + "..." if len(self.content) > 30 else self.content