Skip to content

Commit 8ad0dd9

Browse files
committed
Implemented support for api create/get feature requests by slug or id.
1 parent f31bee1 commit 8ad0dd9

File tree

11 files changed

+120
-86
lines changed

11 files changed

+120
-86
lines changed
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Dict
2+
3+
from app.services.features import feature_with_slug_exists, generate_unique_feature_slug
4+
5+
6+
async def validate_feature_slug(body: Dict[str, str]) -> Dict[str, str]:
7+
8+
slug_in = body.get("slug", "").strip()
9+
if not slug_in:
10+
body["slug"] = await generate_unique_feature_slug(title=body["title"])
11+
elif await feature_with_slug_exists(slug_in):
12+
raise ValueError(f"Feature with slug '{slug_in}' already exists")
13+
return body

backend/app/api/http/endpoints/features.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import uuid
2+
3+
from app.api.dependencies.features import validate_feature_slug
14
from app.crud.features import crud_feature
25
from app.schemas.features import FeatureCreate, FeatureOut
3-
from fastapi import HTTPException
4-
from fastapi.routing import APIRouter
6+
from fastapi import APIRouter, Depends, HTTPException
57

68
router = APIRouter()
79

810

911
@router.post("/features", response_model=FeatureOut)
10-
async def create_feature(feature_in: FeatureCreate):
12+
async def create_feature(feature_in: FeatureCreate = Depends(validate_feature_slug)):
1113
feature = await crud_feature.create(obj_in=feature_in)
1214
if feature:
1315
await feature.fetch_related("guests")
@@ -16,9 +18,15 @@ async def create_feature(feature_in: FeatureCreate):
1618
return feature
1719

1820

19-
@router.get("/features/{feature_id}", response_model=FeatureOut)
20-
async def get_feature(feature_id: str):
21-
feature = await crud_feature.get(id=feature_id)
21+
@router.get("/features/{id_or_slug}", response_model=FeatureOut)
22+
async def get_feature(id_or_slug: str):
23+
try:
24+
id_or_slug = uuid.UUID(id_or_slug, version=4)
25+
except ValueError:
26+
feature = await crud_feature.get_by_slug(slug=id_or_slug)
27+
else:
28+
feature = await crud_feature.get(id=id_or_slug)
29+
2230
if feature:
2331
await feature.fetch_related("guests")
2432
else:

backend/app/crud/base.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import uuid
12
from typing import Generic, Optional, Type, TypeVar
23

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

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

2122
# async def get_multi(self, *, skip=0, limit=100) -> List[ModelType]:

backend/app/crud/features.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from typing import Optional
2+
13
from app.crud.base import CRUDBase
24
from app.models.features import Feature
35
from app.schemas.features import FeatureCreate, FeatureUpdate
46

57

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

912

1013
crud_feature = CRUDFeature(Feature)

backend/app/schemas/features.py

+1-15
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
from datetime import timedelta
2-
from typing import Optional
32

43
from app.models.features import Feature
5-
from app.services.features import feature_with_slug_exists, generate_unique_feature_slug
6-
from pydantic import validator
74
from tortoise import Tortoise
85
from tortoise.contrib.pydantic import pydantic_model_creator
96

@@ -12,20 +9,9 @@
129

1310
class FeatureCreate(CustomPydanticBase):
1411
title: str
15-
slug: Optional[str]
12+
slug: str
1613
turn_duration: int = 180
1714

18-
@validator("slug", always=True)
19-
@classmethod
20-
def validate_or_generate_slug(
21-
cls, slug, values,
22-
):
23-
if not slug:
24-
slug = generate_unique_feature_slug(title=values["title"])
25-
elif feature_with_slug_exists(slug):
26-
raise ValueError(f"Feature with slug '{slug}' already exists")
27-
return slug
28-
2915

3016
class FeatureUpdate(CustomPydanticBase):
3117
title: str

backend/app/services/features.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1+
from app.models.features import Feature
12
from slugify import slugify # type: ignore
23

34

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

88

9-
def generate_unique_feature_slug(title: str) -> str:
9+
async def generate_unique_feature_slug(title: str) -> str:
1010
slugified_title = slugify(title, max_length=50, word_boundary=True)
11-
if not feature_with_slug_exists(slugified_title):
11+
if not await feature_with_slug_exists(slugified_title):
1212
return slugified_title
1313
for index in range(99):
1414
numbered_slugified_title = f"{slugified_title[:47]}-{index}"
15-
if not feature_with_slug_exists(numbered_slugified_title):
15+
if not await feature_with_slug_exists(numbered_slugified_title):
1616
return numbered_slugified_title
1717
raise ValueError("Error generating slug")

backend/poetry.lock

+25-25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ max-complexity=18
1212
select=B,C,E,F,W,T4
1313

1414
[mypy]
15-
files=app,live,tests
15+
files=app,tests
1616
ignore_missing_imports=true
1717
plugins = pydantic.mypy

backend/tests/crud/test_feature_crud.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
from app.crud.features import crud_feature
33
from app.schemas.features import FeatureCreate
44
from async_asgi_testclient import TestClient
5-
from tests._utils.features import create_random_feature, create_random_feature_title
5+
from tests._utils.features import (
6+
create_random_feature,
7+
create_random_feature_slug,
8+
create_random_feature_title,
9+
)
610
from tests._utils.guests import create_random_guest
711

812

