feat: implement document management module
- Backend (FastAPI): - Attachment and AttachmentVersion models with migration - FileStorageService with SHA-256 checksum validation - File type validation (whitelist/blacklist) - Full CRUD API with version control support - Audit trail integration for upload/download/delete - Configurable upload directory and file size limit - Frontend (React + Vite): - AttachmentUpload component with drag & drop - AttachmentList component with download/delete - TaskAttachments combined component - Attachments service for API calls - Testing: - 12 tests for storage service and API endpoints - OpenSpec: - add-document-management change archived 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""Document management tables
|
||||
|
||||
Revision ID: 006
|
||||
Revises: 005
|
||||
Create Date: 2024-12-29
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers
|
||||
revision = '006'
|
||||
down_revision = '005'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Create attachments table
|
||||
op.create_table(
|
||||
'pjctrl_attachments',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('task_id', sa.String(36), sa.ForeignKey('pjctrl_tasks.id', ondelete='CASCADE'), nullable=False),
|
||||
sa.Column('filename', sa.String(255), nullable=False),
|
||||
sa.Column('original_filename', sa.String(255), nullable=False),
|
||||
sa.Column('mime_type', sa.String(100), nullable=False),
|
||||
sa.Column('file_size', sa.BigInteger, nullable=False),
|
||||
sa.Column('current_version', sa.Integer, default=1, nullable=False),
|
||||
sa.Column('is_encrypted', sa.Boolean, default=False, nullable=False),
|
||||
sa.Column('uploaded_by', sa.String(36), sa.ForeignKey('pjctrl_users.id', ondelete='SET NULL'), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean, default=False, nullable=False),
|
||||
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index('idx_attachment_task', 'pjctrl_attachments', ['task_id', 'is_deleted'])
|
||||
|
||||
# Create attachment_versions table
|
||||
op.create_table(
|
||||
'pjctrl_attachment_versions',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('attachment_id', sa.String(36), sa.ForeignKey('pjctrl_attachments.id', ondelete='CASCADE'), nullable=False),
|
||||
sa.Column('version', sa.Integer, nullable=False),
|
||||
sa.Column('file_path', sa.String(1000), nullable=False),
|
||||
sa.Column('file_size', sa.BigInteger, nullable=False),
|
||||
sa.Column('checksum', sa.String(64), nullable=False),
|
||||
sa.Column('uploaded_by', sa.String(36), sa.ForeignKey('pjctrl_users.id', ondelete='SET NULL'), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index('idx_version_attachment', 'pjctrl_attachment_versions', ['attachment_id', 'version'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('idx_version_attachment', 'pjctrl_attachment_versions')
|
||||
op.drop_table('pjctrl_attachment_versions')
|
||||
op.drop_index('idx_attachment_task', 'pjctrl_attachments')
|
||||
op.drop_table('pjctrl_attachments')
|
||||
Reference in New Issue
Block a user