Skip to content

Commit

Permalink
PDCT-310 Added get, search, and count for family events (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
katybaulch authored Oct 18, 2023
1 parent 7ae4b2f commit cd8d332
Show file tree
Hide file tree
Showing 26 changed files with 911 additions and 31 deletions.
1 change: 1 addition & 0 deletions app/api/api_v1/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .document import document_router
from .config import config_router
from .analytics import analytics_router
from .event import event_router
91 changes: 91 additions & 0 deletions app/api/api_v1/routers/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Endpoints for managing Family Event entities."""
import logging

from fastapi import APIRouter, HTTPException, status

import app.service.event as event_service
from app.errors import RepositoryError, ValidationError
from app.model.event import EventReadDTO

event_router = r = APIRouter()

_LOGGER = logging.getLogger(__name__)


@r.get(
"/events",
response_model=list[EventReadDTO],
)
async def get_all_events() -> list[EventReadDTO]:
"""
Returns all family events.
:return EventDTO: returns a EventDTO if the event is found.
"""
found_events = event_service.all()

if not found_events:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No family events found",
)

return found_events


@r.get(
"/events/",
response_model=list[EventReadDTO],
)
async def search_event(q: str = "") -> list[EventReadDTO]:
"""
Searches for family events 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[EventDTO]: A list of matching events.
"""
try:
events_found = event_service.search(q)
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

if not events_found:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Events not found for term: {q}",
)

return events_found


@r.get(
"/events/{import_id}",
response_model=EventReadDTO,
)
async def get_event(import_id: str) -> EventReadDTO:
"""
Returns a specific family event given an import id.
:param str import_id: Specified import_id.
:raises HTTPException: If the event is not found a 404 is returned.
:return EventDTO: returns a EventDTO if the event is found.
"""
try:
event = event_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 event is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Event not found: {import_id}",
)

return event
5 changes: 5 additions & 0 deletions app/clients/db/models/app/authorisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class AuthEndpoint(str, enum.Enum):
DOCUMENT = "DOCUMENTS"
CONFIG = "CONFIG"
ANALYTICS = "ANALYTICS"
EVENT = "EVENTS"


class AuthAccess(str, enum.Enum):
Expand Down Expand Up @@ -74,6 +75,10 @@ class AuthAccess(str, enum.Enum):
},
# Analytics
AuthEndpoint.ANALYTICS: {
AuthOperation.READ: AuthAccess.USER,
},
# Event
AuthEndpoint.EVENT: {
AuthOperation.CREATE: AuthAccess.USER,
AuthOperation.READ: AuthAccess.USER,
AuthOperation.UPDATE: AuthAccess.USER,
Expand Down
8 changes: 8 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
document_router,
config_router,
analytics_router,
event_router,
)
from fastapi import FastAPI, Depends
from fastapi_health import health
Expand Down Expand Up @@ -72,6 +73,13 @@
dependencies=[Depends(check_user_auth)],
)

app.include_router(
event_router,
prefix="/api/v1",
tags=["events"],
dependencies=[Depends(check_user_auth)],
)

app.include_router(auth_router, prefix="/api", tags=["Authentication"])

# Add CORS middleware to allow cross origin requests from any port
Expand Down
20 changes: 20 additions & 0 deletions app/model/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from datetime import datetime
from pydantic import BaseModel
from typing import Optional

from app.clients.db.models.law_policy.family import (
EventStatus,
)


class EventReadDTO(BaseModel):
"""JSON Representation of a Event for reading."""

# From FamilyEvent
import_id: str
event_title: str
date: datetime
event_type_value: str
family_import_id: str
family_document_import_id: Optional[str] = None
event_status: EventStatus
1 change: 1 addition & 0 deletions app/repository/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import app.repository.app_user as app_user_repo
import app.clients.aws.s3bucket as s3bucket_repo
import app.repository.config as config_repo
import app.repository.event as event_repo
from app.repository.protocols import FamilyRepo

family_repo: FamilyRepo
135 changes: 135 additions & 0 deletions app/repository/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Operations on the repository for the Family entity."""

import logging
from datetime import datetime
from typing import Optional, Tuple, cast

from sqlalchemy import or_
from sqlalchemy.orm import Query, Session
from sqlalchemy.exc import NoResultFound
from sqlalchemy_utils import escape_like

from app.clients.db.models.law_policy import (
EventStatus,
FamilyEvent,
Family,
FamilyDocument,
)
from app.model.event import EventReadDTO


_LOGGER = logging.getLogger(__name__)

FamilyEventTuple = Tuple[FamilyEvent, Family, FamilyDocument]


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(FamilyEvent, Family, FamilyDocument)
.filter(FamilyEvent.family_import_id == Family.import_id)
.join(
FamilyDocument,
FamilyDocument.family_import_id == FamilyEvent.family_document_import_id,
isouter=True,
)
)


