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