Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PDCT 416 Add all document endpoints #13

Merged
merged 6 commits into from
Sep 26, 2023
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
169 changes: 153 additions & 16 deletions app/api/api_v1/routers/document.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
"""Endpoints for managing the Document entity."""
import logging
from typing import Optional, Tuple
from fastapi import APIRouter, HTTPException, status
from app.errors import RepositoryError, ValidationError
from app.model.document import (
DocumentReadDTO,
DocumentUploadRequest,
DocumentUploadResponse,
DocumentWriteDTO,
)

import app.service.document as document_service
Expand All @@ -16,12 +14,120 @@
_LOGGER = logging.getLogger(__name__)


@r.get(
"/documents/{import_id}",
response_model=DocumentReadDTO,
)
async def get_document(
import_id: str,
) -> DocumentReadDTO:
"""
Returns a specific document given the import id.

:param str import_id: Specified import_id.
:raises HTTPException: If the document is not found a 404 is returned.
:return DocumentDTO: returns a DocumentDTO of the document found.
"""
try:
document = document_service.get(import_id)
except ValidationError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e.message)
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

if document is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Document not found: {import_id}",
)

return document


@r.get(
"/documents",
response_model=list[DocumentReadDTO],
)
async def get_all_documents() -> list[DocumentReadDTO]:
"""
Returns all documents

:return DocumentDTO: returns a DocumentDTO of the document found.
"""
try:
return document_service.all()
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)


@r.get(
"/documents/",
response_model=list[DocumentReadDTO],
)
async def search_document(q: str = "") -> list[DocumentReadDTO]:
"""
Searches for documents matching the "q" URL parameter.

:param str q: The string to match, defaults to ""
:raises HTTPException: If nothing found a 404 is returned.
:return list[DocumentDTO]: A list of matching documents.
"""
try:
documents = document_service.search(q)
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

if documents is None or len(documents) == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Documents not found for term: {q}",
)

return documents


@r.put(
"/documents",
response_model=DocumentReadDTO,
)
async def update_document(
new_document: DocumentWriteDTO,
) -> DocumentReadDTO:
"""
Updates a specific document given the import id.

:param str import_id: Specified import_id.
:raises HTTPException: If the document is not found a 404 is returned.
:return DocumentDTO: returns a DocumentDTO of the document updated.
"""
try:
document = document_service.update(new_document)
except ValidationError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e.message)
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

if document is None:
detail = f"Document not updated: {new_document.import_id}"
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail)

return document


@r.post(
"/documents", response_model=DocumentReadDTO, status_code=status.HTTP_201_CREATED
)
async def create_document(
new_document: DocumentReadDTO, upload_request: Optional[DocumentUploadRequest]
) -> Tuple[DocumentReadDTO, Optional[DocumentUploadResponse]]:
new_document: DocumentReadDTO,
) -> DocumentReadDTO:
"""
Creates a specific document given the import id.

Expand All @@ -45,21 +151,52 @@ async def create_document(
detail=f"Document not created: {new_document.import_id}",
)

if upload_request is None:
return document, None
# See PDCT-305
#
# TODO: return Tuple[DocumentReadDTO, Optional[DocumentUploadResponse]]
# if upload_request is None:
# return document, None

try:
upload_details = document_service.get_upload_details(
upload_request.filename, upload_request.overwrite
)
# try:
# upload_details = document_service.get_upload_details(
# upload_request.filename, upload_request.overwrite
# )

upload_response = DocumentUploadResponse(
presigned_upload_url=upload_details[0], cdn_url=upload_details[1]
)
# upload_response = DocumentUploadResponse(
# presigned_upload_url=upload_details[0], cdn_url=upload_details[1]
# )
# except RepositoryError as e:
# _LOGGER.error(e.message)
# raise HTTPException(
# status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
# )

return document


@r.delete(
"/documents/{import_id}",
)
async def delete_document(
import_id: str,
) -> None:
"""
Deletes a specific document given the import id.

