Files
sproutly/backend/models.py
derekc 84e7b13575 Add last login tracking, batch date auto-fill, and bug fixes
- Track last_login_at on User model, updated on every successful login
- Show last login date in admin panel user table
- Fix admin/garden date display (datetime strings already contain T separator)
- Fix My Garden Internal Server Error (MySQL does not support NULLS LAST syntax)
- Fix Log Batch infinite loop when user has zero varieties
- Auto-fill batch dates from today when creating a new batch, calculated
  from selected variety's week offsets (germination, greenhouse, garden)
- Update README with new features and batch date auto-fill formula table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 00:48:04 -07:00

129 lines
4.6 KiB
Python

import enum
from sqlalchemy import Column, Integer, String, Date, Boolean, Text, DateTime, Enum, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from database import Base
class Category(str, enum.Enum):
vegetable = "vegetable"
herb = "herb"
flower = "flower"
fruit = "fruit"
class SunRequirement(str, enum.Enum):
full_sun = "full_sun"
part_shade = "part_shade"
full_shade = "full_shade"
class WaterNeeds(str, enum.Enum):
low = "low"
medium = "medium"
high = "high"
class BatchStatus(str, enum.Enum):
planned = "planned"
germinating = "germinating"
seedling = "seedling"
potted_up = "potted_up"
hardening = "hardening"
garden = "garden"
harvested = "harvested"
failed = "failed"
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
email = Column(String(255), unique=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
is_admin = Column(Boolean, default=False, nullable=False)
is_disabled = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime, server_default=func.now())
last_login_at = Column(DateTime, nullable=True)
varieties = relationship("Variety", back_populates="user", cascade="all, delete-orphan")
batches = relationship("Batch", back_populates="user", cascade="all, delete-orphan")
settings = relationship("Settings", back_populates="user", uselist=False, cascade="all, delete-orphan")
notification_logs = relationship("NotificationLog", back_populates="user", cascade="all, delete-orphan")
class Variety(Base):
__tablename__ = "varieties"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
name = Column(String(100), nullable=False)
variety_name = Column(String(100))
category = Column(Enum(Category), default=Category.vegetable)
weeks_to_start = Column(Integer)
weeks_to_greenhouse = Column(Integer)
weeks_to_garden = Column(Integer)
days_to_germinate = Column(Integer, default=7)
direct_sow_ok = Column(Boolean, default=False)
frost_tolerant = Column(Boolean, default=False)
sun_requirement = Column(Enum(SunRequirement), default=SunRequirement.full_sun)
water_needs = Column(Enum(WaterNeeds), default=WaterNeeds.medium)
color = Column(String(7), default="#52b788")
notes = Column(Text)
created_at = Column(DateTime, server_default=func.now())
user = relationship("User", back_populates="varieties")
batches = relationship("Batch", back_populates="variety", cascade="all, delete-orphan")
class Batch(Base):
__tablename__ = "batches"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
variety_id = Column(Integer, ForeignKey("varieties.id"), nullable=False)
label = Column(String(100))
quantity = Column(Integer, default=1)
sow_date = Column(Date)
germination_date = Column(Date)
greenhouse_date = Column(Date)
garden_date = Column(Date)
status = Column(Enum(BatchStatus), default=BatchStatus.planned)
notes = Column(Text)
created_at = Column(DateTime, server_default=func.now())
user = relationship("User", back_populates="batches")
variety = relationship("Variety", back_populates="batches")
class Settings(Base):
__tablename__ = "settings"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False)
last_frost_date = Column(Date)
first_frost_fall_date = Column(Date)
ntfy_topic = Column(String(200))
ntfy_server = Column(String(200), default="https://ntfy.sh")
notification_time = Column(String(5), default="07:00")
timezone = Column(String(50), default="UTC")
location_name = Column(String(100))
ntfy_username = Column(String(200))
ntfy_password = Column(String(200))
ntfy_api_key = Column(String(200))
user = relationship("User", back_populates="settings")
class NotificationLog(Base):
__tablename__ = "notification_log"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
sent_at = Column(DateTime, server_default=func.now())
message = Column(Text)
status = Column(String(20))
error = Column(Text)
user = relationship("User", back_populates="notification_logs")