feat(backend): add Item model and stage/type validation
This commit is contained in:
parent
337c2a60d1
commit
c3e9f2551c
9 changed files with 122 additions and 0 deletions
11
backend/.gitignore
vendored
Normal file
11
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.db
|
||||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
19
backend/app/db.py
Normal file
19
backend/app/db.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine(db_url: str):
|
||||||
|
connect_args = {"check_same_thread": False} if db_url.startswith("sqlite") else {}
|
||||||
|
return create_engine(db_url, connect_args=connect_args)
|
||||||
|
|
||||||
|
|
||||||
|
def make_session_factory(engine):
|
||||||
|
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||||
|
|
||||||
|
|
||||||
|
def init_db(engine):
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
23
backend/app/models.py
Normal file
23
backend/app/models.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from datetime import datetime, date
|
||||||
|
from sqlalchemy import String, Text, DateTime, Date, Integer
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from app.db import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Item(Base):
|
||||||
|
__tablename__ = "items"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
modality: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
raw_content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
captured_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||||
|
sender_id: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||||
|
type: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||||
|
stage: Mapped[str] = mapped_column(String, nullable=False, default="new")
|
||||||
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
follow_up_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
discord_message_id: Mapped[str | None] = mapped_column(String, unique=True, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||||
|
)
|
||||||
14
backend/app/stages.py
Normal file
14
backend/app/stages.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
DONATION_STAGES = ["new", "replied", "pickup_scheduled", "picked_up", "logged_in_bookmark"]
|
||||||
|
OTHER_STAGES = ["new", "done"]
|
||||||
|
|
||||||
|
|
||||||
|
def valid_stages_for(item_type: str | None) -> list[str]:
|
||||||
|
if item_type == "donation":
|
||||||
|
return DONATION_STAGES
|
||||||
|
if item_type == "other":
|
||||||
|
return OTHER_STAGES
|
||||||
|
return ["new"]
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_stage(item_type: str | None, stage: str) -> bool:
|
||||||
|
return stage in valid_stages_for(item_type)
|
||||||
6
backend/requirements.txt
Normal file
6
backend/requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
fastapi==0.115.0
|
||||||
|
uvicorn[standard]==0.30.6
|
||||||
|
sqlalchemy==2.0.35
|
||||||
|
pydantic==2.9.2
|
||||||
|
pytest==8.3.3
|
||||||
|
httpx==0.27.2
|
||||||
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
14
backend/tests/conftest.py
Normal file
14
backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import pytest
|
||||||
|
from app.db import get_engine, make_session_factory, init_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def db_session():
|
||||||
|
engine = get_engine("sqlite:///:memory:")
|
||||||
|
init_db(engine)
|
||||||
|
SessionLocal = make_session_factory(engine)
|
||||||
|
session = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
35
backend/tests/test_stages.py
Normal file
35
backend/tests/test_stages.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from app import stages
|
||||||
|
|
||||||
|
|
||||||
|
def test_donation_stages_in_order():
|
||||||
|
assert stages.DONATION_STAGES == [
|
||||||
|
"new", "replied", "pickup_scheduled", "picked_up", "logged_in_bookmark",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_other_stages_in_order():
|
||||||
|
assert stages.OTHER_STAGES == ["new", "done"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_stages_for_donation():
|
||||||
|
assert stages.valid_stages_for("donation") == stages.DONATION_STAGES
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_stages_for_other():
|
||||||
|
assert stages.valid_stages_for("other") == stages.OTHER_STAGES
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_stages_for_unset_type_defaults_to_new_only():
|
||||||
|
assert stages.valid_stages_for(None) == ["new"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_valid_stage_true_for_matching_type():
|
||||||
|
assert stages.is_valid_stage("donation", "pickup_scheduled") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_valid_stage_false_for_mismatched_type():
|
||||||
|
assert stages.is_valid_stage("other", "pickup_scheduled") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_valid_stage_false_for_unknown_value():
|
||||||
|
assert stages.is_valid_stage("donation", "archived") is False
|
||||||
Loading…
Reference in a new issue