Add Recipes
This commit is contained in:
@@ -17,7 +17,7 @@ APP_TOKEN_VALIDITY = 7200
|
|||||||
|
|
||||||
# Development Database Settings (overridden by PyTest app_config Fixture)
|
# Development Database Settings (overridden by PyTest app_config Fixture)
|
||||||
DB_DRIVER = 'sqlite'
|
DB_DRIVER = 'sqlite'
|
||||||
DB_FILE = 'sigl-test.db'
|
DB_FILE = 'sigl-test-next.db'
|
||||||
|
|
||||||
# Mail Configuration
|
# Mail Configuration
|
||||||
MAIL_ENABLED = True
|
MAIL_ENABLED = True
|
||||||
|
|||||||
47
migrations/versions/3d0cab7d7747_.py
Normal file
47
migrations/versions/3d0cab7d7747_.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 3d0cab7d7747
|
||||||
|
Revises: c28b3a6cdc3a
|
||||||
|
Create Date: 2023-02-25 15:37:19.626908
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3d0cab7d7747'
|
||||||
|
down_revision = 'c28b3a6cdc3a'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('recipes',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('notes', sa.String(), nullable=True),
|
||||||
|
sa.Column('createdAt', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('recipe_entries',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('recipe_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('product_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('quantity', sa.String(length=128), nullable=True),
|
||||||
|
sa.Column('notes', sa.String(), nullable=True),
|
||||||
|
sa.Column('createdAt', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['recipe_id'], ['recipes.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('recipe_entries')
|
||||||
|
op.drop_table('recipes')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -6,9 +6,17 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
|||||||
|
|
||||||
from sigl.domain.models import Product, ProductLocation
|
from sigl.domain.models import Product, ProductLocation
|
||||||
from sigl.domain.models.list import ListEntry, ShoppingList
|
from sigl.domain.models.list import ListEntry, ShoppingList
|
||||||
|
from sigl.domain.models.recipe import Recipe, RecipeEntry
|
||||||
|
|
||||||
from .globals import db
|
from .globals import db
|
||||||
from .tables import list_entries, lists, product_locations, products
|
from .tables import (
|
||||||
|
list_entries,
|
||||||
|
lists,
|
||||||
|
product_locations,
|
||||||
|
products,
|
||||||
|
recipe_entries,
|
||||||
|
recipes,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = ('init_orm', )
|
__all__ = ('init_orm', )
|
||||||
|
|
||||||
@@ -27,6 +35,18 @@ def init_orm():
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Recipe Entries
|
||||||
|
db.mapper(RecipeEntry, recipe_entries, properties={
|
||||||
|
'product': db.relationship(
|
||||||
|
Product,
|
||||||
|
back_populates='recipes'
|
||||||
|
),
|
||||||
|
'recipe': db.relationship(
|
||||||
|
Recipe,
|
||||||
|
back_populates='entries',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
# Products
|
# Products
|
||||||
db.mapper(Product, products, properties={
|
db.mapper(Product, products, properties={
|
||||||
'entries': db.relationship(
|
'entries': db.relationship(
|
||||||
@@ -34,6 +54,11 @@ def init_orm():
|
|||||||
back_populates='product',
|
back_populates='product',
|
||||||
cascade='all, delete-orphan',
|
cascade='all, delete-orphan',
|
||||||
),
|
),
|
||||||
|
'recipes': db.relationship(
|
||||||
|
RecipeEntry,
|
||||||
|
back_populates='product',
|
||||||
|
cascade='all, delete-orphan',
|
||||||
|
),
|
||||||
'locations': db.relationship(
|
'locations': db.relationship(
|
||||||
ProductLocation,
|
ProductLocation,
|
||||||
back_populates='product',
|
back_populates='product',
|
||||||
@@ -57,3 +82,12 @@ def init_orm():
|
|||||||
cascade='all, delete-orphan',
|
cascade='all, delete-orphan',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Recipes
|
||||||
|
db.mapper(Recipe, recipes, properties={
|
||||||
|
'entries': db.relationship(
|
||||||
|
RecipeEntry,
|
||||||
|
back_populates='recipe',
|
||||||
|
cascade='all, delete-orphan',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -94,3 +94,39 @@ product_locations = db.Table(
|
|||||||
db.Column('createdAt', db.DateTime(), default=None),
|
db.Column('createdAt', db.DateTime(), default=None),
|
||||||
db.Column('modifiedAt', db.DateTime(), default=None),
|
db.Column('modifiedAt', db.DateTime(), default=None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Recipe Table
|
||||||
|
recipes = db.Table(
|
||||||
|
'recipes',
|
||||||
|
|
||||||
|
# Primary Key
|
||||||
|
db.Column('id', db.Integer, primary_key=True),
|
||||||
|
|
||||||
|
# List Attributes
|
||||||
|
db.Column('name', db.String(128), nullable=False),
|
||||||
|
|
||||||
|
# Mixin Columns
|
||||||
|
db.Column('notes', db.String(), default=None),
|
||||||
|
db.Column('createdAt', db.DateTime(), default=None),
|
||||||
|
db.Column('modifiedAt', db.DateTime(), default=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Recipe Entry Table
|
||||||
|
recipe_entries = db.Table(
|
||||||
|
'recipe_entries',
|
||||||
|
|
||||||
|
# Primary Key
|
||||||
|
db.Column('id', db.Integer, primary_key=True),
|
||||||
|
|
||||||
|
# Shopping List and Product Link
|
||||||
|
db.Column('recipe_id', db.ForeignKey('recipes.id'), nullable=False),
|
||||||
|
db.Column('product_id', db.ForeignKey('products.id'), nullable=False),
|
||||||
|
|
||||||
|
# Entry Attributes
|
||||||
|
db.Column('quantity', db.String(128), default=None),
|
||||||
|
|
||||||
|
# Mixin Columns
|
||||||
|
db.Column('notes', db.String(), default=None),
|
||||||
|
db.Column('createdAt', db.DateTime(), default=None),
|
||||||
|
db.Column('modifiedAt', db.DateTime(), default=None),
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
|||||||
|
|
||||||
from .list import ListEntry, ShoppingList
|
from .list import ListEntry, ShoppingList
|
||||||
from .product import Product, ProductLocation
|
from .product import Product, ProductLocation
|
||||||
|
from .recipe import Recipe, RecipeEntry
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ListEntry',
|
'ListEntry',
|
||||||
'Product',
|
'Product',
|
||||||
'ProductLocation',
|
'ProductLocation',
|
||||||
|
'Recipe',
|
||||||
|
'RecipeEntry',
|
||||||
'ShoppingList',
|
'ShoppingList',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from .mixins import NotesMixin, TimestampMixin
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .list import ListEntry
|
from .list import ListEntry
|
||||||
|
from .recipe import RecipeEntry
|
||||||
|
|
||||||
__all__ = ('Product', 'ProductLocation')
|
__all__ = ('Product', 'ProductLocation')
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ class Product(NotesMixin, TimestampMixin):
|
|||||||
|
|
||||||
# Relationship Fields
|
# Relationship Fields
|
||||||
entries: List['ListEntry'] = field(default_factory=list)
|
entries: List['ListEntry'] = field(default_factory=list)
|
||||||
|
recipes: List['RecipeEntry'] = field(default_factory=list)
|
||||||
locations: List['ProductLocation'] = field(default_factory=list)
|
locations: List['ProductLocation'] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
42
sigl/domain/models/recipe.py
Normal file
42
sigl/domain/models/recipe.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Sigl Recipe Domain Model.
|
||||||
|
|
||||||
|
Simple Grocery List (Sigl) | sigl.app
|
||||||
|
Copyright (c) 2023 Asymworks, LLC. All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .mixins import NotesMixin, TimestampMixin
|
||||||
|
from .product import Product
|
||||||
|
|
||||||
|
__all__ = ('Recipe', 'RecipeEntry')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RecipeEntry(NotesMixin, TimestampMixin):
|
||||||
|
"""Information about a Product in a Recipe.
|
||||||
|
|
||||||
|
This class contains information about a Product that is in a recipe
|
||||||
|
list, including the quantity to be purchased and notes about the entry.
|
||||||
|
"""
|
||||||
|
id: int = None
|
||||||
|
quantity: str = None
|
||||||
|
|
||||||
|
# Relationship Fields
|
||||||
|
product: Product = None
|
||||||
|
recipe: 'Recipe' = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Recipe(NotesMixin, TimestampMixin):
|
||||||
|
"""Top-Level Recipe.
|
||||||
|
|
||||||
|
Contains a collection of `RecipeEntry` items which are intended to be
|
||||||
|
added to shopping lists as a group.
|
||||||
|
"""
|
||||||
|
id: int = None
|
||||||
|
name: str = None
|
||||||
|
|
||||||
|
# Relationship Fields
|
||||||
|
entries: List[RecipeEntry] = field(default_factory=list)
|
||||||
@@ -11,7 +11,14 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from sigl.exc import DomainError, NotFoundError
|
from sigl.exc import DomainError, NotFoundError
|
||||||
|
|
||||||
from .models import ListEntry, Product, ProductLocation, ShoppingList
|
from .models import (
|
||||||
|
ListEntry,
|
||||||
|
Product,
|
||||||
|
ProductLocation,
|
||||||
|
Recipe,
|
||||||
|
RecipeEntry,
|
||||||
|
ShoppingList,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_addItem(
|
def list_addItem(
|
||||||
@@ -20,7 +27,7 @@ def list_addItem(
|
|||||||
*,
|
*,
|
||||||
productId: Optional[int] = None,
|
productId: Optional[int] = None,
|
||||||
productName: Optional[str] = None,
|
productName: Optional[str] = None,
|
||||||
productCategory: Optional[str] = None,
|
productCategory: Optional[str] = '',
|
||||||
quantity: Optional[str] = None,
|
quantity: Optional[str] = None,
|
||||||
remember: Optional[bool] = None,
|
remember: Optional[bool] = None,
|
||||||
notes: Optional[str] = None,
|
notes: Optional[str] = None,
|
||||||
@@ -71,6 +78,51 @@ def list_addItem(
|
|||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def list_addRecipe(session: Session, listId: int, recipeId: int) -> List[ListEntry]:
|
||||||
|
"""Add a Recipe to a Shopping List.
|
||||||
|
|
||||||
|
This creates new `ListEntry` items for each `RecipeEntry` within the
|
||||||
|
`Recipe` object. Note that any Products that are referenced by the Recipe
|
||||||
|
and that are already in the Shopping List will have the quantity updated
|
||||||
|
to include the Recipe quantity.
|
||||||
|
"""
|
||||||
|
sList = list_by_id(session, listId)
|
||||||
|
if not sList:
|
||||||
|
raise NotFoundError(f'List {listId} does not exist')
|
||||||
|
|
||||||
|
recipe = recipe_by_id(session, recipeId)
|
||||||
|
if not recipe:
|
||||||
|
raise NotFoundError(f'Recipe {recipeId} does not exist')
|
||||||
|
|
||||||
|
lEntries = list()
|
||||||
|
for rEntry in recipe.entries:
|
||||||
|
lEntry = list_entry_by_productId(session, listId, rEntry.product.id)
|
||||||
|
if lEntry:
|
||||||
|
if lEntry.quantity and rEntry.quantity:
|
||||||
|
lEntry.quantity = f'{lEntry.quantity}, {rEntry.quantity} ({recipe.name})'
|
||||||
|
elif rEntry.quantity:
|
||||||
|
lEntry.quantity = rEntry.quantity
|
||||||
|
|
||||||
|
if lEntry.notes and rEntry.notes:
|
||||||
|
lEntry.notes = f'{lEntry.notes}\n{rEntry.notes}'
|
||||||
|
elif rEntry.notes:
|
||||||
|
lEntry.notes = rEntry.notes
|
||||||
|
|
||||||
|
else:
|
||||||
|
lEntry = ListEntry(
|
||||||
|
shoppingList=sList,
|
||||||
|
product=rEntry.product,
|
||||||
|
quantity=rEntry.quantity,
|
||||||
|
notes=rEntry.notes,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(lEntry)
|
||||||
|
lEntries.append(lEntry)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
return lEntries
|
||||||
|
|
||||||
|
|
||||||
def lists_all(session: Session) -> List[ShoppingList]:
|
def lists_all(session: Session) -> List[ShoppingList]:
|
||||||
"""Return all Shopping Lists."""
|
"""Return all Shopping Lists."""
|
||||||
return session.query(ShoppingList).all()
|
return session.query(ShoppingList).all()
|
||||||
@@ -195,6 +247,19 @@ def list_update(
|
|||||||
return sList
|
return sList
|
||||||
|
|
||||||
|
|
||||||
|
def list_entry_by_productId(session: Session, listId: int, productId: int) -> Optional[ListEntry]:
|
||||||
|
"""Load a Shopping List Entry by Product Id."""
|
||||||
|
sList = list_by_id(session, listId)
|
||||||
|
if not sList:
|
||||||
|
raise NotFoundError(f'List {listId} not found')
|
||||||
|
|
||||||
|
product = product_by_id(session, productId)
|
||||||
|
if not product:
|
||||||
|
raise NotFoundError(f'Product {productId} not found')
|
||||||
|
|
||||||
|
return session.query(ListEntry).filter(ListEntry.product == product).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
def list_entry_by_id(session: Session, listId: int, entryId: int) -> Optional[ListEntry]:
|
def list_entry_by_id(session: Session, listId: int, entryId: int) -> Optional[ListEntry]:
|
||||||
"""Load a specific Shopping List Entry."""
|
"""Load a specific Shopping List Entry."""
|
||||||
sList = list_by_id(session, listId)
|
sList = list_by_id(session, listId)
|
||||||
@@ -333,3 +398,159 @@ def product_removeLocation(
|
|||||||
session.delete(loc)
|
session.delete(loc)
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_addItem(
|
||||||
|
session: Session,
|
||||||
|
id: int,
|
||||||
|
*,
|
||||||
|
productId: Optional[int] = None,
|
||||||
|
productName: Optional[str] = None,
|
||||||
|
productCategory: Optional[str] = '',
|
||||||
|
quantity: Optional[str] = None,
|
||||||
|
remember: Optional[bool] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
|
) -> ListEntry:
|
||||||
|
"""Add a Product to a Recipe.
|
||||||
|
|
||||||
|
If the `productId` parameter is provided, the method will look up the
|
||||||
|
product by ID and add it to the list. If the `product` parameter is not
|
||||||
|
provided, a new `Product` will be created with the provided `productName`
|
||||||
|
and `productCategory` values.
|
||||||
|
|
||||||
|
If the `remember` parameter is provided and is `False`, the product will
|
||||||
|
be removed along with the list item, and it will not be offered as a
|
||||||
|
suggestion when adding items.
|
||||||
|
"""
|
||||||
|
recipe = recipe_by_id(session, id)
|
||||||
|
if not recipe:
|
||||||
|
raise NotFoundError(f'Recipe {id} does not exist')
|
||||||
|
|
||||||
|
product = None
|
||||||
|
if not productId:
|
||||||
|
if not productName:
|
||||||
|
raise DomainError('Product Name cannot be empty')
|
||||||
|
|
||||||
|
product = product_by_name(session, productName)
|
||||||
|
if not product:
|
||||||
|
product = Product(name=productName, category=productCategory)
|
||||||
|
if remember is not None:
|
||||||
|
product.remember = remember
|
||||||
|
|
||||||
|
session.add(product)
|
||||||
|
|
||||||
|
else:
|
||||||
|
product = product_by_id(session, productId)
|
||||||
|
if not product:
|
||||||
|
raise NotFoundError(f'Product {productId} does not exist')
|
||||||
|
|
||||||
|
entry = RecipeEntry(
|
||||||
|
recipe=recipe,
|
||||||
|
product=product,
|
||||||
|
quantity=quantity,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(entry)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def recipes_all(session: Session) -> List[Recipe]:
|
||||||
|
"""Return all Recipes."""
|
||||||
|
return session.query(Recipe).all()
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_by_id(session: Session, id: int) -> Optional[Recipe]:
|
||||||
|
"""Load a specific Recipe."""
|
||||||
|
return session.query(Recipe).filter(Recipe.id == id).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_create(session: Session, name: str, *, notes=None) -> Recipe:
|
||||||
|
"""Create a new Recipe."""
|
||||||
|
recipe = Recipe(name=name, notes=notes)
|
||||||
|
session.add(recipe)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_delete(session: Session, id: int):
|
||||||
|
"""Delete a Recipe."""
|
||||||
|
recipe = recipe_by_id(session, id)
|
||||||
|
if not recipe:
|
||||||
|
raise NotFoundError(f'Recipe {id} does not exist')
|
||||||
|
|
||||||
|
session.delete(recipe)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_deleteItem(session: Session, recipeId: int, entryId: int):
|
||||||
|
"""Delete an Entry from a Recipe."""
|
||||||
|
entry = recipe_entry_by_id(session, recipeId, entryId)
|
||||||
|
if not entry.product.remember:
|
||||||
|
session.delete(entry.product)
|
||||||
|
|
||||||
|
session.delete(entry)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_editItem(
|
||||||
|
session: Session,
|
||||||
|
recipeId: int,
|
||||||
|
entryId: int,
|
||||||
|
*,
|
||||||
|
quantity: Optional[str] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
|
) -> ListEntry:
|
||||||
|
"""Edit an Entry in a Recipe."""
|
||||||
|
entry = recipe_entry_by_id(session, recipeId, entryId)
|
||||||
|
entry.quantity = quantity
|
||||||
|
entry.notes = notes
|
||||||
|
entry.set_modified_at()
|
||||||
|
|
||||||
|
session.add(entry)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_update(
|
||||||
|
session: Session,
|
||||||
|
id: int,
|
||||||
|
name: Union[str, None] = None,
|
||||||
|
notes: Union[str, None] = None,
|
||||||
|
) -> ShoppingList:
|
||||||
|
"""Update the Name and/or Notes of a Recipe."""
|
||||||
|
recipe = recipe_by_id(session, id)
|
||||||
|
if not recipe:
|
||||||
|
raise NotFoundError(f'Recipe {id} does not exist')
|
||||||
|
|
||||||
|
recipe.name = name
|
||||||
|
recipe.notes = notes
|
||||||
|
recipe.set_modified_at()
|
||||||
|
|
||||||
|
session.add(recipe)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_entry_by_id(session: Session, recipeId: int, entryId: int) -> Optional[RecipeEntry]:
|
||||||
|
"""Load a specific Recipe Entry."""
|
||||||
|
recipe = recipe_by_id(session, recipeId)
|
||||||
|
if not recipe:
|
||||||
|
raise NotFoundError(f'Recipe {recipeId} not found')
|
||||||
|
|
||||||
|
entry = session.query(RecipeEntry).filter(RecipeEntry.id == entryId).one_or_none()
|
||||||
|
if not entry:
|
||||||
|
raise NotFoundError(f'Recipe Entry {entryId} not found')
|
||||||
|
|
||||||
|
if entry.recipe != recipe:
|
||||||
|
raise DomainError(
|
||||||
|
f'List Entry {entryId} does not belong to List {recipe.name}',
|
||||||
|
status_code=422,
|
||||||
|
)
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|||||||
101
tests/test_22_recipe_service.py
Normal file
101
tests/test_22_recipe_service.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""Test the Product Service Entry Points.
|
||||||
|
|
||||||
|
Simple Grocery List (Sigl) | sigl.app
|
||||||
|
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sigl.domain.service import (
|
||||||
|
product_create,
|
||||||
|
recipe_addItem,
|
||||||
|
recipe_by_id,
|
||||||
|
recipe_create,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Always use 'app' fixture so ORM gets initialized
|
||||||
|
pytestmark = pytest.mark.usefixtures('app')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_recipe_create_defaults(session):
|
||||||
|
"""Test newly created Recipes are empty."""
|
||||||
|
lc = recipe_create(session, 'Test')
|
||||||
|
recipe = recipe_by_id(session, lc.id)
|
||||||
|
|
||||||
|
assert recipe.name == 'Test'
|
||||||
|
assert not recipe.entries
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_recipe_add_product_defaults(session):
|
||||||
|
"""Test adding a Product to a Recipe."""
|
||||||
|
recipe = recipe_create(session, 'Test')
|
||||||
|
entry = recipe_addItem(session, recipe.id, productName='Eggs', productCategory='Dairy')
|
||||||
|
|
||||||
|
assert entry.id is not None
|
||||||
|
assert entry.product is not None
|
||||||
|
assert entry.product.name == 'Eggs'
|
||||||
|
assert entry.product.category == 'Dairy'
|
||||||
|
assert entry.product.remember is True
|
||||||
|
|
||||||
|
assert len(recipe.entries) == 1
|
||||||
|
assert recipe.entries[0] == entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_recipe_add_product_by_id(session):
|
||||||
|
"""Test adding an existing Product to a Recipe by Id."""
|
||||||
|
p1 = product_create(session, 'Eggs', category='Dairy')
|
||||||
|
|
||||||
|
recipe = recipe_create(session, 'Test')
|
||||||
|
entry = recipe_addItem(session, recipe.id, productId=p1.id)
|
||||||
|
|
||||||
|
assert entry.id is not None
|
||||||
|
assert entry.product is not None
|
||||||
|
assert entry.product.name == 'Eggs'
|
||||||
|
assert entry.product.category == 'Dairy'
|
||||||
|
assert entry.product.remember is True
|
||||||
|
|
||||||
|
assert len(recipe.entries) == 1
|
||||||
|
assert recipe.entries[0] == entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_recipe_add_product_by_name(session):
|
||||||
|
"""Test adding an existing Product to a Recipe by Name."""
|
||||||
|
product_create(session, 'Eggs', category='Dairy')
|
||||||
|
|
||||||
|
recipe = recipe_create(session, 'Test')
|
||||||
|
entry = recipe_addItem(session, recipe.id, productName='eggs')
|
||||||
|
|
||||||
|
assert entry.id is not None
|
||||||
|
assert entry.product is not None
|
||||||
|
assert entry.product.name == 'Eggs'
|
||||||
|
assert entry.product.category == 'Dairy'
|
||||||
|
assert entry.product.remember is True
|
||||||
|
|
||||||
|
assert len(recipe.entries) == 1
|
||||||
|
assert recipe.entries[0] == entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_recipe_add_product_no_remember(session):
|
||||||
|
"""Test adding a Product to a Recipe without remembering it."""
|
||||||
|
recipe = recipe_create(session, 'Test')
|
||||||
|
entry = recipe_addItem(
|
||||||
|
session,
|
||||||
|
recipe.id,
|
||||||
|
productName='Eggs',
|
||||||
|
productCategory='Dairy',
|
||||||
|
remember=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert entry.id is not None
|
||||||
|
assert entry.product is not None
|
||||||
|
assert entry.product.name == 'Eggs'
|
||||||
|
assert entry.product.category == 'Dairy'
|
||||||
|
assert entry.product.remember is False
|
||||||
|
|
||||||
|
assert len(recipe.entries) == 1
|
||||||
|
assert recipe.entries[0] == entry
|
||||||
90
tests/test_31_list_recipes.py
Normal file
90
tests/test_31_list_recipes.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""Test the List Recipe Service Entry Points.
|
||||||
|
|
||||||
|
Simple Grocery List (Sigl) | sigl.app
|
||||||
|
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sigl.domain.service import (
|
||||||
|
list_addItem,
|
||||||
|
list_addRecipe,
|
||||||
|
list_create,
|
||||||
|
product_create,
|
||||||
|
recipe_addItem,
|
||||||
|
recipe_create,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Always use 'app' fixture so ORM gets initialized
|
||||||
|
pytestmark = pytest.mark.usefixtures('app')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_add_recipe_empty(session):
|
||||||
|
"""Test adding a Recipe to an empty list."""
|
||||||
|
pEggs = product_create(session, 'Eggs')
|
||||||
|
|
||||||
|
recipe = recipe_create(session, 'Test Recipe')
|
||||||
|
recipe_addItem(session, recipe.id, productId=pEggs.id, quantity='2', notes='Extra Large')
|
||||||
|
recipe_addItem(session, recipe.id, productName='Milk', quantity='1 cup')
|
||||||
|
|
||||||
|
lc = list_create(session, 'Test')
|
||||||
|
lEntries = list_addRecipe(session, lc.id, recipe.id)
|
||||||
|
|
||||||
|
assert(len(lEntries) == 2)
|
||||||
|
assert(len(lc.entries) == 2)
|
||||||
|
|
||||||
|
assert(lc.entries[0].product.name == 'Eggs')
|
||||||
|
assert(lc.entries[0].quantity == '2')
|
||||||
|
assert(lc.entries[0].notes == 'Extra Large')
|
||||||
|
|
||||||
|
assert(lc.entries[1].product.name == 'Milk')
|
||||||
|
assert(lc.entries[1].quantity == '1 cup')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_add_recipe_merge_quantity(session):
|
||||||
|
"""Test adding a Recipe to a list with existing items, merging quantity."""
|
||||||
|
pEggs = product_create(session, 'Eggs')
|
||||||
|
|
||||||
|
recipe = recipe_create(session, 'Test Recipe')
|
||||||
|
recipe_addItem(session, recipe.id, productId=pEggs.id, quantity='2', notes='Extra Large')
|
||||||
|
recipe_addItem(session, recipe.id, productName='Milk', quantity='1 cup')
|
||||||
|
|
||||||
|
lc = list_create(session, 'Test')
|
||||||
|
list_addItem(session, lc.id, productId=pEggs.id, quantity='12')
|
||||||
|
lEntries = list_addRecipe(session, lc.id, recipe.id)
|
||||||
|
|
||||||
|
assert(len(lEntries) == 2)
|
||||||
|
assert(len(lc.entries) == 2)
|
||||||
|
|
||||||
|
assert(lc.entries[0].product.name == 'Eggs')
|
||||||
|
assert(lc.entries[0].quantity == '12, 2 (Test Recipe)')
|
||||||
|
assert(lc.entries[0].notes == 'Extra Large')
|
||||||
|
|
||||||
|
assert(lc.entries[1].product.name == 'Milk')
|
||||||
|
assert(lc.entries[1].quantity == '1 cup')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_add_recipe_merge_notes(session):
|
||||||
|
"""Test adding a Recipe to a list with existing items, merging notes."""
|
||||||
|
pEggs = product_create(session, 'Eggs')
|
||||||
|
|
||||||
|
recipe = recipe_create(session, 'Test Recipe')
|
||||||
|
recipe_addItem(session, recipe.id, productId=pEggs.id, quantity='2', notes='Extra Large')
|
||||||
|
recipe_addItem(session, recipe.id, productName='Milk', quantity='1 cup')
|
||||||
|
|
||||||
|
lc = list_create(session, 'Test')
|
||||||
|
list_addItem(session, lc.id, productId=pEggs.id, notes='Brown, Cage Free')
|
||||||
|
lEntries = list_addRecipe(session, lc.id, recipe.id)
|
||||||
|
|
||||||
|
assert(len(lEntries) == 2)
|
||||||
|
assert(len(lc.entries) == 2)
|
||||||
|
|
||||||
|
assert(lc.entries[0].product.name == 'Eggs')
|
||||||
|
assert(lc.entries[0].quantity == '2')
|
||||||
|
assert(lc.entries[0].notes == 'Brown, Cage Free\nExtra Large')
|
||||||
|
|
||||||
|
assert(lc.entries[1].product.name == 'Milk')
|
||||||
|
assert(lc.entries[1].quantity == '1 cup')
|
||||||
Reference in New Issue
Block a user