Add Remember flag to Products
This commit is contained in:
26
migrations/versions/c28b3a6cdc3a_.py
Normal file
26
migrations/versions/c28b3a6cdc3a_.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: c28b3a6cdc3a
|
||||||
|
Revises: 22dc32e475dd
|
||||||
|
Create Date: 2022-12-24 08:56:13.784788
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c28b3a6cdc3a'
|
||||||
|
down_revision = '22dc32e475dd'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Add the 'remember' column and set to true (original default)
|
||||||
|
op.add_column('products', sa.Column('remember', sa.Boolean(), nullable=True, default=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('products', 'remember')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -64,6 +64,7 @@ products = db.Table(
|
|||||||
db.Column('name', db.String(128), nullable=False),
|
db.Column('name', db.String(128), nullable=False),
|
||||||
db.Column('category', db.String(128), nullable=False, index=True),
|
db.Column('category', db.String(128), nullable=False, index=True),
|
||||||
db.Column('defaultQty', db.String(128), default=None),
|
db.Column('defaultQty', db.String(128), default=None),
|
||||||
|
db.Column('remember', db.Boolean, nullable=False, default=True),
|
||||||
|
|
||||||
# Mixin Columns
|
# Mixin Columns
|
||||||
db.Column('notes', db.String(), default=None),
|
db.Column('notes', db.String(), default=None),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class Product(NotesMixin, TimestampMixin):
|
|||||||
name: str = None
|
name: str = None
|
||||||
category: str = None
|
category: str = None
|
||||||
defaultQty: str = None
|
defaultQty: str = None
|
||||||
|
remember: bool = True
|
||||||
|
|
||||||
# Relationship Fields
|
# Relationship Fields
|
||||||
entries: List['ListEntry'] = field(default_factory=list)
|
entries: List['ListEntry'] = field(default_factory=list)
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ def list_addItem(
|
|||||||
session: Session,
|
session: Session,
|
||||||
id: int,
|
id: int,
|
||||||
*,
|
*,
|
||||||
productId: Optional[int],
|
productId: Optional[int] = None,
|
||||||
productName: Optional[str],
|
productName: Optional[str] = None,
|
||||||
productCategory: Optional[str],
|
productCategory: Optional[str] = None,
|
||||||
quantity: Optional[str],
|
quantity: Optional[str] = None,
|
||||||
notes: Optional[str],
|
remember: Optional[bool] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
) -> ListEntry:
|
) -> ListEntry:
|
||||||
"""Add a Product to a Shopping List.
|
"""Add a Product to a Shopping List.
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ def list_addItem(
|
|||||||
product by ID and add it to the list. If the `product` parameter is not
|
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`
|
provided, a new `Product` will be created with the provided `productName`
|
||||||
and `productCategory` values.
|
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.
|
||||||
"""
|
"""
|
||||||
sList = list_by_id(session, id)
|
sList = list_by_id(session, id)
|
||||||
if not sList:
|
if not sList:
|
||||||
@@ -40,6 +45,8 @@ def list_addItem(
|
|||||||
raise DomainError('Product Name cannot be empty')
|
raise DomainError('Product Name cannot be empty')
|
||||||
|
|
||||||
product = Product(name=productName, category=productCategory)
|
product = Product(name=productName, category=productCategory)
|
||||||
|
if remember is not None:
|
||||||
|
product.remember = remember
|
||||||
|
|
||||||
session.add(product)
|
session.add(product)
|
||||||
|
|
||||||
@@ -97,6 +104,8 @@ def list_deleteCrossedOff(session: Session, id: int) -> ShoppingList:
|
|||||||
raise NotFoundError(f'List {id} does not exist')
|
raise NotFoundError(f'List {id} does not exist')
|
||||||
|
|
||||||
for entry in sList.entries:
|
for entry in sList.entries:
|
||||||
|
if not entry.product.remember:
|
||||||
|
session.delete(entry.product)
|
||||||
if entry.crossedOff:
|
if entry.crossedOff:
|
||||||
session.delete(entry)
|
session.delete(entry)
|
||||||
|
|
||||||
@@ -108,6 +117,8 @@ def list_deleteCrossedOff(session: Session, id: int) -> ShoppingList:
|
|||||||
def list_deleteItem(session: Session, listId: int, entryId: int):
|
def list_deleteItem(session: Session, listId: int, entryId: int):
|
||||||
"""Delete an Entry from a Shopping List."""
|
"""Delete an Entry from a Shopping List."""
|
||||||
entry = list_entry_by_id(session, listId, entryId)
|
entry = list_entry_by_id(session, listId, entryId)
|
||||||
|
if not entry.product.remember:
|
||||||
|
session.delete(entry.product)
|
||||||
|
|
||||||
session.delete(entry)
|
session.delete(entry)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -118,8 +129,8 @@ def list_editItem(
|
|||||||
listId: int,
|
listId: int,
|
||||||
entryId: int,
|
entryId: int,
|
||||||
*,
|
*,
|
||||||
quantity: Optional[str],
|
quantity: Optional[str] = None,
|
||||||
notes: Optional[str],
|
notes: Optional[str] = None,
|
||||||
) -> ListEntry:
|
) -> ListEntry:
|
||||||
"""Edit an Entry on a Shopping List."""
|
"""Edit an Entry on a Shopping List."""
|
||||||
entry = list_entry_by_id(session, listId, entryId)
|
entry = list_entry_by_id(session, listId, entryId)
|
||||||
@@ -133,7 +144,7 @@ def list_editItem(
|
|||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
def list_stores(session: Session, id: Optional[int]) -> List[str]:
|
def list_stores(session: Session, id: Optional[int] = None) -> List[str]:
|
||||||
"""Get a list of all Stores for the List.
|
"""Get a list of all Stores for the List.
|
||||||
|
|
||||||
This helper returns a list of all Stores for which the Products in the
|
This helper returns a list of all Stores for which the Products in the
|
||||||
@@ -163,8 +174,8 @@ def list_stores(session: Session, id: Optional[int]) -> List[str]:
|
|||||||
def list_update(
|
def list_update(
|
||||||
session: Session,
|
session: Session,
|
||||||
id: int,
|
id: int,
|
||||||
name: Union[str, None],
|
name: Union[str, None] = None,
|
||||||
notes: Union[str, None],
|
notes: Union[str, None] = None,
|
||||||
) -> ShoppingList:
|
) -> ShoppingList:
|
||||||
"""Update the Name and/or Notes of a Shopping List."""
|
"""Update the Name and/or Notes of a Shopping List."""
|
||||||
sList = list_by_id(session, id)
|
sList = list_by_id(session, id)
|
||||||
@@ -215,7 +226,7 @@ def list_entry_set_crossedOff(session: Session, listId: int, entryId: int, cross
|
|||||||
|
|
||||||
def products_all(session: Session) -> List[Product]:
|
def products_all(session: Session) -> List[Product]:
|
||||||
"""Return all Products."""
|
"""Return all Products."""
|
||||||
return session.query(Product).all()
|
return session.query(Product).filter(Product.remember is True).all()
|
||||||
|
|
||||||
|
|
||||||
def product_by_id(session: Session, id: int) -> Optional[Product]:
|
def product_by_id(session: Session, id: int) -> Optional[Product]:
|
||||||
@@ -227,11 +238,15 @@ def product_create(
|
|||||||
session: Session,
|
session: Session,
|
||||||
name: str,
|
name: str,
|
||||||
*,
|
*,
|
||||||
category: Optional[str],
|
category: Optional[str] = None,
|
||||||
notes: Optional[str],
|
remember: Optional[bool] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
) -> Product:
|
) -> Product:
|
||||||
"""Create a new Product."""
|
"""Create a new Product."""
|
||||||
product = Product(name=name, category=category, notes=notes)
|
product = Product(name=name, category=category, notes=notes)
|
||||||
|
if remember is not None:
|
||||||
|
product.remember = remember
|
||||||
|
|
||||||
session.add(product)
|
session.add(product)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@@ -252,8 +267,8 @@ def product_update(
|
|||||||
session: Session,
|
session: Session,
|
||||||
id: int,
|
id: int,
|
||||||
name: str,
|
name: str,
|
||||||
category: Optional[str],
|
category: Optional[str] = None,
|
||||||
notes: Optional[str],
|
notes: Optional[str] = None,
|
||||||
) -> Product:
|
) -> Product:
|
||||||
"""Update a Product."""
|
"""Update a Product."""
|
||||||
product = product_by_id(session, id)
|
product = product_by_id(session, id)
|
||||||
@@ -276,8 +291,8 @@ def product_addLocation(
|
|||||||
id: int,
|
id: int,
|
||||||
store: str,
|
store: str,
|
||||||
*,
|
*,
|
||||||
aisle: Optional[str],
|
aisle: Optional[str] = None,
|
||||||
bin: Optional[str]
|
bin: Optional[str] = None,
|
||||||
) -> ProductLocation:
|
) -> ProductLocation:
|
||||||
"""Add a Store Location to a Product."""
|
"""Add a Store Location to a Product."""
|
||||||
product = product_by_id(session, id)
|
product = product_by_id(session, id)
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ def session(request, monkeypatch, app):
|
|||||||
monkeypatch.setattr(_db, 'session', session)
|
monkeypatch.setattr(_db, 'session', session)
|
||||||
|
|
||||||
def teardown():
|
def teardown():
|
||||||
transaction.rollback()
|
if transaction.is_active:
|
||||||
|
transaction.rollback()
|
||||||
connection.close()
|
connection.close()
|
||||||
session.remove()
|
session.remove()
|
||||||
|
|
||||||
|
|||||||
@@ -76,3 +76,13 @@ def test_product_model_same_store_fails(session):
|
|||||||
|
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_product_model_remembers_by_default(session):
|
||||||
|
"""Test that the Product defaults to remembering."""
|
||||||
|
p = Product(name='Eggs', category='Dairy')
|
||||||
|
session.add(p)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
assert p.remember is True
|
||||||
|
|||||||
36
tests/test_21_product_service.py
Normal file
36
tests/test_21_product_service.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""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_by_id, product_create
|
||||||
|
|
||||||
|
# Always use 'app' fixture so ORM gets initialized
|
||||||
|
pytestmark = pytest.mark.usefixtures('app')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_product_create_defaults(session):
|
||||||
|
"""Test newly created Products have no Locations."""
|
||||||
|
pc = product_create(session, 'Eggs', category='Dairy')
|
||||||
|
p = product_by_id(session, pc.id)
|
||||||
|
|
||||||
|
assert p.name == 'Eggs'
|
||||||
|
assert p.category == 'Dairy'
|
||||||
|
assert p.remember is True
|
||||||
|
assert not p.locations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_product_create_forget(session):
|
||||||
|
"""Test newly created Products can have remember as false."""
|
||||||
|
pc = product_create(session, 'Eggs', category='Dairy', remember=False)
|
||||||
|
p = product_by_id(session, pc.id)
|
||||||
|
|
||||||
|
assert p.name == 'Eggs'
|
||||||
|
assert p.category == 'Dairy'
|
||||||
|
assert p.remember is False
|
||||||
|
assert not p.locations
|
||||||
107
tests/test_22_list_service.py
Normal file
107
tests/test_22_list_service.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""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 (
|
||||||
|
list_addItem,
|
||||||
|
list_by_id,
|
||||||
|
list_create,
|
||||||
|
list_deleteCrossedOff,
|
||||||
|
list_entry_set_crossedOff,
|
||||||
|
product_by_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Always use 'app' fixture so ORM gets initialized
|
||||||
|
pytestmark = pytest.mark.usefixtures('app')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_create_defaults(session):
|
||||||
|
"""Test newly created Lists are empty."""
|
||||||
|
lc = list_create(session, 'Test')
|
||||||
|
list = list_by_id(session, lc.id)
|
||||||
|
|
||||||
|
assert list.name == 'Test'
|
||||||
|
assert not list.entries
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_add_product_defaults(session):
|
||||||
|
"""Test adding a Product to a List."""
|
||||||
|
list = list_create(session, 'Test')
|
||||||
|
entry = list_addItem(session, list.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(list.entries) == 1
|
||||||
|
assert list.entries[0] == entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_add_product_no_remember(session):
|
||||||
|
"""Test adding a Product to a List without remembering it."""
|
||||||
|
list = list_create(session, 'Test')
|
||||||
|
entry = list_addItem(
|
||||||
|
session,
|
||||||
|
list.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(list.entries) == 1
|
||||||
|
assert list.entries[0] == entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_removes_product_with_remember(session):
|
||||||
|
"""Test that checking off and deleting a remembered Product does not delete the Product Entry."""
|
||||||
|
list = list_create(session, 'Test')
|
||||||
|
entry = list_addItem(
|
||||||
|
session,
|
||||||
|
list.id,
|
||||||
|
productName='Eggs',
|
||||||
|
productCategory='Dairy',
|
||||||
|
remember=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
pid = entry.product.id
|
||||||
|
|
||||||
|
list_entry_set_crossedOff(session, list.id, entry.id, True)
|
||||||
|
list_deleteCrossedOff(session, list.id)
|
||||||
|
|
||||||
|
assert product_by_id(session, pid) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_list_removes_product_no_remember(session):
|
||||||
|
"""Test that checking off and deleting a non-remembered Product deletes the Product Entry also."""
|
||||||
|
list = list_create(session, 'Test')
|
||||||
|
entry = list_addItem(
|
||||||
|
session,
|
||||||
|
list.id,
|
||||||
|
productName='Eggs',
|
||||||
|
productCategory='Dairy',
|
||||||
|
remember=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
pid = entry.product.id
|
||||||
|
|
||||||
|
list_entry_set_crossedOff(session, list.id, entry.id, True)
|
||||||
|
list_deleteCrossedOff(session, list.id)
|
||||||
|
|
||||||
|
assert product_by_id(session, pid) is None
|
||||||
Reference in New Issue
Block a user