:param str import_id: Specified import_id.
:raises HTTPException: If the document is not found a 404 is returned.
"""
try:
document_deleted = document_service.delete(import_id)
except ValidationError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e.message)
except RepositoryError as e:
_LOGGER.error(e.message)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

return document, upload_response
if not document_deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Document not deleted: {import_id}",
)
8 changes: 8 additions & 0 deletions app/clients/db/models/app/authorisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class AuthEntity(str, enum.Enum):

FAMILY = "FAMILIES"
COLLECTION = "COLLECTIONS"
DOCUMENT = "DOCUMENTS"
CONFIG = "CONFIG"


Expand Down Expand Up @@ -59,6 +60,13 @@ class AuthAccess(str, enum.Enum):
AuthOperation.UPDATE: AuthAccess.USER,
AuthOperation.DELETE: AuthAccess.ADMIN,
},
# Collection
AuthEntity.DOCUMENT: {
AuthOperation.CREATE: AuthAccess.USER,
AuthOperation.READ: AuthAccess.USER,
AuthOperation.UPDATE: AuthAccess.USER,
AuthOperation.DELETE: AuthAccess.ADMIN,
},
AuthEntity.CONFIG: {
AuthOperation.READ: AuthAccess.USER,
},
Expand Down
3 changes: 3 additions & 0 deletions app/model/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class DocumentReadDTO(BaseModel):
# languages: list[]


DocumentWriteDTO = DocumentReadDTO


class DocumentUploadRequest(BaseModel):
"""Request for a document upload"""

Expand Down
1 change: 1 addition & 0 deletions app/repository/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import app.repository.metadata as metadata_repo
import app.repository.organisation as organisation_repo
import app.repository.collection as collection_repo
import app.repository.document as document_repo
import app.repository.app_user as app_user_repo
import app.clients.aws.s3bucket as s3bucket_repo
import app.repository.config as config_repo
58 changes: 32 additions & 26 deletions app/repository/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,38 @@
from sqlalchemy import or_, update as db_update
from sqlalchemy_utils import escape_like

from app.repository.helpers import generate_slug


_LOGGER = logging.getLogger(__name__)

DocumentTuple = Tuple[FamilyDocument, PhysicalDocument]
CreateObjects = Tuple[Slug, PhysicalDocumentLanguage, DocumentTuple]
DocumentTuple = Tuple[FamilyDocument, PhysicalDocument, Slug]
CreateObjects = Tuple[PhysicalDocumentLanguage, FamilyDocument, PhysicalDocument]


def _get_query(db: Session) -> Query:
# NOTE: SqlAlchemy will make a complete hash of the query generation
# if columns are used in the query() call. Therefore, entire
# objects are returned.
return db.query(FamilyDocument, PhysicalDocument).filter(
FamilyDocument.physical_document_id == PhysicalDocument.id,

# FIXME: TODO: will this work with multiple slugs????
return (
db.query(FamilyDocument, PhysicalDocument, Slug)
.filter(FamilyDocument.physical_document_id == PhysicalDocument.id)
.filter(Slug.family_document_import_id == FamilyDocument.import_id)
)


def _document_to_dto(doc_tuple: DocumentTuple) -> DocumentReadDTO:
fd, pd = doc_tuple
fd, pd, slug = doc_tuple
return DocumentReadDTO(
import_id=cast(str, fd.import_id),
family_import_id=cast(str, fd.family_import_id),
variant_name=cast(Variant, fd.variant_name),
status=cast(DocumentStatus, fd.document_status),
role=cast(FamilyDocumentRole, fd.document_role),
type=cast(FamilyDocumentType, fd.document_type),
slug=cast(str, fd.slugs[-1]),
slug=cast(str, slug.name),
physical_id=cast(int, pd.id),
title=cast(str, pd.title),
md5_sum=cast(str, pd.md5_sum),
Expand All @@ -74,27 +80,24 @@ def _dto_to_family_document_dict(dto: DocumentReadDTO) -> dict:


def _document_tuple_from_dto(db: Session, dto: DocumentReadDTO) -> CreateObjects:
slug = Slug(name="", document_import_id=0)
language = PhysicalDocumentLanguage(
language_id=db.query(Language.id)
.filter(Language.name == dto.user_language_name)
.scalar(),
document_id=0,
document_id=None,
source=LanguageSource.USER,
visible=True,
)
docs = (
FamilyDocument(**_dto_to_family_document_dict(dto)),
PhysicalDocument(
id=0,
title=dto.title,
md5_sum=dto.md5_sum,
cdn_object=dto.cdn_object,
source_url=dto.source_url,
content_type=dto.content_type,
),
fam_doc = FamilyDocument(**_dto_to_family_document_dict(dto))
phys_doc = PhysicalDocument(
id=None,
title=dto.title,
md5_sum=dto.md5_sum,
cdn_object=dto.cdn_object,
source_url=dto.source_url,
content_type=dto.content_type,
)
return slug, language, docs
return language, fam_doc, phys_doc


def all(db: Session) -> list[DocumentReadDTO]:
Expand Down Expand Up @@ -195,27 +198,30 @@ def create(db: Session, document: DocumentReadDTO) -> Optional[DocumentReadDTO]:
:return Optional[DocumentDTO]: the new document created
"""
try:
slug, language, doc_tuple = _document_tuple_from_dto(db, document)
fd, pd = doc_tuple
language, fd, pd = _document_tuple_from_dto(db, document)

db.add(pd)
db.flush()

# Update the FamilyDocument with the new PhysicalDocument id
fd.physical_document_id = pd.id
# Update the language link with the new PhysicalDocument id
language.physical_document_id = pd.id
language.document_id = pd.id

db.add(fd)
db.add(language)
db.flush()

# Finally the slug
slug.family_document_import_id = fd.import_id
db.add(slug)
db.add(
Slug(
family_document_import_id=fd.import_id,
name=generate_slug(db, document.title),
)
)
except Exception as e:
_LOGGER.error(e)
return
_LOGGER.exception("Error when creating document!")
raise RepositoryError(str(e))

return document

Expand Down
Loading