def _event_to_dto(db: Session, family_event_meta: FamilyEventTuple) -> EventReadDTO:
family_event = family_event_meta[0]

family_document_import_id = (
None
if family_event.family_document_import_id is None
else cast(str, family_event.family_document_import_id)
)

return EventReadDTO(
import_id=cast(str, family_event.import_id),
event_title=cast(str, family_event.title),
date=cast(datetime, family_event.date),
family_import_id=cast(str, family_event.family_import_id),
family_document_import_id=family_document_import_id,
event_type_value=cast(str, family_event.event_type_name),
event_status=cast(EventStatus, family_event.status),
)


def all(db: Session) -> list[EventReadDTO]:
"""
Returns all family events.
:param db Session: The database connection.
:return Optional[EventReadDTO]: All family events in the database.
"""
family_event_metas = _get_query(db).all()

if not family_event_metas:
return []

result = [_event_to_dto(db, event_meta) for event_meta in family_event_metas]
return result


def get(db: Session, import_id: str) -> Optional[EventReadDTO]:
"""
Gets a single family event from the repository.
:param db Session: The database connection.
:param str import_id: The import_id of the event.
:return Optional[EventReadDTO]: A single family event or nothing.
"""
try:
family_event_meta = (
_get_query(db).filter(FamilyEvent.import_id == import_id).one()
)
except NoResultFound as e:
_LOGGER.error(e)
return

return _event_to_dto(db, family_event_meta)


def search(db: Session, search_term: str) -> Optional[list[EventReadDTO]]:
"""
Get family events matching a search term on the event title or type.
:param db Session: The database connection.
:param str search_term: Any search term to filter on the event title
or event type name.
:return Optional[list[EventReadDTO]]: A list of matching family
events or none.
"""
term = f"%{escape_like(search_term)}%"
search = or_(FamilyEvent.title.ilike(term), FamilyEvent.event_type_name.ilike(term))

try:
found = _get_query(db).filter(search).all()
except NoResultFound as e:
_LOGGER.error(e)
return

if not found:
return []

return [_event_to_dto(db, f) for f in found]


def count(db: Session) -> Optional[int]:
"""
Counts the number of family events in the repository.
:param db Session: The database connection.
:return Optional[int]: The number of family events in the repository
or nothing.
"""
try:
n_events = _get_query(db).count()
except NoResultFound as e:
_LOGGER.error(e)
return

return n_events
6 changes: 4 additions & 2 deletions app/service/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import logging

from pydantic import ConfigDict, validate_call
from sqlalchemy import exc

from app.errors import RepositoryError
from app.model.analytics import SummaryDTO
import app.service.collection as collection_service
import app.service.document as document_service
import app.service.family as family_service
from sqlalchemy import exc
import app.service.event as event_service


_LOGGER = logging.getLogger(__name__)
Expand All @@ -29,7 +31,7 @@ def summary() -> SummaryDTO:
n_collections = collection_service.count()
n_families = family_service.count()
n_documents = document_service.count()
n_events = 0
n_events = event_service.count()

return SummaryDTO(
n_documents=n_documents,
Expand Down
Loading

0 comments on commit cd8d332

Please sign in to comment.