Skip to content

Commit

Permalink
Implemented support for api create/get feature requests by slug or id.
Browse files Browse the repository at this point in the history
  • Loading branch information
garytyler committed Apr 5, 2020
1 parent f31bee1 commit 8ad0dd9
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 86 deletions.
13 changes: 13 additions & 0 deletions backend/app/api/dependencies/features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Dict

from app.services.features import feature_with_slug_exists, generate_unique_feature_slug


async def validate_feature_slug(body: Dict[str, str]) -> Dict[str, str]:

slug_in = body.get("slug", "").strip()
if not slug_in:
body["slug"] = await generate_unique_feature_slug(title=body["title"])
elif await feature_with_slug_exists(slug_in):
raise ValueError(f"Feature with slug '{slug_in}' already exists")
return body
20 changes: 14 additions & 6 deletions backend/app/api/http/endpoints/features.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import uuid

from app.api.dependencies.features import validate_feature_slug
from app.crud.features import crud_feature
from app.schemas.features import FeatureCreate, FeatureOut
from fastapi import HTTPException
from fastapi.routing import APIRouter
from fastapi import APIRouter, Depends, HTTPException

router = APIRouter()


@router.post("/features", response_model=FeatureOut)
async def create_feature(feature_in: FeatureCreate):
async def create_feature(feature_in: FeatureCreate = Depends(validate_feature_slug)):
feature = await crud_feature.create(obj_in=feature_in)
if feature:
await feature.fetch_related("guests")
Expand All @@ -16,9 +18,15 @@ async def create_feature(feature_in: FeatureCreate):
return feature


@router.get("/features/{feature_id}", response_model=FeatureOut)
async def get_feature(feature_id: str):
feature = await crud_feature.get(id=feature_id)
@router.get("/features/{id_or_slug}", response_model=FeatureOut)
async def get_feature(id_or_slug: str):
try:
id_or_slug = uuid.UUID(id_or_slug, version=4)
except ValueError:
feature = await crud_feature.get_by_slug(slug=id_or_slug)
else:
feature = await crud_feature.get(id=id_or_slug)

if feature:
await feature.fetch_related("guests")
else:
Expand Down
3 changes: 2 additions & 1 deletion backend/app/crud/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from typing import Generic, Optional, Type, TypeVar

from app.models.base import CustomTortoiseBase
Expand All @@ -15,7 +16,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
self.model = model

async def get(self, id: str) -> Optional[ModelType]:
async def get(self, id: uuid.UUID) -> Optional[ModelType]:
return await self.model.filter(id=id).first()

# async def get_multi(self, *, skip=0, limit=100) -> List[ModelType]:
Expand Down
5 changes: 4 additions & 1 deletion backend/app/crud/features.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from typing import Optional

from app.crud.base import CRUDBase
from app.models.features import Feature
from app.schemas.features import FeatureCreate, FeatureUpdate


class CRUDFeature(CRUDBase[Feature, FeatureCreate, FeatureUpdate]):
pass
async def get_by_slug(self, slug: str) -> Optional[Feature]:
return await self.model.filter(slug=slug).first()


crud_feature = CRUDFeature(Feature)
16 changes: 1 addition & 15 deletions backend/app/schemas/features.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
from datetime import timedelta
from typing import Optional

from app.models.features import Feature
from app.services.features import feature_with_slug_exists, generate_unique_feature_slug
from pydantic import validator
from tortoise import Tortoise
from tortoise.contrib.pydantic import pydantic_model_creator

Expand All @@ -12,20 +9,9 @@

class FeatureCreate(CustomPydanticBase):
title: str
slug: Optional[str]
slug: str
turn_duration: int = 180

@validator("slug", always=True)
@classmethod
def validate_or_generate_slug(
cls, slug, values,
):
if not slug:
slug = generate_unique_feature_slug(title=values["title"])
elif feature_with_slug_exists(slug):
raise ValueError(f"Feature with slug '{slug}' already exists")
return slug


class FeatureUpdate(CustomPydanticBase):
title: str
Expand Down
12 changes: 6 additions & 6 deletions backend/app/services/features.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from app.models.features import Feature
from slugify import slugify # type: ignore


def feature_with_slug_exists(slug: str) -> bool:
# TODO Check for slug uniqueness
return False
async def feature_with_slug_exists(slug: str) -> bool:
return bool(await Feature.get_or_none(slug=slug))


def generate_unique_feature_slug(title: str) -> str:
async def generate_unique_feature_slug(title: str) -> str:
slugified_title = slugify(title, max_length=50, word_boundary=True)
if not feature_with_slug_exists(slugified_title):
if not await feature_with_slug_exists(slugified_title):
return slugified_title
for index in range(99):
numbered_slugified_title = f"{slugified_title[:47]}-{index}"
if not feature_with_slug_exists(numbered_slugified_title):
if not await feature_with_slug_exists(numbered_slugified_title):
return numbered_slugified_title
raise ValueError("Error generating slug")
50 changes: 25 additions & 25 deletions backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ max-complexity=18
select=B,C,E,F,W,T4

