diff --git a/README.md b/README.md index b277b8c..eeb2ee6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ DocFlow is a powerful Document Management API designed to streamline document ha ## 😎 Upcoming Updates -- 🟨 Document Interactions - Adding Comments and Tags +- 🟨 Document Interactions - Adding Comments - 🟨 Import documents from unread emails - 🟨 Video Preview - 🟨 Adding custom metadata fields to document diff --git a/app/api/router.py b/app/api/router.py index 24e12b7..02166b6 100644 --- a/app/api/router.py +++ b/app/api/router.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from app.api.routes.auth.auth import router as auth_router +from app.api.routes.documents.comments import router as document_comment from app.api.routes.documents.documents_metadata import ( router as documents_metadata_router, ) @@ -15,6 +16,7 @@ router.include_router(auth_router, prefix="/u") router.include_router(documents_router, prefix="") +router.include_router(document_comment, prefix="/comments") router.include_router(notify_router, prefix="/notifications") router.include_router(documents_metadata_router, prefix="/metadata") router.include_router(document_organization_router, prefix="/filter") diff --git a/app/api/routes/documents/comments.py b/app/api/routes/documents/comments.py new file mode 100644 index 0000000..3bb0686 --- /dev/null +++ b/app/api/routes/documents/comments.py @@ -0,0 +1,67 @@ +from typing import List +from uuid import UUID + +from fastapi import APIRouter, status, Depends + +from app.api.dependencies.auth_utils import get_current_user +from app.api.dependencies.repositories import get_repository +from app.db.repositories.documents.comments import CommentRepository +from app.db.repositories.documents.documents import DocumentRepository +from app.schemas.auth.bands import TokenData +from app.schemas.documents.comments import CommentRead, CommentCreate, CommentUpdate + +router = APIRouter(tags=["Comments"]) + + +@router.post( + "/", + response_model=CommentRead, + status_code=status.HTTP_201_CREATED, + name="create_comment", +) +async def create_comment( + comment: CommentCreate, + user: TokenData = Depends(get_current_user), + doc_repo: DocumentRepository = Depends(get_repository(DocumentRepository)), + repository: CommentRepository = Depends(get_repository(CommentRepository)), +) -> CommentRead: ... + + +@router.get( + "/{doc_id}", + response_model=List[CommentRead], + status_code=status.HTTP_200_OK, + name="get_comments", +) +async def get_document_comments( + doc_id: UUID, + user: TokenData = Depends(get_current_user), + doc_repo: DocumentRepository = Depends(get_repository(DocumentRepository)), + repository: CommentRepository = Depends(get_repository(CommentRepository)), +) -> List[CommentRead]: ... + + +@router.put( + "/{comment_id}", + response_model=CommentRead, + status_code=status.HTTP_200_OK, + name="update_comment", +) +async def update_comment( + comment_id: UUID, + comment_update: CommentUpdate, + user: TokenData = Depends(get_current_user), + repository: CommentRepository = Depends(get_repository(CommentRepository)), +) -> CommentRead: ... + + +@router.delete( + "/{comment_id}", + status_code=status.HTTP_204_NO_CONTENT, + name="delete_comment", +) +async def delete_comment( + comment_id: UUID, + user: TokenData = Depends(get_current_user), + repository: CommentRepository = Depends(get_repository(CommentRepository)), +) -> None: ... diff --git a/app/db/repositories/documents/comments.py b/app/db/repositories/documents/comments.py new file mode 100644 index 0000000..648866f --- /dev/null +++ b/app/db/repositories/documents/comments.py @@ -0,0 +1,28 @@ +from typing import Optional, List +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.tables.documents.documents_metadata import DocumentComment +from app.schemas.documents.comments import CommentCreate, CommentUpdate + + +class CommentRepository: + def __init__(self, session: AsyncSession): + self.session = session + + async def create( + self, comment: CommentCreate, author_id: str + ) -> DocumentComment: ... + + async def get(self, comment_id: UUID) -> Optional[DocumentComment]: ... + + async def get_document_comments(self, doc_id: UUID) -> List[DocumentComment]: ... + + async def update( + self, comment_id: UUID, comment_update: CommentUpdate + ) -> Optional[DocumentComment]: ... + + async def delete(self, comment_id: UUID) -> bool: ... + + async def user_can_modify(self, comment_id: UUID, user_id: str) -> bool: ... diff --git a/app/db/tables/auth/auth.py b/app/db/tables/auth/auth.py index 503e17d..e6aac01 100644 --- a/app/db/tables/auth/auth.py +++ b/app/db/tables/auth/auth.py @@ -25,3 +25,4 @@ class User(Base): ) owner_of = relationship("DocumentMetadata", back_populates="owner") + comments = relationship("DocumentComment", back_populates="author") diff --git a/app/db/tables/documents/documents_metadata.py b/app/db/tables/documents/documents_metadata.py index 5d454b2..eab22ed 100644 --- a/app/db/tables/documents/documents_metadata.py +++ b/app/db/tables/documents/documents_metadata.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone from uuid import uuid4 -from typing import List, Optional +from typing import List, Optional, Text from sqlalchemy import ( Column, String, @@ -61,3 +61,35 @@ class DocumentMetadata(Base): "User", secondary=doc_user_access, passive_deletes=True ) owner = relationship("User", back_populates="owner_of") + comments = relationship( + "DocumentComment", back_populates="document", cascade="all, delete-orphan" + ) + + +class DocumentComment(Base): + __tablename__ = "document_comments" + + id: UUID = Column( + UUID(as_uuid=True), default=uuid4, primary_key=True, index=True, nullable=False + ) + doc_id: UUID = Column( + UUID(as_uuid=True), ForeignKey("document_metadata.id", ondelete="CASCADE") + ) + author_id: str = Column(String, ForeignKey("users.id"), nullable=False) + comment: str = Column(Text, nullable=False) + created_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone.utc), + nullable=False, + server_default=text("NOW()"), + ) + updated_at = Column( + DateTime(timezone=True), + default=datetime.now(timezone.utc), + onupdate=datetime.now(timezone.utc), + nullable=False, + server_default=text("NOW()"), + ) + + document = relationship("DocumentMetadata", back_populates="comments") + author = relationship("User", back_populates="comments") diff --git a/app/schemas/documents/bands.py b/app/schemas/documents/bands.py index 2ff7623..b3f8af3 100644 --- a/app/schemas/documents/bands.py +++ b/app/schemas/documents/bands.py @@ -67,3 +67,8 @@ class Notification(BaseModel): class NotifyPatchStatus(BaseModel): status: NotifyEnum = NotifyEnum.unread mark_all: bool = False + + +# comments +class CommentBase(BaseModel): + comment: str diff --git a/app/schemas/documents/comments.py b/app/schemas/documents/comments.py new file mode 100644 index 0000000..bf77b0d --- /dev/null +++ b/app/schemas/documents/comments.py @@ -0,0 +1,22 @@ +from datetime import datetime +from uuid import UUID + +from app.schemas.documents.bands import CommentBase + + +class CommentCreate(CommentBase): + doc_id: UUID + + +class CommentUpdate(CommentBase): ... + + +class CommentRead(CommentBase): + id: UUID + doc_id: UUID + author_id: str + created_at: datetime + updated_at: datetime + + class Config: + from_attribute = True diff --git a/migrations/env.py b/migrations/env.py index fb4cffa..138e890 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -8,7 +8,7 @@ from app.db.models import Base from app.core.config import settings -from app.db.tables.documents.documents_metadata import DocumentMetadata +from app.db.tables.documents.documents_metadata import DocumentMetadata, DocumentComment from app.db.tables.auth.auth import User from app.db.tables.documents.document_sharing import DocumentSharing from app.db.tables.documents.notify import Notify diff --git a/migrations/versions/2a02384ab925_initial_almebic.py b/migrations/versions/2a02384ab925_initial_almebic.py index f23c173..efb3239 100644 --- a/migrations/versions/2a02384ab925_initial_almebic.py +++ b/migrations/versions/2a02384ab925_initial_almebic.py @@ -21,6 +21,7 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.create_table( "notify", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), diff --git a/migrations/versions/cb2ea87be0bd_document_comments.py b/migrations/versions/cb2ea87be0bd_document_comments.py new file mode 100644 index 0000000..f117039 --- /dev/null +++ b/migrations/versions/cb2ea87be0bd_document_comments.py @@ -0,0 +1,64 @@ +"""document comments + +Revision ID: cb2ea87be0bd +Revises: 2a02384ab925 +Create Date: 2024-12-15 01:24:30.954923 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "cb2ea87be0bd" +down_revision: Union[str, None] = "2a02384ab925" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "document_comments", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("doc_id", sa.UUID(), nullable=True), + sa.Column("author_id", sa.String(), nullable=False), + sa.Column("comment", sa.Text(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("NOW()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("NOW()"), + nullable=False, + ), + sa.ForeignKeyConstraint( + ["author_id"], + ["users.id"], + ), + sa.ForeignKeyConstraint( + ["doc_id"], ["document_metadata.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_document_comments_id"), "document_comments", ["id"], unique=False + ) + op.drop_index("ix_notify_id", table_name="notify") + op.create_unique_constraint(None, "share_url", ["url_id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "share_url", type_="unique") + op.drop_index(op.f("ix_document_comments_id"), table_name="document_comments") + op.drop_table("document_comments") + # ### end Alembic commands ###