Skip to content

Commit

Permalink
fix delete bug, add beginnings of config endpoint# (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
diversemix authored Sep 21, 2023
1 parent dfa3079 commit c8ad81e
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 9 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 @@ -2,3 +2,4 @@
from .collection import collections_router
from .auth import auth_router
from .document import document_router
from .config import config_router
5 changes: 5 additions & 0 deletions app/api/api_v1/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/tokens")

# TODO: I don't really like this - we should use maybe https://pypi.org/project/fastapi_auth_middleware/
# PDCT-410


def check_user_auth(request: Request, token: str = Depends(oauth2_scheme)) -> None:
"""
Expand All @@ -35,6 +38,8 @@ def check_user_auth(request: Request, token: str = Depends(oauth2_scheme)) -> No
headers={"WWW-Authenticate": "Bearer"},
)

request.state.user = user


@r.post("/tokens")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
Expand Down
28 changes: 28 additions & 0 deletions app/api/api_v1/routers/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import logging

from fastapi import APIRouter, HTTPException, Request, status
import app.service.config as config_service
from app.errors import RepositoryError

config_router = r = APIRouter()

_LOGGER = logging.getLogger(__file__)


@r.get("/config")
async def get_config(request: Request):
user = request.state.user
_LOGGER.info(f"User {user.email} is getting config")
try:
config = config_service.get()
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

if config is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Config not found"
)

return config
4 changes: 4 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"
CONFIG = "CONFIG"


class AuthAccess(str, enum.Enum):
Expand Down Expand Up @@ -58,4 +59,7 @@ class AuthAccess(str, enum.Enum):
AuthOperation.UPDATE: AuthAccess.USER,
AuthOperation.DELETE: AuthAccess.ADMIN,
},
AuthEntity.CONFIG: {
AuthOperation.READ: AuthAccess.USER,
},
}
8 changes: 8 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
auth_router,
collections_router,
document_router,
config_router,
)
from fastapi import FastAPI, Depends
from fastapi_health import health
Expand All @@ -28,6 +29,13 @@
setup_json_logging(app)
add_pagination(app)

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

app.include_router(
families_router,
prefix="/api/v1",
Expand Down
14 changes: 14 additions & 0 deletions app/model/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Mapping, Sequence, Union

from pydantic import BaseModel


TaxonomyData = Mapping[str, Mapping[str, Union[bool, str, Sequence[str]]]]


class ConfigReadDTO(BaseModel):
"""Definition of the new Config which just includes taxonomy."""

geographies: Sequence[dict]
taxonomies: Mapping[str, TaxonomyData]
languages: Mapping[str, str]
80 changes: 80 additions & 0 deletions app/repository/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import logging
from typing import Any
from sqlalchemy.orm import Session
from app.clients.db.models.app.users import Organisation
from app.clients.db.models.document.physical_document import Language
from app.clients.db.models.law_policy.geography import Geography
from app.clients.db.models.law_policy.metadata import (
MetadataOrganisation,
MetadataTaxonomy,
)
from app.clients.db.session import AnyModel
from app.model.config import ConfigReadDTO, TaxonomyData


_LOGGER = logging.getLogger(__name__)


def _tree_table_to_json(
table: AnyModel,
db: Session,
) -> list[dict]:
json_out = []
child_list_map: dict[int, Any] = {}

for row in db.query(table).order_by(table.id).all():
row_object = {col.name: getattr(row, col.name) for col in row.__table__.columns}
row_children: list[dict[str, Any]] = []
child_list_map[row_object["id"]] = row_children

# No parent indicates a top level element
node_row_object = {"node": row_object, "children": row_children}
node_id = row_object["parent_id"]
if node_id is None:
json_out.append(node_row_object)
else:
append_list = child_list_map.get(node_id)
if append_list is None:
raise RuntimeError(f"Could not locate parent node with id {node_id}")
append_list.append(node_row_object)

return json_out


def _get_organisation_taxonomy_by_name(db: Session, org_name: str) -> TaxonomyData:
"""
Returns the TaxonomyConfig for the named organisation
:param Session db: connection to the database
:return TaxonomyConfig: the TaxonomyConfig from the db
"""
return (
db.query(MetadataTaxonomy.valid_metadata)
.join(
MetadataOrganisation,
MetadataOrganisation.taxonomy_id == MetadataTaxonomy.id,
)
.join(Organisation, Organisation.id == MetadataOrganisation.organisation_id)
.filter_by(name=org_name)
.one()[0]
)


def get(db: Session) -> ConfigReadDTO:
"""
Returns the configuration for the admin service.
:param Session db: connection to the database
:return ConfigReadDTO: The config data
"""

# TODO: Return the event types too
geographies = _tree_table_to_json(table=Geography, db=db)
taxonomies = {
org.name: _get_organisation_taxonomy_by_name(db=db, org_name=org.name)
for org in db.query(Organisation).all()
}
languages = {lang.language_code: lang.name for lang in db.query(Language).all()}
return ConfigReadDTO(
geographies=geographies, taxonomies=taxonomies, languages=languages
)
3 changes: 2 additions & 1 deletion app/repository/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def _family_to_dto(db: Session, fam_geo_meta_org: FamilyGeoMetaOrg) -> FamilyRea
category=str(f.family_category),
status=str(f.family_status),
metadata=metadata,
slug=str(f.slugs[0].name if len(f.slugs) > 0 else ""),
slug=str(f.slugs[-1].name if len(f.slugs) > 0 else ""),
events=[str(e.import_id) for e in f.events],
published_date=f.published_date,
last_updated_date=f.last_updated_date,
Expand Down Expand Up @@ -281,6 +281,7 @@ def delete(db: Session, import_id: str) -> bool:
FamilyOrganisation.family_import_id == import_id
),
db_delete(FamilyMetadata).where(FamilyMetadata.family_import_id == import_id),
db_delete(Slug).where(Slug.family_import_id == import_id),
db_delete(Family).where(Family.import_id == import_id),
]
for c in commands:
Expand Down
18 changes: 18 additions & 0 deletions app/service/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging
from app.errors import RepositoryError
import app.repository.config as config_repo

from sqlalchemy import exc
import app.clients.db.session as db_session


_LOGGER = logging.getLogger(__name__)


def get():
try:
with db_session.get_db() as db:
return config_repo.get(db)
except exc.SQLAlchemyError:
_LOGGER.exception("Error while getting config")
raise RepositoryError("Could not get the config")
7 changes: 6 additions & 1 deletion app/service/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,9 @@ def delete(import_id: str, db: Session = db_session.get_db()) -> bool:
:return bool: True if deleted else False.
"""
id.validate(import_id)
return family_repo.delete(db, import_id)
try:
return family_repo.delete(db, import_id)
except exc.SQLAlchemyError:
msg = f"Unable to delete family {import_id}"
_LOGGER.exception(msg)
raise RepositoryError(msg)
8 changes: 4 additions & 4 deletions integration_tests/family/test_family_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def test_update_family(client: TestClient, test_db: Session, user_header_token):
assert db_family.geography_id == 210
assert db_family.family_category == "UNFCCC"
db_slug = test_db.query(Slug).filter(Slug.family_import_id == "A.0.0.2").all()
assert len(db_slug) == 1
assert str(db_slug[0].name).startswith("updated-title")
assert len(db_slug) == 2
assert str(db_slug[-1].name).startswith("updated-title")


