refactor(models): one iso() for every to_dict timestamp; mixins replace hand-rolled created_at/updated_at (#2827, milestone 296 area 3)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 26s

Reading all 28 models against each other: 54 `x.isoformat() if x else None` /
`x.isoformat()` expressions in 23 to_dict methods, in two guarded/unguarded
wordings, become iso() from models/base.py — uniform, and a row read before
flush serialises as null instead of raising. Rulebook / RulebookTopic / Rule
carried byte-identical copies of TimestampMixin's two columns;
InvitationToken / PasswordResetToken / NoteUsageEvent carried CreatedAtMixin's —
all six now use the mixin. AppLog and RetrievalLog keep their explicit
created_at, commented: their composite index orders on `created_at.desc()`,
which needs the column object in the class body. Schema-neutral (same column
definitions) — no migration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 11:17:42 -04:00
co-authored by Claude Fable 5
parent 848ce1592e
commit b0eda32575
24 changed files with 101 additions and 119 deletions
+4 -4
View File
@@ -4,7 +4,7 @@ from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from scribe.models.base import CreatedAtMixin, iso
class ApiKey(Base, CreatedAtMixin):
@@ -36,7 +36,7 @@ class ApiKey(Base, CreatedAtMixin):
"name": self.name,
"key_prefix": self.key_prefix,
"scope": self.scope,
"last_used_at": self.last_used_at.isoformat() if self.last_used_at else None,
"created_at": self.created_at.isoformat(),
"revoked_at": self.revoked_at.isoformat() if self.revoked_at else None,
"last_used_at": iso(self.last_used_at),
"created_at": iso(self.created_at),
"revoked_at": iso(self.revoked_at),
}
+5 -1
View File
@@ -4,6 +4,7 @@ from sqlalchemy import DateTime, Float, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import iso
class AppLog(Base):
@@ -20,6 +21,9 @@ class AppLog(Base):
duration_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
ip_address: Mapped[str | None] = mapped_column(Text, nullable=True)
details: Mapped[str | None] = mapped_column(Text, nullable=True)
# Declared here rather than via CreatedAtMixin on purpose: the composite
# index below orders on `created_at.desc()`, which needs the column object
# in this class body — a mixin's column is not in scope there.
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
@@ -44,5 +48,5 @@ class AppLog(Base):
"duration_ms": self.duration_ms,
"ip_address": self.ip_address,
"details": self.details,
"created_at": self.created_at.isoformat() if self.created_at else None,
"created_at": iso(self.created_at),
}
+11 -1
View File
@@ -1,9 +1,19 @@
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from sqlalchemy import DateTime, Text
from sqlalchemy.orm import Mapped, mapped_column
def iso(value: datetime | date | None) -> str | None:
"""ISO-8601 for a payload, None for an unset column.
Every model's to_dict serialises timestamps through this one helper so a
row read before flush (created_at still None) and a nullable column both
come out as null instead of raising on `.isoformat()`.
"""
return value.isoformat() if value else None
class SoftDeleteMixin:
"""Recoverable-delete columns. NULL deleted_at = live row. deleted_batch_id
groups rows soft-deleted in one operation so a cascade restores as a unit."""
+7 -7
View File
@@ -13,7 +13,7 @@ from sqlalchemy import (
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin
from scribe.models.base import TimestampMixin, iso
# The classification vocabulary (note 2786). `unclassified` is the default and
# THE todo state; every other status is a judgment, stamped with who made it.
@@ -153,18 +153,18 @@ class CodeShape(Base, TimestampMixin):
"snippet_id": self.snippet_id,
"reason": self.reason,
"classified_by": self.classified_by,
"classified_at": self.classified_at.isoformat() if self.classified_at else None,
"classified_at": iso(self.classified_at),
"first_seen_commit": self.first_seen_commit,
"last_seen_commit": self.last_seen_commit,
"vanished_at": self.vanished_at.isoformat() if self.vanished_at else None,
"vanished_at": iso(self.vanished_at),
"signature": self.signature,
"body_sha": self.body_sha,
"proposal": self.proposal,
"classified_sha": self.classified_sha,
"recheck_at": self.recheck_at.isoformat() if self.recheck_at else None,
"recheck_at": iso(self.recheck_at),
"diverges_from": self.diverges_from,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
@@ -221,5 +221,5 @@ class CodeShapeEvent(Base):
"classified_by": self.classified_by,
"reason": self.reason,
"commit": self.commit,
"at": self.at.isoformat(),
"at": iso(self.at),
}
+5 -5
View File
@@ -20,7 +20,7 @@ from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
@@ -56,8 +56,8 @@ class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
"description": self.description or "",
"guidance": self.guidance or "",
"parent_id": self.parent_id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
@@ -153,6 +153,6 @@ class DesignToken(Base, TimestampMixin, SoftDeleteMixin):
"rationale": self.rationale,
"supersedes": self.supersedes or [],
"order_index": self.order_index,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+3 -3
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin
from scribe.models.base import TimestampMixin, iso
class ForgeConnection(Base, TimestampMixin):
@@ -40,6 +40,6 @@ class ForgeConnection(Base, TimestampMixin):
"kind": self.kind,
"base_url": self.base_url,
"host": self.host,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+4 -4
View File
@@ -4,7 +4,7 @@ from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, TimestampMixin
from scribe.models.base import CreatedAtMixin, TimestampMixin, iso
class Group(Base, TimestampMixin):
@@ -27,8 +27,8 @@ class Group(Base, TimestampMixin):
"name": self.name,
"description": self.description,
"created_by": self.created_by,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
@@ -53,5 +53,5 @@ class GroupMembership(Base, CreatedAtMixin):
"group_id": self.group_id,
"user_id": self.user_id,
"role": self.role,
"created_at": self.created_at.isoformat(),
"created_at": iso(self.created_at),
}
+2 -4
View File
@@ -4,9 +4,10 @@ from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
class InvitationToken(Base):
class InvitationToken(Base, CreatedAtMixin):
__tablename__ = "invitation_tokens"
id: Mapped[int] = mapped_column(primary_key=True)
@@ -15,9 +16,6 @@ class InvitationToken(Base):
invited_by: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
used: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
__table_args__ = (
Index("ix_invitation_tokens_token_hash", "token_hash"),
+3 -3
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, SoftDeleteMixin
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
class Milestone(Base, TimestampMixin, SoftDeleteMixin):
@@ -30,6 +30,6 @@ class Milestone(Base, TimestampMixin, SoftDeleteMixin):
"body": self.body,
"status": self.status,
"order_index": self.order_index,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+7 -11
View File
@@ -6,7 +6,7 @@ from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, SoftDeleteMixin
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
class TaskStatus(str, enum.Enum):
@@ -105,18 +105,14 @@ class Note(Base, TimestampMixin, SoftDeleteMixin):
"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,
"due_date": iso(self.due_date),
"started_at": iso(self.started_at),
"completed_at": iso(self.completed_at),
"recurrence_rule": self.recurrence_rule,
"recurrence_next_spawn_at": (
self.recurrence_next_spawn_at.isoformat()
if self.recurrence_next_spawn_at
else None
),
"recurrence_next_spawn_at": iso(self.recurrence_next_spawn_at),
"is_task": self.is_task,
"note_type": self.note_type or "note",
"task_kind": self.task_kind,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+3 -3
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin
from scribe.models.base import TimestampMixin, iso
class NoteDraft(Base, TimestampMixin):
@@ -25,6 +25,6 @@ class NoteDraft(Base, TimestampMixin):
"original_body": self.original_body,
"instruction": self.instruction,
"scope": self.scope,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+2 -2
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from scribe.models.base import CreatedAtMixin, iso
class NoteSupersession(Base, CreatedAtMixin):
@@ -66,5 +66,5 @@ class NoteSupersession(Base, CreatedAtMixin):
"id": self.id,
"superseder_id": self.superseder_id,
"superseded_id": self.superseded_id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"created_at": iso(self.created_at),
}
+4 -8
View File
@@ -1,15 +1,14 @@
from datetime import datetime, timezone
from sqlalchemy import DateTime, Index, Integer, Text
from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
SURFACED = "surfaced"
PULLED = "pulled"
class NoteUsageEvent(Base):
class NoteUsageEvent(Base, CreatedAtMixin):
"""One row per time a note was SURFACED to the agent, or PULLED in full.
Answers the question RetrievalLog cannot: not "what did the ranker return
@@ -44,9 +43,6 @@ class NoteUsageEvent(Base):
__tablename__ = "note_usage_events"
id: Mapped[int] = mapped_column(primary_key=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
note_id: Mapped[int] = mapped_column(Integer, nullable=False)
# 'surfaced' | 'pulled'
@@ -81,7 +77,7 @@ class NoteUsageEvent(Base):
def to_dict(self) -> dict:
return {
"id": self.id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"created_at": iso(self.created_at),
"user_id": self.user_id,
"note_id": self.note_id,
"event": self.event,
+2 -2
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ARRAY, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from scribe.models.base import CreatedAtMixin, iso
class NoteVersion(Base, CreatedAtMixin):
@@ -26,7 +26,7 @@ class NoteVersion(Base, CreatedAtMixin):
"tags": self.tags or [],
"pin_kind": self.pin_kind,
"pin_label": self.pin_label,
"created_at": self.created_at.isoformat(),
"created_at": iso(self.created_at),
}
if include_body:
d["body"] = self.body
+3 -3
View File
@@ -5,7 +5,7 @@ from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from scribe.models.base import CreatedAtMixin, iso
class Notification(Base, CreatedAtMixin):
@@ -26,6 +26,6 @@ class Notification(Base, CreatedAtMixin):
"user_id": self.user_id,
"type": self.type,
"payload": self.payload,
"read_at": self.read_at.isoformat() if self.read_at else None,
"created_at": self.created_at.isoformat(),
"read_at": iso(self.read_at),
"created_at": iso(self.created_at),
}
+2 -4
View File
@@ -4,9 +4,10 @@ from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
class PasswordResetToken(Base):
class PasswordResetToken(Base, CreatedAtMixin):
__tablename__ = "password_reset_tokens"
id: Mapped[int] = mapped_column(primary_key=True)
@@ -14,9 +15,6 @@ class PasswordResetToken(Base):
token_hash: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
used: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
__table_args__ = (
Index("ix_password_reset_tokens_token_hash", "token_hash"),
+3 -3
View File
@@ -2,7 +2,7 @@ import enum
from sqlalchemy import BigInteger, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, SoftDeleteMixin
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
class ProjectStatus(str, enum.Enum):
@@ -48,6 +48,6 @@ class Project(Base, TimestampMixin, SoftDeleteMixin):
"color": self.color,
"design_system_id": self.design_system_id,
"forge_connection_id": self.forge_connection_id,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+3 -3
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin
from scribe.models.base import TimestampMixin, iso
class RepoBinding(Base, TimestampMixin):
@@ -35,6 +35,6 @@ class RepoBinding(Base, TimestampMixin):
"user_id": self.user_id,
"project_id": self.project_id,
"repo_key": self.repo_key,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+5 -1
View File
@@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import iso
class RetrievalLog(Base):
@@ -23,6 +24,9 @@ class RetrievalLog(Base):
__tablename__ = "retrieval_logs"
id: Mapped[int] = mapped_column(primary_key=True)
# Declared here rather than via CreatedAtMixin on purpose: the composite
# index below orders on `created_at.desc()`, which needs the column object
# in this class body — a mixin's column is not in scope there.
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
@@ -54,7 +58,7 @@ class RetrievalLog(Base):
def to_dict(self) -> dict:
return {
"id": self.id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"created_at": iso(self.created_at),
"user_id": self.user_id,
"source": self.source,
"query": self.query,
+10 -34
View File
@@ -4,10 +4,10 @@ from sqlalchemy import BigInteger, Boolean, Column, DateTime, ForeignKey, Index,
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
class Rulebook(Base, SoftDeleteMixin):
class Rulebook(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "rulebooks"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
@@ -19,14 +19,6 @@ class Rulebook(Base, SoftDeleteMixin):
always_on: Mapped[bool] = mapped_column(
Boolean, default=False, nullable=False, server_default="false"
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)
def to_dict(self) -> dict:
return {
@@ -35,12 +27,12 @@ class Rulebook(Base, SoftDeleteMixin):
"title": self.title,
"description": self.description or "",
"always_on": self.always_on,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
class RulebookTopic(Base, SoftDeleteMixin):
class RulebookTopic(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "rulebook_topics"
# Partial unique: a title is unique among LIVE topics in a rulebook, so a
# trashed topic doesn't block recreating/restoring the same title.
@@ -58,14 +50,6 @@ class RulebookTopic(Base, SoftDeleteMixin):
title: Mapped[str] = mapped_column(Text)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
order_index: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)
def to_dict(self) -> dict:
return {
@@ -74,12 +58,12 @@ class RulebookTopic(Base, SoftDeleteMixin):
"title": self.title,
"description": self.description or "",
"order_index": self.order_index,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
class Rule(Base, SoftDeleteMixin):
class Rule(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "rules"
# Partial unique: title unique among LIVE rules in a topic (soft-deleted
# rules don't block recreating/restoring the same title).
@@ -109,14 +93,6 @@ class Rule(Base, SoftDeleteMixin):
why: Mapped[str | None] = mapped_column(Text, nullable=True)
how_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True)
order_index: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)
def to_dict(self) -> dict:
return {
@@ -128,8 +104,8 @@ class Rule(Base, SoftDeleteMixin):
"why": self.why or "",
"how_to_apply": self.how_to_apply or "",
"order_index": self.order_index,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+5 -5
View File
@@ -2,7 +2,7 @@ from sqlalchemy import CheckConstraint, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin
from scribe.models.base import TimestampMixin, iso
class ProjectShare(Base, TimestampMixin):
@@ -37,8 +37,8 @@ class ProjectShare(Base, TimestampMixin):
"shared_with_group_id": self.shared_with_group_id,
"permission": self.permission,
"invited_by": self.invited_by,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
@@ -74,6 +74,6 @@ class NoteShare(Base, TimestampMixin):
"shared_with_group_id": self.shared_with_group_id,
"permission": self.permission,
"invited_by": self.invited_by,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+3 -3
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Index, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, TimestampMixin, SoftDeleteMixin
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
class System(Base, TimestampMixin, SoftDeleteMixin):
@@ -44,8 +44,8 @@ class System(Base, TimestampMixin, SoftDeleteMixin):
"color": self.color,
"status": self.status,
"order_index": self.order_index,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+3 -3
View File
@@ -2,7 +2,7 @@ from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin
from scribe.models.base import TimestampMixin, iso
class TaskLog(Base, TimestampMixin):
@@ -21,6 +21,6 @@ class TaskLog(Base, TimestampMixin):
"user_id": self.user_id,
"content": self.content,
"duration_minutes": self.duration_minutes,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"created_at": iso(self.created_at),
"updated_at": iso(self.updated_at),
}
+2 -2
View File
@@ -2,7 +2,7 @@ from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from scribe.models.base import CreatedAtMixin, iso
class User(Base, CreatedAtMixin):
@@ -26,6 +26,6 @@ class User(Base, CreatedAtMixin):
"username": self.username,
"email": self.email,
"role": self.role,
"created_at": self.created_at.isoformat(),
"created_at": iso(self.created_at),
"has_password": self.password_hash is not None,
}