feat: add compose and decompose numbers problem
This commit is contained in:
@@ -53,6 +53,8 @@ For a new grade, follow the same pattern:
|
||||
3. Include the new grade router from `app/routers/math.py`.
|
||||
4. Add tests.
|
||||
|
||||
Every time you add a new problem, also add an endpoint curl example on the ENDPOINTS-EXAMPLE.md file.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Endpoint paths use the existing style: `/math/grade_1/<problem_name>`.
|
||||
|
||||
@@ -49,3 +49,20 @@ curl -X POST "http://127.0.0.1:8000/math/grade_1/where_are_more_items" \
|
||||
"seed": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### Compose and decompose numbers
|
||||
|
||||
```
|
||||
curl -X POST "http://127.0.0.1:8000/math/grade_1/compose_and_decompose_numbers" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"picture": {
|
||||
"id": "cube",
|
||||
"name": "Cube",
|
||||
"image_path": "/images/cube.png"
|
||||
},
|
||||
"whole": 10,
|
||||
"randomize_rows": false,
|
||||
"seed": 1
|
||||
}'
|
||||
```
|
||||
|
||||
42
app/problems/grade_1/compose_and_decompose_numbers.py
Normal file
42
app/problems/grade_1/compose_and_decompose_numbers.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import random
|
||||
|
||||
from app.schemas.grade_1.compose_and_decompose_numbers import (
|
||||
ComposeAndDecomposeNumbersProblem,
|
||||
DecompositionRow,
|
||||
PictureAsset,
|
||||
)
|
||||
|
||||
|
||||
def compose_and_decompose_numbers(
|
||||
picture: dict,
|
||||
whole: int = 10,
|
||||
randomize_rows: bool = False,
|
||||
seed: int | None = None,
|
||||
) -> dict:
|
||||
"""Generate number decomposition rows for one picture asset."""
|
||||
if whole < 2:
|
||||
raise ValueError("whole must be at least 2")
|
||||
|
||||
selected_picture = PictureAsset.model_validate(picture)
|
||||
pairs = [(first_part, whole - first_part) for first_part in range(whole - 1, 0, -1)]
|
||||
|
||||
if randomize_rows:
|
||||
rng = random.Random(seed)
|
||||
rng.shuffle(pairs)
|
||||
|
||||
problem = ComposeAndDecomposeNumbersProblem(
|
||||
whole=whole,
|
||||
picture=selected_picture,
|
||||
rows=[
|
||||
DecompositionRow(
|
||||
position=index + 1,
|
||||
whole=whole,
|
||||
first_part=first_part,
|
||||
second_part=second_part,
|
||||
picture=selected_picture,
|
||||
)
|
||||
for index, (first_part, second_part) in enumerate(pairs)
|
||||
],
|
||||
)
|
||||
|
||||
return problem.model_dump()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@@ -1,9 +1,16 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from app.problems.grade_1.compose_and_decompose_numbers import (
|
||||
compose_and_decompose_numbers,
|
||||
)
|
||||
from app.problems.grade_1.join_pictures_with_quantity import (
|
||||
join_pictures_with_quantity,
|
||||
)
|
||||
from app.problems.grade_1.where_are_more_items import where_are_more_items
|
||||
from app.schemas.grade_1.compose_and_decompose_numbers import (
|
||||
ComposeAndDecomposeNumbersProblem,
|
||||
ComposeAndDecomposeNumbersRequest,
|
||||
)
|
||||
from app.schemas.grade_1.join_pictures_with_quantity import (
|
||||
JoinPicturesWithQuantityProblem,
|
||||
JoinPicturesWithQuantityRequest,
|
||||
@@ -16,6 +23,24 @@ from app.schemas.grade_1.where_are_more_items import (
|
||||
router = APIRouter(prefix="/grade_1", tags=["Grade 1"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/compose_and_decompose_numbers",
|
||||
response_model=ComposeAndDecomposeNumbersProblem,
|
||||
)
|
||||
def create_compose_and_decompose_numbers_problem(
|
||||
request: ComposeAndDecomposeNumbersRequest,
|
||||
) -> dict:
|
||||
try:
|
||||
return compose_and_decompose_numbers(
|
||||
picture=request.picture.model_dump(),
|
||||
whole=request.whole,
|
||||
randomize_rows=request.randomize_rows,
|
||||
seed=request.seed,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/join_pictures_with_quantity",
|
||||
response_model=JoinPicturesWithQuantityProblem,
|
||||
|
||||
30
app/schemas/grade_1/compose_and_decompose_numbers.py
Normal file
30
app/schemas/grade_1/compose_and_decompose_numbers.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from pydantic import BaseModel, Field, PositiveInt
|
||||
|
||||
|
||||
class PictureAsset(BaseModel):
|
||||
id: str = Field(min_length=1)
|
||||
name: str = Field(min_length=1)
|
||||
image_path: str = Field(min_length=1)
|
||||
|
||||
|
||||
class DecompositionRow(BaseModel):
|
||||
position: PositiveInt
|
||||
whole: PositiveInt
|
||||
first_part: PositiveInt
|
||||
second_part: PositiveInt
|
||||
picture: PictureAsset
|
||||
has_answer_boxes: bool = True
|
||||
|
||||
|
||||
class ComposeAndDecomposeNumbersProblem(BaseModel):
|
||||
instructions: str = "Compón y descompón el número."
|
||||
whole: PositiveInt
|
||||
picture: PictureAsset
|
||||
rows: list[DecompositionRow]
|
||||
|
||||
|
||||
class ComposeAndDecomposeNumbersRequest(BaseModel):
|
||||
picture: PictureAsset
|
||||
whole: PositiveInt = 10
|
||||
randomize_rows: bool = False
|
||||
seed: int | None = None
|
||||
66
tests/test_compose_and_decompose_numbers_endpoint.py
Normal file
66
tests/test_compose_and_decompose_numbers_endpoint.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import unittest
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
class ComposeAndDecomposeNumbersEndpointTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.client = TestClient(create_app())
|
||||
self.picture = {
|
||||
"id": "cube",
|
||||
"name": "Cube",
|
||||
"image_path": "/images/cube.png",
|
||||
}
|
||||
|
||||
def test_creates_ordered_problem(self) -> None:
|
||||
response = self.client.post(
|
||||
"/math/grade_1/compose_and_decompose_numbers",
|
||||
json={"picture": self.picture},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
problem = response.json()
|
||||
rows = problem["rows"]
|
||||
|
||||
self.assertEqual(problem["instructions"], "Compón y descompón el número.")
|
||||
self.assertEqual(problem["whole"], 10)
|
||||
self.assertEqual(problem["picture"], self.picture)
|
||||
self.assertEqual(len(rows), 9)
|
||||
self.assertEqual(
|
||||
[(row["first_part"], row["second_part"]) for row in rows],
|
||||
[(9, 1), (8, 2), (7, 3), (6, 4), (5, 5), (4, 6), (3, 7), (2, 8), (1, 9)],
|
||||
)
|
||||
|
||||
for row in rows:
|
||||
self.assertEqual(row["whole"], 10)
|
||||
self.assertEqual(row["first_part"] + row["second_part"], 10)
|
||||
self.assertEqual(row["picture"], self.picture)
|
||||
|
||||
def test_can_randomize_rows(self) -> None:
|
||||
response = self.client.post(
|
||||
"/math/grade_1/compose_and_decompose_numbers",
|
||||
json={"picture": self.picture, "randomize_rows": True, "seed": 1},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
rows = response.json()["rows"]
|
||||
ordered_pairs = [(9, 1), (8, 2), (7, 3), (6, 4), (5, 5), (4, 6), (3, 7), (2, 8), (1, 9)]
|
||||
randomized_pairs = [(row["first_part"], row["second_part"]) for row in rows]
|
||||
|
||||
self.assertNotEqual(randomized_pairs, ordered_pairs)
|
||||
self.assertCountEqual(randomized_pairs, ordered_pairs)
|
||||
|
||||
def test_returns_bad_request_for_whole_less_than_two(self) -> None:
|
||||
response = self.client.post(
|
||||
"/math/grade_1/compose_and_decompose_numbers",
|
||||
json={"picture": self.picture, "whole": 1},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json(), {"detail": "whole must be at least 2"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user