feat: add where are more items problem
This commit is contained in:
@@ -55,8 +55,8 @@ For a new grade, follow the same pattern:
|
|||||||
|
|
||||||
## Naming Conventions
|
## Naming Conventions
|
||||||
|
|
||||||
- Endpoint paths use the existing style: `/math/1_grade/<problem_name>`.
|
- Endpoint paths use the existing style: `/math/grade_1/<problem_name>`.
|
||||||
- Python package names should use valid identifiers like `grade_1`, not `1_grade`.
|
- Python package names should use valid identifiers like `grade_1`.
|
||||||
- Problem files should use snake case, for example `join_pictures_with_quantity.py`.
|
- Problem files should use snake case, for example `join_pictures_with_quantity.py`.
|
||||||
- Request schema names should end in `Request`.
|
- Request schema names should end in `Request`.
|
||||||
- Response schema names should describe the generated problem, for example `JoinPicturesWithQuantityProblem`.
|
- Response schema names should describe the generated problem, for example `JoinPicturesWithQuantityProblem`.
|
||||||
|
|||||||
51
ENDPOINTS-EXAMPLE.md
Normal file
51
ENDPOINTS-EXAMPLE.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Endpoints examples
|
||||||
|
|
||||||
|
Test quick examples for each endpoint:
|
||||||
|
|
||||||
|
## Grade 1
|
||||||
|
|
||||||
|
### Join pictures with quantity
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://127.0.0.1:8000/math/grade_1/join_pictures_with_quantity" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"available_pictures": [
|
||||||
|
{"id": "picture-1", "name": "Apple", "image_path": "/images/apple.png"},
|
||||||
|
{"id": "picture-2", "name": "Banana", "image_path": "/images/banana.png"},
|
||||||
|
{"id": "picture-3", "name": "Car", "image_path": "/images/car.png"},
|
||||||
|
{"id": "picture-4", "name": "Dog", "image_path": "/images/dog.png"},
|
||||||
|
{"id": "picture-5", "name": "Elephant", "image_path": "/images/elephant.png"},
|
||||||
|
{"id": "picture-6", "name": "Fish", "image_path": "/images/fish.png"},
|
||||||
|
{"id": "picture-7", "name": "Grape", "image_path": "/images/grape.png"},
|
||||||
|
{"id": "picture-8", "name": "Hat", "image_path": "/images/hat.png"},
|
||||||
|
{"id": "picture-9", "name": "Ice Cream", "image_path": "/images/ice-cream.png"},
|
||||||
|
{"id": "picture-10", "name": "Juice", "image_path": "/images/juice.png"}
|
||||||
|
],
|
||||||
|
"container_count_per_side": 5,
|
||||||
|
"min_quantity": 1,
|
||||||
|
"max_quantity": 10,
|
||||||
|
"seed": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Where are more items
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://127.0.0.1:8000/math/grade_1/where_are_more_items" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"available_pictures": [
|
||||||
|
{"id": "picture-1", "name": "Panda", "image_path": "/images/panda.png"},
|
||||||
|
{"id": "picture-2", "name": "Koala", "image_path": "/images/koala.png"},
|
||||||
|
{"id": "picture-3", "name": "Orange", "image_path": "/images/orange.png"},
|
||||||
|
{"id": "picture-4", "name": "Apple", "image_path": "/images/apple.png"},
|
||||||
|
{"id": "picture-5", "name": "Donut", "image_path": "/images/donut.png"},
|
||||||
|
{"id": "picture-6", "name": "Candy", "image_path": "/images/candy.png"}
|
||||||
|
],
|
||||||
|
"comparison_count": 3,
|
||||||
|
"min_quantity": 1,
|
||||||
|
"max_quantity": 10,
|
||||||
|
"seed": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 333 KiB |
55
app/problems/grade_1/where_are_more_items.py
Normal file
55
app/problems/grade_1/where_are_more_items.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
from app.schemas.grade_1.where_are_more_items import (
|
||||||
|
ItemGroup,
|
||||||
|
MoreItemsComparison,
|
||||||
|
PictureAsset,
|
||||||
|
WhereAreMoreItemsProblem,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def where_are_more_items(
|
||||||
|
available_pictures: list[dict],
|
||||||
|
comparison_count: int = 3,
|
||||||
|
min_quantity: int = 1,
|
||||||
|
max_quantity: int = 10,
|
||||||
|
seed: int | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Generate comparison cards where students choose the group with more items."""
|
||||||
|
pictures = [PictureAsset.model_validate(picture) for picture in available_pictures]
|
||||||
|
|
||||||
|
if len(pictures) < 2:
|
||||||
|
raise ValueError("available_pictures must contain at least 2 pictures")
|
||||||
|
|
||||||
|
if min_quantity >= max_quantity:
|
||||||
|
raise ValueError("max_quantity must be greater than min_quantity")
|
||||||
|
|
||||||
|
rng = random.Random(seed)
|
||||||
|
quantity_values = list(range(min_quantity, max_quantity + 1))
|
||||||
|
comparisons: list[MoreItemsComparison] = []
|
||||||
|
|
||||||
|
for index in range(comparison_count):
|
||||||
|
left_picture, right_picture = rng.sample(pictures, 2)
|
||||||
|
left_quantity, right_quantity = rng.sample(quantity_values, 2)
|
||||||
|
answer_side = "left" if left_quantity > right_quantity else "right"
|
||||||
|
|
||||||
|
comparisons.append(
|
||||||
|
MoreItemsComparison(
|
||||||
|
position=index + 1,
|
||||||
|
left_group=ItemGroup(
|
||||||
|
side="left",
|
||||||
|
picture=left_picture,
|
||||||
|
quantity=left_quantity,
|
||||||
|
),
|
||||||
|
right_group=ItemGroup(
|
||||||
|
side="right",
|
||||||
|
picture=right_picture,
|
||||||
|
quantity=right_quantity,
|
||||||
|
),
|
||||||
|
answer_side=answer_side,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
problem = WhereAreMoreItemsProblem(comparisons=comparisons)
|
||||||
|
|
||||||
|
return problem.model_dump()
|
||||||
@@ -3,10 +3,15 @@ from fastapi import APIRouter, HTTPException
|
|||||||
from app.problems.grade_1.join_pictures_with_quantity import (
|
from app.problems.grade_1.join_pictures_with_quantity import (
|
||||||
join_pictures_with_quantity,
|
join_pictures_with_quantity,
|
||||||
)
|
)
|
||||||
|
from app.problems.grade_1.where_are_more_items import where_are_more_items
|
||||||
from app.schemas.grade_1.join_pictures_with_quantity import (
|
from app.schemas.grade_1.join_pictures_with_quantity import (
|
||||||
JoinPicturesWithQuantityProblem,
|
JoinPicturesWithQuantityProblem,
|
||||||
JoinPicturesWithQuantityRequest,
|
JoinPicturesWithQuantityRequest,
|
||||||
)
|
)
|
||||||
|
from app.schemas.grade_1.where_are_more_items import (
|
||||||
|
WhereAreMoreItemsProblem,
|
||||||
|
WhereAreMoreItemsRequest,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/grade_1", tags=["Grade 1"])
|
router = APIRouter(prefix="/grade_1", tags=["Grade 1"])
|
||||||
|
|
||||||
@@ -30,3 +35,24 @@ def create_join_pictures_with_quantity_problem(
|
|||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/where_are_more_items",
|
||||||
|
response_model=WhereAreMoreItemsProblem,
|
||||||
|
)
|
||||||
|
def create_where_are_more_items_problem(
|
||||||
|
request: WhereAreMoreItemsRequest,
|
||||||
|
) -> dict:
|
||||||
|
try:
|
||||||
|
return where_are_more_items(
|
||||||
|
available_pictures=[
|
||||||
|
picture.model_dump() for picture in request.available_pictures
|
||||||
|
],
|
||||||
|
comparison_count=request.comparison_count,
|
||||||
|
min_quantity=request.min_quantity,
|
||||||
|
max_quantity=request.max_quantity,
|
||||||
|
seed=request.seed,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|||||||
36
app/schemas/grade_1/where_are_more_items.py
Normal file
36
app/schemas/grade_1/where_are_more_items.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
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 ItemGroup(BaseModel):
|
||||||
|
side: Literal["left", "right"]
|
||||||
|
picture: PictureAsset
|
||||||
|
quantity: PositiveInt
|
||||||
|
|
||||||
|
|
||||||
|
class MoreItemsComparison(BaseModel):
|
||||||
|
position: PositiveInt
|
||||||
|
left_group: ItemGroup
|
||||||
|
right_group: ItemGroup
|
||||||
|
answer_side: Literal["left", "right"]
|
||||||
|
has_answer_box: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class WhereAreMoreItemsProblem(BaseModel):
|
||||||
|
instructions: str = "Marca dónde hay más."
|
||||||
|
comparisons: list[MoreItemsComparison]
|
||||||
|
|
||||||
|
|
||||||
|
class WhereAreMoreItemsRequest(BaseModel):
|
||||||
|
available_pictures: list[PictureAsset] = Field(min_length=1)
|
||||||
|
comparison_count: PositiveInt = 3
|
||||||
|
min_quantity: PositiveInt = 1
|
||||||
|
max_quantity: PositiveInt = 10
|
||||||
|
seed: int | None = None
|
||||||
@@ -11,7 +11,7 @@ class JoinPicturesWithQuantityEndpointTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_creates_problem(self) -> None:
|
def test_creates_problem(self) -> None:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/math/1_grade/join_pictures_with_quantity",
|
"/math/grade_1/join_pictures_with_quantity",
|
||||||
json={
|
json={
|
||||||
"available_pictures": [
|
"available_pictures": [
|
||||||
{
|
{
|
||||||
@@ -37,7 +37,7 @@ class JoinPicturesWithQuantityEndpointTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_returns_bad_request_for_too_few_pictures(self) -> None:
|
def test_returns_bad_request_for_too_few_pictures(self) -> None:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/math/1_grade/join_pictures_with_quantity",
|
"/math/grade_1/join_pictures_with_quantity",
|
||||||
json={
|
json={
|
||||||
"available_pictures": [
|
"available_pictures": [
|
||||||
{
|
{
|
||||||
|
|||||||
72
tests/test_where_are_more_items_endpoint.py
Normal file
72
tests/test_where_are_more_items_endpoint.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import create_app
|
||||||
|
|
||||||
|
|
||||||
|
class WhereAreMoreItemsEndpointTest(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.client = TestClient(create_app())
|
||||||
|
|
||||||
|
def test_creates_problem(self) -> None:
|
||||||
|
response = self.client.post(
|
||||||
|
"/math/grade_1/where_are_more_items",
|
||||||
|
json={
|
||||||
|
"available_pictures": [
|
||||||
|
{
|
||||||
|
"id": f"picture-{index}",
|
||||||
|
"name": f"Picture {index}",
|
||||||
|
"image_path": f"/images/{index}.png",
|
||||||
|
}
|
||||||
|
for index in range(6)
|
||||||
|
],
|
||||||
|
"seed": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
problem = response.json()
|
||||||
|
self.assertEqual(problem["instructions"], "Marca dónde hay más.")
|
||||||
|
self.assertEqual(len(problem["comparisons"]), 3)
|
||||||
|
|
||||||
|
for comparison in problem["comparisons"]:
|
||||||
|
left_group = comparison["left_group"]
|
||||||
|
right_group = comparison["right_group"]
|
||||||
|
|
||||||
|
self.assertEqual(left_group["side"], "left")
|
||||||
|
self.assertEqual(right_group["side"], "right")
|
||||||
|
self.assertNotEqual(left_group["quantity"], right_group["quantity"])
|
||||||
|
self.assertIn("picture", left_group)
|
||||||
|
self.assertIn("picture", right_group)
|
||||||
|
|
||||||
|
expected_answer_side = (
|
||||||
|
"left"
|
||||||
|
if left_group["quantity"] > right_group["quantity"]
|
||||||
|
else "right"
|
||||||
|
)
|
||||||
|
self.assertEqual(comparison["answer_side"], expected_answer_side)
|
||||||
|
|
||||||
|
def test_returns_bad_request_for_too_few_figures(self) -> None:
|
||||||
|
response = self.client.post(
|
||||||
|
"/math/grade_1/where_are_more_items",
|
||||||
|
json={
|
||||||
|
"available_pictures": [
|
||||||
|
{
|
||||||
|
"id": "picture-1",
|
||||||
|
"name": "Picture 1",
|
||||||
|
"image_path": "/images/1.png",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(
|
||||||
|
response.json(),
|
||||||
|
{"detail": "available_pictures must contain at least 2 pictures"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user