Fix a lot of missing relationship information, plus some upstream CRUDKit changes.
This commit is contained in:
parent
8643d177ca
commit
cf56baabe2
11 changed files with 82 additions and 22 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -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)})>"
|
||||||
|
|
|
||||||
|
|
@ -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})>"
|
||||||
|
|
|
||||||
|
|
@ -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}"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)})>"
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue