Skip to content

Commit

Permalink
feat: Finished documents routes
Browse files Browse the repository at this point in the history
  • Loading branch information
kojicmarko committed Mar 1, 2024
1 parent de7e635 commit c69c6a6
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 11 deletions.
15 changes: 15 additions & 0 deletions src/documents/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from typing import Annotated
from uuid import UUID

from fastapi import Depends, HTTPException, UploadFile, status
from sqlalchemy.orm import Session

from src.config import settings
from src.database import get_db
from src.documents import models


def valid_filename(file: UploadFile) -> UploadFile:
Expand All @@ -20,3 +24,14 @@ def valid_file(file: Annotated[UploadFile, Depends(valid_filename)]) -> UploadFi
detail="Unsupported file type",
)
return file


def get_doc_by_id(
doc_id: UUID, db: Annotated[Session, Depends(get_db)]
) -> models.Document:
document = db.query(models.Document).filter(models.Document.id == doc_id).first()
if not document:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Document not found"
)
return document
50 changes: 50 additions & 0 deletions src/documents/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, UploadFile, status
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session

from src.database import get_db
from src.documents import models as doc_models
from src.documents import schemas as doc_schemas
from src.documents import service as doc_service
from src.documents.dependencies import get_doc_by_id, valid_file
from src.users import schemas as user_schemas

# from src.projects import service as project_service
from src.users.dependencies import get_curr_user

router = APIRouter(prefix="/documents")


@router.get("/{doc_id}", status_code=status.HTTP_200_OK)
def download(
document: Annotated[doc_models.Document, Depends(get_doc_by_id)],
user: Annotated[user_schemas.User, Depends(get_curr_user)],
db: Annotated[Session, Depends(get_db)],
) -> FileResponse:
doc = doc_service.read(document, user.id, db)
return FileResponse(doc.url, filename=doc.name)


@router.put("/{doc_id}", status_code=status.HTTP_200_OK)
def update(
doc_id: UUID,
document: Annotated[doc_models.Document, Depends(get_doc_by_id)],
file: Annotated[UploadFile, Depends(valid_file)],
user: Annotated[user_schemas.User, Depends(get_curr_user)],
db: Annotated[Session, Depends(get_db)],
) -> doc_schemas.Document:
doc = doc_service.update(document, file, user.id, db)
return doc


@router.delete("/{doc_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete(
doc_id: UUID,
document: Annotated[doc_models.Document, Depends(get_doc_by_id)],
user: Annotated[user_schemas.User, Depends(get_curr_user)],
db: Annotated[Session, Depends(get_db)],
) -> None:
doc_service.delete(document, user.id, db)
83 changes: 77 additions & 6 deletions src/documents/service.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,101 @@
import os
import pathlib
from uuid import UUID

from fastapi import UploadFile
from fastapi import HTTPException, UploadFile, status
from sqlalchemy.orm import Session
from sqlalchemy.sql import exists

from src.documents import models
from src.documents import models as doc_models
from src.documents import schemas as doc_schemas
from src.models import ProjectUser
from src.projects.dependencies import get_proj_by_id
from src.users import schemas as user_schemas


def read(
document: doc_models.Document, user_id: UUID, db: Session
) -> doc_schemas.Document:
project_user = db.query(
exists().where(
ProjectUser.user_id == user_id,
ProjectUser.project_id == document.project_id,
)
).scalar()

if not project_user:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden access"
)

return doc_schemas.Document.model_validate(document)


def create(
name: str | None, url: str, proj_id: UUID, user: user_schemas.User, db: Session
) -> doc_schemas.Document:
document = models.Document(name=name, url=url, owner_id=user.id, project_id=proj_id)
document = doc_models.Document(
name=name, url=url, owner_id=user.id, project_id=proj_id
)
db.add(document)
db.commit()
return doc_schemas.Document.model_validate(document)


async def file_upload(file: UploadFile, proj_id: UUID) -> str:
def update(
document: doc_models.Document, file: UploadFile, user_id: UUID, db: Session
) -> doc_schemas.Document:
project_user = db.query(
exists().where(
ProjectUser.user_id == user_id,
ProjectUser.project_id == document.project_id,
)
).scalar()