@@ -11,6 +15,7 @@ async def test_feature_crud_create(app):
1115
async with TestClient(app):
1216
params = {
1317
"title": create_random_feature_title(),
18+
"slug": create_random_feature_slug(),
1419
"turn_duration": 90,
1520
}
1621
feature_in = FeatureCreate(**params)
@@ -20,7 +25,7 @@ async def test_feature_crud_create(app):
2025

2126

2227
@pytest.mark.asyncio
23-
async def test_feature_crud_get(app,):
28+
async def test_feature_crud_get(app):
2429
async with TestClient(app):
2530
created_feature = await create_random_feature()
2631
gotten_feature = await crud_feature.get(id=created_feature.id)
@@ -32,7 +37,7 @@ async def test_feature_crud_get(app,):
3237

3338
@pytest.mark.skip
3439
@pytest.mark.asyncio
35-
async def test_feature_crud_get_guests(app,):
40+
async def test_feature_crud_get_guests(app):
3641

3742
async with TestClient(app):
3843
created_feature = await create_random_feature()

backend/tests/http/test_feature_endpoints.py

+45-5
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,79 @@
33
import pytest
44
from app.models.features import Feature
55
from async_asgi_testclient import TestClient
6-
from tests._utils.features import create_random_feature, create_random_feature_title
6+
from tests._utils.features import (
7+
create_random_feature,
8+
create_random_feature_slug,
9+
create_random_feature_title,
10+
)
711
from tests._utils.guests import create_random_guest
812

913

1014
@pytest.mark.asyncio
11-
async def test_feature_http_post(app):
15+
async def test_features_http_post_with_custom_slug(app):
1216
path = "/api/features"
1317
async with TestClient(app) as client:
1418
data = {
1519
"title": create_random_feature_title(),
20+
"slug": create_random_feature_slug(),
1621
"turn_duration": randint(0, 99),
1722
}
1823
response = await client.post(path, json=data)
1924
assert response.status_code == 200
2025
content = response.json()
2126
assert content["title"] == data["title"]
27+
assert content["slug"] == data["slug"]
2228
assert content["turn_duration"] == data["turn_duration"]
2329
assert "id" in content
2430
feature = await Feature.get_or_none(pk=content["id"])
2531
assert feature
2632
assert feature.title == data["title"]
33+
assert feature.slug == data["slug"]
2734
assert feature.turn_duration == data["turn_duration"]
2835

2936

3037
@pytest.mark.asyncio
31-
async def test_feature_http_get(app):
32-
path = "/api/features/{feature_id}"
38+
async def test_features_http_post_without_custom_slug(app):
39+
path = "/api/features"
40+
async with TestClient(app) as client:
41+
data = {"title": create_random_feature_title(), "turn_duration": randint(0, 99)}
42+
response = await client.post(path, json=data)
43+
assert response.status_code == 200
44+
content = response.json()
45+
assert content["slug"].strip()
46+
assert content["title"] == data["title"]
47+
assert content["turn_duration"] == data["turn_duration"]
48+
assert "id" in content
49+
feature = await Feature.get_or_none(pk=content["id"])
50+
assert feature
51+
assert feature.title == data["title"]
52+
assert feature.turn_duration == data["turn_duration"]
53+
54+
55+
@pytest.mark.asyncio
56+
async def test_features_http_get_by_id(app):
57+
path = "/api/features/{id}"
58+
async with TestClient(app) as client:
59+
random_feature = await create_random_feature()
60+
await create_random_guest(random_feature)
61+
await create_random_guest(random_feature)
62+
await create_random_guest(random_feature)
63+
response = await client.get(path.format(id=random_feature.id))
64+
assert response.status_code == 200
65+
content = response.json()
66+
assert content["title"] == random_feature.title
67+
assert content["turn_duration"] == random_feature.turn_duration
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_features_http_get_by_slug(app):
72+
path = "/api/features/{slug}"
3373
async with TestClient(app) as client:
3474
random_feature = await create_random_feature()
3575
await create_random_guest(random_feature)
3676
await create_random_guest(random_feature)
3777
await create_random_guest(random_feature)
38-
response = await client.get(path.format(feature_id=random_feature.id))
78+
response = await client.get(path.format(slug=random_feature.slug))
3979
assert response.status_code == 200
4080
content = response.json()
4181
assert content["title"] == random_feature.title

backend/tests/schemas/test_feature_schemas.py

+1-23
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,8 @@
11
import pytest
2-
from app.models.features import Feature
3-
from app.schemas.features import FeatureCreate, FeatureOut
2+
from app.schemas.features import FeatureOut
43
from async_asgi_testclient import TestClient
54
from tests._utils.features import create_random_feature
65
from tests._utils.guests import create_random_guest
7-
from tests._utils.strings import create_random_string
8-
9-
10-
@pytest.mark.asyncio
11-
async def test_schemas_feature_create_autogenerates_slug(app):
12-
async with TestClient(app):
13-
title = create_random_string(
14-
min_length=5,
15-
max_length=20,
16-
min_words=2,
17-
max_words=6,
18-
uppercase_letters=True,
19-
lowercase_letters=True,
20-
numbers=True,
21-
)
22-
feature_in = FeatureCreate(title=title)
23-
feature = await Feature.create(**feature_in.dict())
24-
assert feature.title == title
25-
assert len(feature.slug)
26-
assert len(feature.slug.split()) == 1
27-
assert len(feature.slug.split()) < len(title.split())
286

297

308
@pytest.mark.asyncio

0 commit comments

Comments
 (0)