def test_update_family_when_not_authenticated(client: TestClient, test_db: Session):
Expand Down Expand Up @@ -101,8 +101,8 @@ def test_update_family_rollback(
assert db_family.description != "just a test"

db_slug = test_db.query(Slug).filter(Slug.family_import_id == "A.0.0.2").all()
# Ensure no slug was created
assert len(db_slug) == 0
# Ensure no extra slug was created
assert len(db_slug) == 1

db_meta = (
test_db.query(FamilyMetadata)
Expand Down
14 changes: 11 additions & 3 deletions integration_tests/setup_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
Family,
FamilyCategory,
FamilyOrganisation,
Slug,
)
from app.clients.db.models.law_policy.metadata import (
FamilyMetadata,
MetadataOrganisation,
MetadataTaxonomy,
)

# TODO: Change this to use the service.family.create - so we don't miss anything here

EXPECTED_FAMILIES = [
{
Expand All @@ -29,7 +31,7 @@
"status": "Created",
"metadata": {"size": [3], "color": ["red"]},
"organisation": "test_org",
"slug": "",
"slug": "Slug1",
"events": [],
"published_date": None,
"last_updated_date": None,
Expand All @@ -45,7 +47,7 @@
"status": "Created",
"metadata": {"size": [4], "color": ["green"]},
"organisation": "test_org",
"slug": "",
"slug": "Slug2",
"events": [],
"published_date": None,
"last_updated_date": None,
Expand All @@ -61,7 +63,7 @@
"status": "Created",
"metadata": {"size": [100], "color": ["blue"]},
"organisation": "test_org",
"slug": "",
"slug": "Slug3",
"events": [],
"published_date": None,
"last_updated_date": None,
Expand Down Expand Up @@ -212,3 +214,9 @@ def _setup_family_data(test_db: Session, org_id: int):
value=EXPECTED_FAMILIES[index]["metadata"],
)
)
test_db.add(
Slug(
name=f"Slug{index+1}",
family_import_id=EXPECTED_FAMILIES[index]["import_id"],
)
)
19 changes: 19 additions & 0 deletions integration_tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi.testclient import TestClient
from fastapi import status
from sqlalchemy.orm import Session
from integration_tests.setup_db import setup_db


def test_get_config(client: TestClient, test_db: Session, user_header_token):
setup_db(test_db)

response = client.get(
"/api/v1/config",
headers=user_header_token,
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
keys = data.keys()
assert "geographies" in keys
assert "taxonomies" in keys
assert "languages" in keys
6 changes: 6 additions & 0 deletions unit_tests/routers/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from fastapi.testclient import TestClient


def test_get_config(client: TestClient, user_header_token):
# TODO: Finish the config tests by adding mock for repo
pass

0 comments on commit c8ad81e

Please sign in to comment.