Files
PROJECT-CONTORL/backend/app/models/task.py
beabigegg 10db2c9d1f feat: implement audit trail alignment (soft delete & permission audit)
- Task Soft Delete:
  - Add is_deleted, deleted_at, deleted_by fields to Task model
  - Convert DELETE to soft delete with cascade to subtasks
  - Add include_deleted query param (admin only)
  - Add POST /api/tasks/{id}/restore endpoint
  - Exclude deleted tasks from subtask_count

- Permission Change Audit:
  - Add user.role_change event (high sensitivity)
  - Add user.admin_change event (critical, triggers alert)
  - Add PATCH /api/users/{id}/admin endpoint
  - Add role.permission_change event type

- Append-Only Enforcement:
  - Add DB triggers for audit_logs immutability (manual for production)
  - Migration 008 with graceful trigger failure handling

- Tests: 11 new soft delete tests (153 total passing)
- OpenSpec: fix-audit-trail archived, fix-realtime-notifications & fix-weekly-report proposals added

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 06:58:30 +08:00

58 lines
2.7 KiB
Python

from sqlalchemy import Column, String, Text, Integer, Boolean, DateTime, Numeric, Enum, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
import enum
class Priority(str, enum.Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class Task(Base):
__tablename__ = "pjctrl_tasks"
id = Column(String(36), primary_key=True)
project_id = Column(String(36), ForeignKey("pjctrl_projects.id", ondelete="CASCADE"), nullable=False)
parent_task_id = Column(String(36), ForeignKey("pjctrl_tasks.id", ondelete="CASCADE"), nullable=True)
title = Column(String(500), nullable=False)
description = Column(Text, nullable=True)
assignee_id = Column(String(36), ForeignKey("pjctrl_users.id"), nullable=True)
status_id = Column(String(36), ForeignKey("pjctrl_task_statuses.id"), nullable=True)
priority = Column(
Enum("low", "medium", "high", "urgent", name="priority_enum"),
default="medium",
nullable=False
)
original_estimate = Column(Numeric(8, 2), nullable=True)
time_spent = Column(Numeric(8, 2), default=0, nullable=False)
blocker_flag = Column(Boolean, default=False, nullable=False)
due_date = Column(DateTime, nullable=True)
position = Column(Integer, default=0, nullable=False)
created_by = Column(String(36), ForeignKey("pjctrl_users.id"), nullable=False)
created_at = Column(DateTime, server_default=func.now(), nullable=False)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
# Soft delete fields
is_deleted = Column(Boolean, default=False, nullable=False, index=True)
deleted_at = Column(DateTime, nullable=True)
deleted_by = Column(String(36), ForeignKey("pjctrl_users.id"), nullable=True)
# Relationships
project = relationship("Project", back_populates="tasks")
parent_task = relationship("Task", remote_side=[id], back_populates="subtasks")
subtasks = relationship("Task", back_populates="parent_task", cascade="all, delete-orphan")
assignee = relationship("User", foreign_keys=[assignee_id], back_populates="assigned_tasks")
creator = relationship("User", foreign_keys=[created_by], back_populates="created_tasks")
deleter = relationship("User", foreign_keys=[deleted_by])
status = relationship("TaskStatus", back_populates="tasks")
# Collaboration relationships
comments = relationship("Comment", back_populates="task", cascade="all, delete-orphan")
blockers = relationship("Blocker", back_populates="task", cascade="all, delete-orphan")
attachments = relationship("Attachment", back_populates="task", cascade="all, delete-orphan")
trigger_logs = relationship("TriggerLog", back_populates="task")