Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Make project_id nullable in document

Revision ID: 35020a30cb48
Revises: dcd734dfe9c4
Create Date: 2026-03-01 15:05:44.684815

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# pylint: disable=E1101

# revision identifiers, used by Alembic.
revision: str = '35020a30cb48'
down_revision: Union[str, None] = 'dcd734dfe9c4'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.alter_column('document', 'project_id',
existing_type=sa.INTEGER(),
nullable=False)


def downgrade() -> None:
op.alter_column('document', 'project_id',
existing_type=sa.INTEGER(),
nullable=True)
4 changes: 1 addition & 3 deletions backend/app/documents/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ class Document(Base):
created_by: Mapped[int] = mapped_column(ForeignKey("user.id"))
processing_status: Mapped[str] = mapped_column()
upload_time: Mapped[datetime] = mapped_column(default=utc_time)
project_id: Mapped[int | None] = mapped_column(
ForeignKey("projects.id"), nullable=True
)
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"))

records: Mapped[list["DocumentRecord"]] = relationship(
back_populates="document",
Expand Down
2 changes: 1 addition & 1 deletion backend/app/documents/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def update_document(
if name is not None:
document.name = name
if project_id is not None:
document.project_id = project_id if project_id != -1 else None
document.project_id = project_id
self.__db.commit()
self.__db.refresh(document)
return document
Expand Down
4 changes: 2 additions & 2 deletions backend/app/documents/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class DocumentUpdate(BaseModel):
)
project_id: int | None = Field(
default=None,
description="ID of project to assign document to. Set to -1 to unassign.",
description="ID of project to assign document to.",
)

model_config = ConfigDict(from_attributes=True)
Expand All @@ -121,7 +121,7 @@ class DocumentUpdate(BaseModel):
class DocumentUpdateResponse(BaseModel):
id: int
name: str
project_id: int | None
project_id: int

model_config = ConfigDict(from_attributes=True)

Expand Down
3 changes: 2 additions & 1 deletion backend/app/routers/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ async def create_doc(
file: Annotated[UploadFile, File()],
service: Annotated[DocumentService, Depends(get_service)],
current_user: Annotated[int, Depends(get_current_user_id)],
project_id: Annotated[int, Form()],
) -> doc_schema.Document:
try:
return await service.create_document(file, current_user)
return await service.create_document(file, current_user, project_id)
except EntityNotFound as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except BusinessLogicError as e:
Expand Down
17 changes: 13 additions & 4 deletions backend/app/services/document_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,21 @@ def get_document(self, doc_id: int) -> doc_schema.DocumentWithRecordsCount:
)

async def create_document(
self, file: UploadFile, user_id: int
self, file: UploadFile, user_id: int, project_id: int
) -> doc_schema.Document:
"""
Create a new document from uploaded file.

Args:
file: Uploaded file
user_id: ID of user creating the document
project_id: Optional ID of project to assign document to

Returns:
Created Document object

Raises:
EntityNotFound: If file type is unsupported
EntityNotFound: If file type is unsupported or project not found
"""
cutoff_date = datetime.now() - timedelta(days=1)