if not project_user:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden access"
)
if file.filename is not None:
document.name = file.filename

old_url = document.url
url = file_upload(file, document.project_id)

document.url = url
db.commit()

file_delete(old_url)

return doc_schemas.Document.model_validate(document)


def delete(
document: doc_models.Document,
user_id: UUID,
db: Session,
) -> None:
project = get_proj_by_id(document.project_id, db)
if project.owner_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden access"
)

file_delete(document.url)
db.delete(document)
db.commit()


def file_upload(file: UploadFile, proj_id: UUID) -> str:
filename = f"{proj_id}_{file.filename}"
path = (
pathlib.Path(__file__).parent.parent.parent / "bucket" / "documents" / filename
)
with open(path, "wb+") as f:
contents = await file.read()
f.write(contents)
f.write(file.file.read())

return str(path)


def file_delete(url: str) -> None:
os.remove(url)
2 changes: 2 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import FastAPI

from src.documents import router as documents_router
from src.projects import router as projects_router
from src.users import router as auth_router
from src.utils.logger.main import setup_logging
Expand All @@ -11,6 +12,7 @@

app.include_router(projects_router.router)
app.include_router(auth_router.router)
app.include_router(documents_router.router)


@app.get("/health")
Expand Down
4 changes: 2 additions & 2 deletions src/projects/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ def invite(


@router.post("/{proj_id}/documents", status_code=status.HTTP_201_CREATED)
async def upload(
def upload(
proj_id: UUID,
document: Annotated[UploadFile, Depends(valid_file)],
user: Annotated[user_schemas.User, Depends(is_participant)],
db: Annotated[Session, Depends(get_db)],
) -> doc_schemas.Document:
url = await doc_service.file_upload(document, proj_id)
url = doc_service.file_upload(document, proj_id)
return doc_service.create(document.filename, url, proj_id, user, db)


Expand Down
14 changes: 11 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,20 @@ def test_documents(
db: Session, test_user: User, test_projects: list[Project]
) -> list[Document]:
project = test_projects[0]
documents = [DocumentCreate(name=f"document{i}") for i in range(3)]
files = [
UploadFile(file=BytesIO(b"file content"), filename=f"document{i}")
for i in range(3)
]
documents = [DocumentCreate(name=file.filename) for file in files if file.filename]
return [
doc_service.create(
document.name, f"/path/to/document{i}", project.id, test_user, db
doc.name,
doc_service.file_upload(file, project.id),
project.id,
test_user,
db,
)
for i, document in enumerate(documents)
for doc, file in zip(documents, files)
]


Expand Down
Empty file added tests/documents/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions tests/documents/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import os

from fastapi import UploadFile, status
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session

from src.documents.schemas import Document
from src.projects.schemas import Project
from src.users.schemas import User


def test_download_document(
client: TestClient,
db: Session,
test_user: User,
test_projects: list[Project],
test_token: str,
mock_upload_file: UploadFile,
test_documents: list[Document],
) -> None:
document = test_documents[0]

res = client.get(
f"/documents/{document.id}/",
headers={"Authorization": f"Bearer {test_token}"},
)

assert res.status_code == 200


def test_update_document(
client: TestClient,
db: Session,
test_user: User,
test_projects: list[Project],
test_token: str,
mock_upload_file: UploadFile,
test_documents: list[Document],
) -> None:
project = test_projects[0]
document = test_documents[0]

data = {
"file": ("mock_file.pdf", mock_upload_file.file, mock_upload_file.content_type)
}

res = client.put(
f"/documents/{document.id}/",
headers={"Authorization": f"Bearer {test_token}"},
files=data,
)

expected_end_of_path = os.path.join(
"bucket", "documents", f"{project.id}_mock_file.pdf"
)

assert res.json()["url"].endswith(expected_end_of_path)


def test_delete_document(
client: TestClient,
db: Session,
test_user: User,
test_projects: list[Project],
test_token: str,
mock_upload_file: UploadFile,
test_documents: list[Document],
) -> None:
document = test_documents[0]

res = client.delete(
f"/documents/{document.id}", headers={"Authorization": f"Bearer {test_token}"}
)

assert res.status_code == status.HTTP_204_NO_CONTENT

0 comments on commit c69c6a6

Please sign in to comment.