[mypy]
files=app,live,tests
files=app,tests
ignore_missing_imports=true
plugins = pydantic.mypy
11 changes: 8 additions & 3 deletions backend/tests/crud/test_feature_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
from app.crud.features import crud_feature
from app.schemas.features import FeatureCreate
from async_asgi_testclient import TestClient
from tests._utils.features import create_random_feature, create_random_feature_title
from tests._utils.features import (
create_random_feature,
create_random_feature_slug,
create_random_feature_title,
)
from tests._utils.guests import create_random_guest


Expand All @@ -11,6 +15,7 @@ async def test_feature_crud_create(app):
async with TestClient(app):
params = {
"title": create_random_feature_title(),
"slug": create_random_feature_slug(),
"turn_duration": 90,
}
feature_in = FeatureCreate(**params)
Expand All @@ -20,7 +25,7 @@ async def test_feature_crud_create(app):


@pytest.mark.asyncio
async def test_feature_crud_get(app,):
async def test_feature_crud_get(app):
async with TestClient(app):
created_feature = await create_random_feature()
gotten_feature = await crud_feature.get(id=created_feature.id)
Expand All @@ -32,7 +37,7 @@ async def test_feature_crud_get(app,):

@pytest.mark.skip
@pytest.mark.asyncio
async def test_feature_crud_get_guests(app,):
async def test_feature_crud_get_guests(app):

async with TestClient(app):
created_feature = await create_random_feature()
Expand Down
50 changes: 45 additions & 5 deletions backend/tests/http/test_feature_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,79 @@
import pytest
from app.models.features import Feature
from async_asgi_testclient import TestClient
from tests._utils.features import create_random_feature, create_random_feature_title
from tests._utils.features import (
create_random_feature,
create_random_feature_slug,
create_random_feature_title,
)
from tests._utils.guests import create_random_guest


@pytest.mark.asyncio
async def test_feature_http_post(app):
async def test_features_http_post_with_custom_slug(app):
path = "/api/features"
async with TestClient(app) as client:
data = {
"title": create_random_feature_title(),
"slug": create_random_feature_slug(),
"turn_duration": randint(0, 99),
}
response = await client.post(path, json=data)
assert response.status_code == 200
content = response.json()
assert content["title"] == data["title"]
assert content["slug"] == data["slug"]
assert content["turn_duration"] == data["turn_duration"]
assert "id" in content
feature = await Feature.get_or_none(pk=content["id"])
assert feature
assert feature.title == data["title"]
assert feature.slug == data["slug"]
assert feature.turn_duration == data["turn_duration"]


@pytest.mark.asyncio
async def test_feature_http_get(app):
path = "/api/features/{feature_id}"
async def test_features_http_post_without_custom_slug(app):
path = "/api/features"
async with TestClient(app) as client:
data = {"title": create_random_feature_title(), "turn_duration": randint(0, 99)}
response = await client.post(path, json=data)
assert response.status_code == 200
content = response.json()
assert content["slug"].strip()
assert content["title"] == data["title"]
assert content["turn_duration"] == data["turn_duration"]
assert "id" in content
feature = await Feature.get_or_none(pk=content["id"])
assert feature
assert feature.title == data["title"]
assert feature.turn_duration == data["turn_duration"]


@pytest.mark.asyncio
async def test_features_http_get_by_id(app):
path = "/api/features/{id}"
async with TestClient(app) as client:
random_feature = await create_random_feature()
await create_random_guest(random_feature)
await create_random_guest(random_feature)
await create_random_guest(random_feature)
response = await client.get(path.format(id=random_feature.id))
assert response.status_code == 200
content = response.json()
assert content["title"] == random_feature.title
assert content["turn_duration"] == random_feature.turn_duration


@pytest.mark.asyncio
async def test_features_http_get_by_slug(app):
path = "/api/features/{slug}"
async with TestClient(app) as client:
random_feature = await create_random_feature()
await create_random_guest(random_feature)
await create_random_guest(random_feature)
await create_random_guest(random_feature)
response = await client.get(path.format(feature_id=random_feature.id))
response = await client.get(path.format(slug=random_feature.slug))
assert response.status_code == 200
content = response.json()
assert content["title"] == random_feature.title
Expand Down
24 changes: 1 addition & 23 deletions backend/tests/schemas/test_feature_schemas.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
import pytest
from app.models.features import Feature
from app.schemas.features import FeatureCreate, FeatureOut
from app.schemas.features import FeatureOut
from async_asgi_testclient import TestClient
from tests._utils.features import create_random_feature
from tests._utils.guests import create_random_guest
from tests._utils.strings import create_random_string


@pytest.mark.asyncio
async def test_schemas_feature_create_autogenerates_slug(app):
async with TestClient(app):
title = create_random_string(
min_length=5,
max_length=20,
min_words=2,
max_words=6,
uppercase_letters=True,
lowercase_letters=True,
numbers=True,
)
feature_in = FeatureCreate(title=title)
feature = await Feature.create(**feature_in.dict())
assert feature.title == title
assert len(feature.slug)
assert len(feature.slug.split()) == 1
assert len(feature.slug.split()) < len(title.split())


@pytest.mark.asyncio
Expand Down

0 comments on commit 8ad0dd9

Please sign in to comment.