Expand All @@ -127,12 +128,20 @@ async def create_document(
else:
raise BusinessLogicError("Unsupported file type")

# Validate project_id if provided
try:
pq = ProjectQuery(self.__db)
pq._get_project(project_id)
except NotFoundProjectExc:
raise EntityNotFound("Project", project_id)

doc = Document(
name=name,
type=doc_type,
processing_status=models.DocumentStatus.UPLOADED.value,
upload_time=datetime.now(),
created_by=user_id,
project_id=project_id,
)
self.__query.add_document(doc, original_document)
return doc_schema.Document(
Expand All @@ -141,7 +150,7 @@ async def create_document(
status=models.DocumentStatus(doc.processing_status),
created_by=doc.created_by,
type=doc.type.value,
project_id=None,
project_id=doc.project_id,
)

def delete_document(self, doc_id: int) -> models.StatusMessage:
Expand Down Expand Up @@ -638,7 +647,7 @@ def update_document(
"""
self._get_document_by_id(doc_id)
try:
if update_data.project_id is not None and update_data.project_id != -1:
if update_data.project_id is not None:
pq = ProjectQuery(self.__db)
# verify project exists
pq._get_project(update_data.project_id)
Expand Down
29 changes: 3 additions & 26 deletions backend/app/services/project_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Project service for project management operations."""

from datetime import UTC, datetime

from sqlalchemy.orm import Session

Expand Down Expand Up @@ -35,14 +34,6 @@ def list_projects(self, user_id: int) -> list[ProjectResponse]:
"""
projects = self.__query.list_projects(user_id)
return [
ProjectResponse(
id=-1,
name="Unnamed project",
created_by=-1,
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
] + [
ProjectResponse(
id=project.id,
name=project.name,
Expand All @@ -69,24 +60,10 @@ def get_project(self, project_id: int, user_id: int) -> DetailedProjectResponse:
UnauthorizedAccess: If user doesn't own the project
"""
try:
project = (
self.__query._get_project(project_id)
if project_id != -1
else Project(
id=-1,
name="Unnamed project",
created_by=-1,
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
)
project = self.__query._get_project(project_id)
self._check_ownership(project, user_id)
aggregates = self.__query.get_project_aggregates(
project_id if project_id != -1 else None
)
documents = self.__query.get_project_documents(
project_id if project_id != -1 else None
)
aggregates = self.__query.get_project_aggregates(project_id)
documents = self.__query.get_project_documents(project_id)

def find_doc(doc_id: int):
for aggregate in aggregates:
Expand Down
26 changes: 26 additions & 0 deletions backend/tests/routers/test_routes_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@

from app.comments.models import Comment
from app.documents.models import Document, DocumentRecord, DocumentType
from app.projects.query import ProjectQuery
from app.projects.schema import ProjectCreate

# pylint: disable=C0116


def test_can_get_comments_for_record(user_logged_client: TestClient, session: Session):
"""Test getting all comments for a document record"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
# Create document with records
records = [
DocumentRecord(source="Hello World", target="Привет Мир"),
Expand All @@ -24,6 +27,7 @@ def test_can_get_comments_for_record(user_logged_client: TestClient, session: Se
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)

Expand Down Expand Up @@ -63,6 +67,7 @@ def test_get_comments_returns_empty_for_no_comments(
):
"""Test getting comments for record with no comments"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -71,6 +76,7 @@ def test_get_comments_returns_empty_for_no_comments(
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)
s.commit()
Expand All @@ -93,6 +99,7 @@ def test_get_comments_returns_404_for_nonexistent_record(
def test_can_create_comment(user_logged_client: TestClient, session: Session):
"""Test creating a new comment"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -101,6 +108,7 @@ def test_can_create_comment(user_logged_client: TestClient, session: Session):
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)
s.commit()
Expand Down Expand Up @@ -129,6 +137,7 @@ def test_create_comment_returns_404_for_nonexistent_record(
def test_create_comment_requires_text(user_logged_client: TestClient, session: Session):
"""Test that creating comment requires text field"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -137,6 +146,7 @@ def test_create_comment_requires_text(user_logged_client: TestClient, session: S
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)
s.commit()
Expand All @@ -150,6 +160,7 @@ def test_create_comment_requires_min_length_text(
):
"""Test that creating comment requires minimum length text"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -158,6 +169,7 @@ def test_create_comment_requires_min_length_text(
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)
s.commit()
Expand All @@ -171,6 +183,7 @@ def test_create_comment_requires_authentication(
):
"""Test that creating comment requires authentication"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -179,6 +192,7 @@ def test_create_comment_requires_authentication(
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)
s.commit()
Expand All @@ -191,6 +205,7 @@ def test_create_comment_requires_authentication(
def test_can_update_own_comment(user_logged_client: TestClient, session: Session):
"""Test that user can update their own comment"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -199,6 +214,7 @@ def test_can_update_own_comment(user_logged_client: TestClient, session: Session
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)

Expand All @@ -223,6 +239,7 @@ def test_can_update_own_comment(user_logged_client: TestClient, session: Session
def test_cannot_update_others_comment(user_logged_client: TestClient, session: Session):
"""Test that user cannot update another user's comment"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -231,6 +248,7 @@ def test_cannot_update_others_comment(user_logged_client: TestClient, session: S
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)

Expand Down Expand Up @@ -259,6 +277,7 @@ def test_cannot_update_others_comment(user_logged_client: TestClient, session: S
def test_can_delete_own_comment(user_logged_client: TestClient, session: Session):
"""Test that user can delete their own comment"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -267,6 +286,7 @@ def test_can_delete_own_comment(user_logged_client: TestClient, session: Session
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)

Expand Down Expand Up @@ -294,6 +314,7 @@ def test_can_delete_own_comment(user_logged_client: TestClient, session: Session
def test_cannot_delete_others_comment(user_logged_client: TestClient, session: Session):
"""Test that user cannot delete another user's comment"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -302,6 +323,7 @@ def test_cannot_delete_others_comment(user_logged_client: TestClient, session: S
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)

Expand Down Expand Up @@ -377,6 +399,7 @@ def test_admin_can_update_any_comment(
):
"""Test that admin can update any comment"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -385,6 +408,7 @@ def test_admin_can_update_any_comment(
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)

Expand All @@ -411,6 +435,7 @@ def test_admin_can_delete_any_comment(
):
"""Test that admin can delete any comment"""
with session as s:
p = ProjectQuery(s).create_project(1, ProjectCreate(name="test"))
records = [DocumentRecord(source="Hello World", target="Привет Мир")]
s.add(
Document(
Expand All @@ -419,6 +444,7 @@ def test_admin_can_delete_any_comment(
records=records,
processing_status="done",
created_by=1,
project_id=p.id,
)
)

Expand Down
Loading