Skip to content

Commit cd8d332

Browse files
authored
PDCT-310 Added get, search, and count for family events (#23)
1 parent 7ae4b2f commit cd8d332

26 files changed

+911
-31
lines changed

app/api/api_v1/routers/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from .document import document_router
55
from .config import config_router
66
from .analytics import analytics_router
7+
from .event import event_router

app/api/api_v1/routers/event.py

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Endpoints for managing Family Event entities."""
2+
import logging
3+
4+
from fastapi import APIRouter, HTTPException, status
5+
6+
import app.service.event as event_service
7+
from app.errors import RepositoryError, ValidationError
8+
from app.model.event import EventReadDTO
9+
10+
event_router = r = APIRouter()
11+
12+
_LOGGER = logging.getLogger(__name__)
13+
14+
15+
@r.get(
16+
"/events",
17+
response_model=list[EventReadDTO],
18+
)
19+
async def get_all_events() -> list[EventReadDTO]:
20+
"""
21+
Returns all family events.
22+
23+
:return EventDTO: returns a EventDTO if the event is found.
24+
"""
25+
found_events = event_service.all()
26+
27+
if not found_events:
28+
raise HTTPException(
29+
status_code=status.HTTP_404_NOT_FOUND,
30+
detail="No family events found",
31+
)
32+
33+
return found_events
34+
35+
36+
@r.get(
37+
"/events/",
38+
response_model=list[EventReadDTO],
39+
)
40+
async def search_event(q: str = "") -> list[EventReadDTO]:
41+
"""
42+
Searches for family events matching the "q" URL parameter.
43+
44+
:param str q: The string to match, defaults to ""
45+
:raises HTTPException: If nothing found a 404 is returned.
46+
:return list[EventDTO]: A list of matching events.
47+
"""
48+
try:
49+
events_found = event_service.search(q)
50+
except RepositoryError as e:
51+
raise HTTPException(
52+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
53+
)
54+
55+
if not events_found:
56+
raise HTTPException(
57+
status_code=status.HTTP_404_NOT_FOUND,
58+
detail=f"Events not found for term: {q}",
59+
)
60+
61+
return events_found
62+
63+
64+
@r.get(
65+
"/events/{import_id}",
66+
response_model=EventReadDTO,
67+
)
68+
async def get_event(import_id: str) -> EventReadDTO:
69+
"""
70+
Returns a specific family event given an import id.
71+
72+
:param str import_id: Specified import_id.
73+
:raises HTTPException: If the event is not found a 404 is returned.
74+
:return EventDTO: returns a EventDTO if the event is found.
75+
"""
76+
try:
77+
event = event_service.get(import_id)
78+
except ValidationError as e:
79+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e.message)
80+
except RepositoryError as e:
81+
raise HTTPException(
82+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
83+
)
84+
85+
if event is None:
86+
raise HTTPException(
87+
status_code=status.HTTP_404_NOT_FOUND,
88+
detail=f"Event not found: {import_id}",
89+
)
90+
91+
return event

app/clients/db/models/app/authorisation.py

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class AuthEndpoint(str, enum.Enum):
3434
DOCUMENT = "DOCUMENTS"
3535
CONFIG = "CONFIG"
3636
ANALYTICS = "ANALYTICS"
37+
EVENT = "EVENTS"
3738

3839

3940
class AuthAccess(str, enum.Enum):
@@ -74,6 +75,10 @@ class AuthAccess(str, enum.Enum):
7475
},
7576
# Analytics
7677
AuthEndpoint.ANALYTICS: {
78+
AuthOperation.READ: AuthAccess.USER,
79+
},
80+
# Event
81+
AuthEndpoint.EVENT: {
7782
AuthOperation.CREATE: AuthAccess.USER,
7883
AuthOperation.READ: AuthAccess.USER,
7984
AuthOperation.UPDATE: AuthAccess.USER,

app/main.py

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
document_router,
1515
config_router,
1616
analytics_router,
17+
event_router,
1718
)
1819
from fastapi import FastAPI, Depends
1920
from fastapi_health import health
@@ -72,6 +73,13 @@
7273
dependencies=[Depends(check_user_auth)],
7374
)
7475

76+
app.include_router(
77+
event_router,
78+
prefix="/api/v1",
79+
tags=["events"],
80+
dependencies=[Depends(check_user_auth)],
81+
)
82+
7583
app.include_router(auth_router, prefix="/api", tags=["Authentication"])
7684

7785
# Add CORS middleware to allow cross origin requests from any port

app/model/event.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from datetime import datetime
2+
from pydantic import BaseModel
3+
from typing import Optional
4+
5+
from app.clients.db.models.law_policy.family import (
6+
EventStatus,
7+
)
8+
9+
10+
class EventReadDTO(BaseModel):
11+
"""JSON Representation of a Event for reading."""
12+
13+
# From FamilyEvent
14+
import_id: str
15+
event_title: str
16+
date: datetime
17+
event_type_value: str
18+
family_import_id: str
19+
family_document_import_id: Optional[str] = None
20+
event_status: EventStatus

app/repository/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import app.repository.app_user as app_user_repo
88
import app.clients.aws.s3bucket as s3bucket_repo
99
import app.repository.config as config_repo
10+
import app.repository.event as event_repo
1011
from app.repository.protocols import FamilyRepo
1112

1213
family_repo: FamilyRepo

app/repository/event.py

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Operations on the repository for the Family entity."""
2+
3+
import logging
4+
from datetime import datetime
5+
from typing import Optional, Tuple, cast
6+
7+
from sqlalchemy import or_
8+
from sqlalchemy.orm import Query, Session
9+
from sqlalchemy.exc import NoResultFound
10+
from sqlalchemy_utils import escape_like
11+
12+
from app.clients.db.models.law_policy import (
13+
EventStatus,
14+
FamilyEvent,
15+
Family,
16+
FamilyDocument,
17+
)
18+
from app.model.event import EventReadDTO
19+
20+
21+
_LOGGER = logging.getLogger(__name__)
22+
23+
FamilyEventTuple = Tuple[FamilyEvent, Family, FamilyDocument]
24+
25+
26+
def _get_query(db: Session) -> Query:
27+
# NOTE: SqlAlchemy will make a complete hash of the query generation
28+
# if columns are used in the query() call. Therefore, entire
29+
# objects are returned.
30+
return (
31+
db.query(FamilyEvent, Family, FamilyDocument)
32+
.filter(FamilyEvent.family_import_id == Family.import_id)
33+
.join(
34+
FamilyDocument,
35+
FamilyDocument.family_import_id == FamilyEvent.family_document_import_id,
36+
isouter=True,
37+
)
38+
)
39+
40+
41+
def _event_to_dto(db: Session, family_event_meta: FamilyEventTuple) -> EventReadDTO:
42+
family_event = family_event_meta[0]
43+
44+
family_document_import_id = (
45+
None
46+
if family_event.family_document_import_id is None
47+
else cast(str, family_event.family_document_import_id)
48+
)
49+
50+
return EventReadDTO(
51+
import_id=cast(str, family_event.import_id),
52+
event_title=cast(str, family_event.title),
53+
date=cast(datetime, family_event.date),
54+
family_import_id=cast(str, family_event.family_import_id),
55+
family_document_import_id=family_document_import_id,
56+
event_type_value=cast(str, family_event.event_type_name),
57+
event_status=cast(EventStatus, family_event.status),
58+
)
59+
60+
61+
def all(db: Session) -> list[EventReadDTO]:
62+
"""
63+
Returns all family events.
64+
65+
:param db Session: The database connection.
66+
:return Optional[EventReadDTO]: All family events in the database.
67+
"""
68+
family_event_metas = _get_query(db).all()
69+
70+
if not family_event_metas:
71+
return []
72+
73+
result = [_event_to_dto(db, event_meta) for event_meta in family_event_metas]
74+
return result
75+
76+
77+
def get(db: Session, import_id: str) -> Optional[EventReadDTO]:
78+
"""
79+
Gets a single family event from the repository.
80+
81+
:param db Session: The database connection.
82+
:param str import_id: The import_id of the event.
83+
:return Optional[EventReadDTO]: A single family event or nothing.
84+
"""
85+
try:
86+
family_event_meta = (
87+
_get_query(db).filter(FamilyEvent.import_id == import_id).one()
88+
)
89+
except NoResultFound as e:
90+
_LOGGER.error(e)
91+
return
92+
93+
return _event_to_dto(db, family_event_meta)
94+
95+
96+
def search(db: Session, search_term: str) -> Optional[list[EventReadDTO]]:
97+
"""
98+
Get family events matching a search term on the event title or type.
99+
100+
:param db Session: The database connection.
101+
:param str search_term: Any search term to filter on the event title
102+
or event type name.
103+
:return Optional[list[EventReadDTO]]: A list of matching family
104+
events or none.
105+
"""
106+
term = f"%{escape_like(search_term)}%"
107+
search = or_(FamilyEvent.title.ilike(term), FamilyEvent.event_type_name.ilike(term))
108+
109+
try:
110+
found = _get_query(db).filter(search).all()
111+
except NoResultFound as e:
112+
_LOGGER.error(e)
113+
return
114+
115+
if not found:
116+
return []
117+
118+
return [_event_to_dto(db, f) for f in found]
119+
120+
121+
def count(db: Session) -> Optional[int]:
122+
"""
123+
Counts the number of family events in the repository.
124+
125+
:param db Session: The database connection.
126+
:return Optional[int]: The number of family events in the repository
127+
or nothing.
128+
"""
129+
try:
130+
n_events = _get_query(db).count()
131+
except NoResultFound as e:
132+
_LOGGER.error(e)
133+
return
134+
135+
return n_events

app/service/analytics.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import logging
88

99
from pydantic import ConfigDict, validate_call
10+
from sqlalchemy import exc
11+
1012
from app.errors import RepositoryError
1113
from app.model.analytics import SummaryDTO
1214
import app.service.collection as collection_service
1315
import app.service.document as document_service
1416
import app.service.family as family_service
15-
from sqlalchemy import exc
17+
import app.service.event as event_service
1618

1719

1820
_LOGGER = logging.getLogger(__name__)
@@ -29,7 +31,7 @@ def summary() -> SummaryDTO:
2931
n_collections = collection_service.count()
3032
n_families = family_service.count()
3133
n_documents = document_service.count()
32-
n_events = 0
34+
n_events = event_service.count()
3335

3436
return SummaryDTO(
3537
n_documents=n_documents,

0 commit comments

Comments
 (0)