Files
FabledScribe/src/fabledassistant/models/note.py
T
bvandeusen 95056d5be7 fix: rename Note.metadata → entity_meta (reserved by SQLAlchemy Declarative API)
SQLAlchemy reserves 'metadata' as a class attribute on declarative models.
Renamed to 'entity_meta' with explicit column name 'metadata' so the DB
column is unchanged but the Python attribute no longer conflicts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 18:13:47 -04:00

105 lines
4.1 KiB
Python

import enum
from datetime import date, datetime
from sqlalchemy import Date, DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, mapped_column
from fabledassistant.models import Base
from fabledassistant.models.base import TimestampMixin
class TaskStatus(str, enum.Enum):
todo = "todo"
in_progress = "in_progress"
done = "done"
cancelled = "cancelled"
class TaskPriority(str, enum.Enum):
none = "none"
low = "low"
medium = "medium"
high = "high"
class Note(Base, TimestampMixin):
__tablename__ = "notes"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True
)
title: Mapped[str] = mapped_column(Text, default="")
body: Mapped[str] = mapped_column(Text, default="")
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list)
parent_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
)
project_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("projects.id", ondelete="SET NULL"), nullable=True
)
milestone_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("milestones.id", ondelete="SET NULL"), nullable=True
)
status: Mapped[str | None] = mapped_column(Text, nullable=True)
priority: Mapped[str | None] = mapped_column(Text, nullable=True)
due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
recurrence_rule: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
recurrence_next_spawn_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Entity type — 'note' (default), 'person', 'place', 'list'
note_type: Mapped[str] = mapped_column(Text, default="note", server_default="note")
# Structured metadata for entity types (person/place/list)
# Named 'entity_meta' to avoid collision with SQLAlchemy's reserved 'metadata' attribute
entity_meta: Mapped[dict | None] = mapped_column("metadata", JSONB, nullable=True)
__table_args__ = (
Index("ix_notes_tags", "tags", postgresql_using="gin"),
Index("ix_notes_status", "status"),
Index("ix_notes_title", "title"),
Index("ix_notes_user_id", "user_id"),
Index("ix_notes_project_id", "project_id"),
Index("ix_notes_milestone_id", "milestone_id"),
Index("ix_notes_note_type", "note_type"),
)
@property
def is_task(self) -> bool:
return self.status is not None
@property
def entity_type(self) -> str:
"""Normalised type: 'note', 'person', 'place', or 'list'."""
return self.note_type or "note"
def to_dict(self) -> dict:
return {
"id": self.id,
"title": self.title,
"body": self.body,
"tags": self.tags or [],
"parent_id": self.parent_id,
"project_id": self.project_id,
"milestone_id": self.milestone_id,
"status": self.status,
"priority": self.priority,
"due_date": self.due_date.isoformat() if self.due_date else None,
"started_at": self.started_at.isoformat() if self.started_at else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"recurrence_rule": self.recurrence_rule,
"recurrence_next_spawn_at": (
self.recurrence_next_spawn_at.isoformat()
if self.recurrence_next_spawn_at
else None
),
"is_task": self.is_task,
"note_type": self.entity_type,
"metadata": self.entity_meta or {},
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}