- Recipe model with ingredients (amount/range, unit, group, note), steps, lowercase tags, author notes, source URL and private/public visibility - Central RecipeQuerySet.visible_to() used by every read path; edit only by owner, delete by owner or admin (visible recipes only) - Text-based editing: ingredient/step parser with roundtrip and scaling - Photo upload (JPEG/PNG/WebP/HEIC), re-encoded to WebP without EXIF, thumbnails, cover image, served only through permission-checked view - Search across title, description, ingredients, tags; ingredient and tag filters; "mine" and favourites views - Admin can delete all recipes of a deactivated user - Acceptance tests: private recipes unreachable for others and admins in list, search, tags, favourites, detail, edit, delete and image URLs Co-Authored-By: Claude Opus 5.5 <[email protected]>
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from recipes.parsing import (
|
|
format_amount,
|
|
ingredients_to_text,
|
|
parse_ingredient_line,
|
|
parse_ingredients,
|
|
parse_steps,
|
|
parse_tags,
|
|
scale_amount,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"line, amount, amount_max, unit, name, note",
|
|
[
|
|
("200 g Mehl, gesiebt", "200", None, "g", "Mehl", "gesiebt"),
|
|
("200g Mehl", "200", None, "g", "Mehl", ""),
|
|
("1,5l Milch", "1.5", None, "l", "Milch", ""),
|
|
("1 ½ TL Salz", "1.5", None, "TL", "Salz", ""),
|
|
("1/2 Bund Petersilie", "0.5", None, "Bund", "Petersilie", ""),
|
|
("2 1/2 Tassen Reis", "2.5", None, "Tassen", "Reis", ""),
|
|
("2-3 Zehen Knoblauch", "2", "3", "Zehen", "Knoblauch", ""),
|
|
("2 bis 3 EL Öl", "2", "3", "EL", "Öl", ""),
|
|
("2 Eier", "2", None, "", "Eier", ""),
|
|
("3 Essl. Zucker", "3", None, "EL", "Zucker", ""),
|
|
("- 1 Prise Salz", "1", None, "Prise", "Salz", ""),
|
|
("½ Zitrone, nur Saft", "0.5", None, "", "Zitrone", "nur Saft"),
|
|
("Salz und Pfeffer", None, None, "", "Salz und Pfeffer", ""),
|
|
("1 Dose Tomaten (400 g)", "1", None, "Dose", "Tomaten (400 g)", ""),
|
|
],
|
|
)
|
|
def test_parse_ingredient_line(line, amount, amount_max, unit, name, note):
|
|
item = parse_ingredient_line(line)
|
|
assert item.amount == (Decimal(amount) if amount else None)
|
|
assert item.amount_max == (Decimal(amount_max) if amount_max else None)
|
|
assert (item.unit, item.name, item.note) == (unit, name, note)
|
|
|
|
|
|
def test_parse_ingredients_with_groups():
|
|
items = parse_ingredients("Für den Teig:\n200 g Mehl\n\nFür die Soße:\n1 Dose Tomaten\n")
|
|
assert [(i.group, i.name) for i in items] == [("Für den Teig", "Mehl"), ("Für die Soße", "Tomaten")]
|
|
|
|
|
|
def test_ingredient_text_roundtrip():
|
|
text = "200 g Mehl, gesiebt\n2-3 Zehen Knoblauch\n\nFür die Soße:\n½ TL Salz\nPfeffer"
|
|
items = parse_ingredients(text)
|
|
assert parse_ingredients(ingredients_to_text(items)) == items
|
|
|
|
|
|
def test_parse_steps_by_blank_lines():
|
|
assert parse_steps("Ofen vorheizen.\nAuf 180 °C.\n\n2. Mischen.") == ["Ofen vorheizen. Auf 180 °C.", "Mischen."]
|
|
|
|
|
|
def test_parse_steps_by_lines_and_strip_numbers():
|
|
assert parse_steps("1. Schneiden\n2) Braten\nSchritt 3: Servieren") == ["Schneiden", "Braten", "Servieren"]
|
|
|
|
|
|
def test_parse_tags_lowercase_unique():
|
|
assert parse_tags("Pasta, #Vegetarisch, Schnell Gemacht ,pasta,") == ["pasta", "vegetarisch", "schnell gemacht"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value, expected",
|
|
[("2", "2"), ("0.5", "½"), ("1.5", "1 ½"), ("0.333", "⅓"), ("0.25", "¼"), ("2.37", "2,37"), ("1.999", "2")],
|
|
)
|
|
def test_format_amount(value, expected):
|
|
assert format_amount(Decimal(value)) == expected
|
|
|
|
|
|
def test_scale_amount_normalises_metric():
|
|
assert scale_amount(Decimal(600), None, "g", Decimal(2)) == (Decimal("1.2"), None, "kg")
|
|
assert scale_amount(Decimal(250), None, "ml", Decimal("0.5")) == (Decimal(125), None, "ml")
|
|
assert scale_amount(Decimal(2), Decimal(3), "EL", Decimal(2)) == (Decimal(4), Decimal(6), "EL")
|
|
assert scale_amount(None, None, "", Decimal(2)) == (None, None, "")
|