Fix a lot of missing relationship information, plus some upstream CRUDKit changes.

This commit is contained in:
Yaro Kasear 2025-09-05 09:45:22 -05:00
parent 8643d177ca
commit cf56baabe2
11 changed files with 82 additions and 22 deletions

View file

@ -10,7 +10,13 @@ class CRUDMixin:
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
def as_dict(self): 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): class Version(Base):
__tablename__ = "versions" __tablename__ = "versions"

View file

@ -1,5 +1,5 @@
from typing import Type, TypeVar, Generic 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.base import Version
from crudkit.core.spec import CRUDSpec from crudkit.core.spec import CRUDSpec
@ -9,13 +9,22 @@ def _is_truthy(val):
return str(val).lower() in ('1', 'true', 'yes', 'on') return str(val).lower() in ('1', 'true', 'yes', 'on')
class CRUDService(Generic[T]): 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.model = model
self.session = session self.session = session
self.polymorphic = polymorphic
self.supports_soft_delete = hasattr(model, 'is_deleted') 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: 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: if obj is None:
return None return None
if self.supports_soft_delete and not include_deleted and obj.is_deleted: if self.supports_soft_delete and not include_deleted and obj.is_deleted:
@ -23,7 +32,7 @@ class CRUDService(Generic[T]):
return obj return obj
def list(self, params=None) -> list[T]: def list(self, params=None) -> list[T]:
query = self.session.query(self.model) query = self.get_query()
if params: if params:
if self.supports_soft_delete: if self.supports_soft_delete:

View file

@ -3,7 +3,10 @@ import os
from flask import Flask 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: def create_app() -> Flask:
app = Flask(__name__) app = Flask(__name__)
@ -16,6 +19,21 @@ def create_app() -> Flask:
create_all_tables() 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("/") @app.get("/")
def index(): def index():
return {"status": "ok"} return {"status": "ok"}

View file

@ -4,3 +4,19 @@ from sqlalchemy import Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from crudkit.core.base import Base, CRUDMixin 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",
]

View file

@ -15,4 +15,4 @@ class Brand(Base, CRUDMixin):
is_deleted: Mapped[Boolean] = mapped_column(Boolean, nullable=False, default=False) is_deleted: Mapped[Boolean] = mapped_column(Boolean, nullable=False, default=False)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Brand(id={self.id}, name={repr(self.name)})>"4 return f"<Brand(id={self.id}, name={repr(self.name)})>"

View file

@ -12,9 +12,9 @@ class Image(Base, CRUDMixin):
caption: Mapped[str] = mapped_column(Unicode(255), default="") caption: Mapped[str] = mapped_column(Unicode(255), default="")
timestamp: Mapped[DateTime] = mapped_column(DateTime, default=func.now(), server_default=func.now()) 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') 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): def __repr__(self):
return f"<Image(id={self.id}, filename={self.filename})>" return f"<Image(id={self.id}, filename={self.filename})>"

View file

@ -1,4 +1,4 @@
from typing import Optional from typing import List, Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, Unicode from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@ -15,11 +15,11 @@ class Inventory(Base, CRUDMixin):
condition: Mapped[str] = mapped_column(Unicode(255)) condition: Mapped[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))
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) timestamp: Mapped[DateTime] = mapped_column(DateTime)
brand: Mapped[Optional['Brand']] = relationship('Brand', back_populates='inventory') 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: 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) 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: 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) 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: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory')
location_id = Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('rooms.id'), nullable=True, index=True) 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: Mapped[Optional['User']] = relationship('User', back_populates='inventory')
owner_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True) 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): def __repr__(self):
parts = [f"id={self.id}"] parts = [f"id={self.id}"]

View file

@ -1,10 +1,13 @@
from typing import List, Optional from typing import List, Optional, TYPE_CHECKING
from sqlalchemy import ForeignKey, Integer, Unicode from sqlalchemy import ForeignKey, Integer, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from crudkit.core.base import Base, CRUDMixin from crudkit.core.base import Base, CRUDMixin
if TYPE_CHECKING:
from .user import User
class Room(Base, CRUDMixin): class Room(Base, CRUDMixin):
__tablename__ = 'rooms' __tablename__ = 'rooms'

View file

@ -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 import Boolean, Integer, ForeignKey, Unicode
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from crudkit.core.base import Base, CRUDMixin from crudkit.core.base import Base, CRUDMixin
if TYPE_CHECKING:
from .room import Room
class User(Base, CRUDMixin): class User(Base, CRUDMixin):
__tablename__ = 'users' __tablename__ = 'users'
@ -20,12 +23,14 @@ class User(Base, CRUDMixin):
inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='owner') inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='owner')
location: Mapped[Optional['Room']] = relationship('Room', back_populates='owner') location: Mapped[Optional['Room']] = relationship('Room', back_populates='users')
location_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True) 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: Mapped[Optional['User']] = relationship('User', back_populates='subordinates', remote_side='User.id')
supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("user.id"), nullable=True, index=True) supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True)
subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor') subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor')
work_logs: Mapped[Optional[List['WorkLog']]] = relationship('WorkLog', back_populates='contact')
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)})>"

View file

@ -14,7 +14,7 @@ class WorkLog(Base, CRUDMixin):
complete: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=False, default=False) complete: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=False, default=False)
contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs') 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') updates: Mapped[List['WorkNote']] = relationship('WorkNote', back_populates='work_log', cascade='all, delete-orphan')

View file

@ -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 sqlalchemy.orm import Mapped, mapped_column, relationship
from crudkit.core.base import Base, CRUDMixin 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()) timestamp: Mapped[DateTime] = mapped_column(DateTime, default=func.now(), server_default=func.now())
work_log: Mapped['WorkLog'] = relationship('WorkLog', back_populates='updates') 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: 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