diff --git a/sigl/domain/service.py b/sigl/domain/service.py index ef7c660..bb2799e 100644 --- a/sigl/domain/service.py +++ b/sigl/domain/service.py @@ -4,12 +4,13 @@ Simple Grocery List (Sigl) | sigl.app Copyright (c) 2022 Asymworks, LLC. All Rights Reserved. """ +from nis import cat from typing import List, Optional, Union from sqlalchemy.orm import Session from sigl.exc import DomainError, NotFoundError -from .models import ListEntry, Product, ShoppingList +from .models import ListEntry, Product, ProductLocation, ShoppingList def list_addItem( @@ -132,12 +133,16 @@ def list_editItem( return entry -def list_stores(session: Session, id: int) -> List[str]: +def list_stores(session: Session, id: Optional[int]) -> List[str]: """Get a list of all Stores for the List. This helper returns a list of all Stores for which the Products in the - List have locations. + List have locations. If the List ID is `None`, all stores for which any + Product has locations are returned. """ + if id is None: + return list(set([l.store for l in session.query(ProductLocation).all()])) + sList = list_by_id(session, id) if not sList: raise NotFoundError(f'List {id} does not exist') @@ -216,3 +221,92 @@ def products_all(session: Session) -> List[Product]: def product_by_id(session: Session, id: int) -> Optional[Product]: """Load a specific Product.""" return session.query(Product).filter(Product.id == id).one_or_none() + + +def product_create( + session: Session, + name: str, + *, + category: Optional[str], + notes: Optional[str], +) -> Product: + """Create a new Product.""" + product = Product(name=name, category=category, notes=notes) + session.add(product) + session.commit() + + return product + + +def product_delete(session: Session, id: int): + """Delete a Product.""" + product = product_by_id(session, id) + if not product: + raise NotFoundError(f'Product {id} does not exist') + + session.delete(product) + session.commit() + + +def product_update( + session: Session, + id: int, + name: str, + category: Optional[str], + notes: Optional[str], +) -> Product: + """Update a Product.""" + product = product_by_id(session, id) + if not product: + raise NotFoundError(f'Product {id} does not exist') + + product.name = name + product.category = category + product.notes = notes + product.set_modified_at() + + session.add(product) + session.commit() + + return product + + +def product_addLocation( + session: Session, + id: int, + store: str, + *, + aisle: Optional[str], + bin: Optional[str] +) -> ProductLocation: + """Add a Store Location to a Product.""" + product = product_by_id(session, id) + if not product: + raise NotFoundError(f'Product {id} does not exist') + + for loc in product.locations: + if loc.store.lower() == store.lower(): + raise DomainError(f'A location already exists for store {loc.store}') + + loc = ProductLocation(product=product, store=store, aisle=aisle, bin=bin) + session.add(loc) + session.commit() + + return loc + + +def product_removeLocation( + session: Session, + id: int, + store: str +): + """Remove a Store Location from a Product.""" + product = product_by_id(session, id) + if not product: + raise NotFoundError(f'Product {id} does not exist') + + for loc in product.locations: + if loc.store.lower() == store.lower(): + session.delete(loc) + + session.commit() diff --git a/sigl/templates/base.html.j2 b/sigl/templates/base.html.j2 index 0db372f..dc615be 100644 --- a/sigl/templates/base.html.j2 +++ b/sigl/templates/base.html.j2 @@ -25,8 +25,8 @@
- Shopping Lists - Products + Shopping Lists + Products
diff --git a/sigl/templates/lists/editItem.html.j2 b/sigl/templates/lists/editItem.html.j2 index c88f76a..f42a96b 100644 --- a/sigl/templates/lists/editItem.html.j2 +++ b/sigl/templates/lists/editItem.html.j2 @@ -24,7 +24,7 @@ Cancel - + + + + +{% endblock %} \ No newline at end of file diff --git a/sigl/templates/products/detail.html.j2 b/sigl/templates/products/detail.html.j2 new file mode 100644 index 0000000..7f8e3e2 --- /dev/null +++ b/sigl/templates/products/detail.html.j2 @@ -0,0 +1,150 @@ +{% extends 'base.html.j2' %} +{% block title %}{{ product.name }} | Sigl{% endblock %} +{% block header %} +
+
Edit {{ product.name }}
+
+{% endblock %} +{% block main %} +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ Product Locations + + + + + + + + + + +{% for loc in product.locations %} + + + + + + +{% endfor %} + + + + + + + + + +
StoreAisleBin
{{ loc.store }}{{ loc.aisle }}{{ loc.bin or '' }} + +
+ +
+
+
+
+
+ + Cancel + + +
+
+ +
+
+
+
+
+
+ + + +
+
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/sigl/templates/products/home.html.j2 b/sigl/templates/products/home.html.j2 new file mode 100644 index 0000000..b8d1745 --- /dev/null +++ b/sigl/templates/products/home.html.j2 @@ -0,0 +1,82 @@ +{% extends 'base.html.j2' %} +{% block title %}Products | Sigl{% endblock %} +{% block header %} +
+
+ +
+ + + + + New Product + +
+{% endblock %} +{% block main %} +{% if products|length == 0 %} +
+
No Products
+
+{% else %} +{% for hdr in groups.keys()|sort %} +{% set outer_loop = loop %} +
{{ hdr }}
+ +{% endfor %} + +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/sigl/views/__init__.py b/sigl/views/__init__.py index f445d97..8dad769 100644 --- a/sigl/views/__init__.py +++ b/sigl/views/__init__.py @@ -5,6 +5,7 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved. """ from .lists import bp as list_bp +from .products import bp as products_bp __all__ = ('init_views', ) @@ -12,6 +13,7 @@ __all__ = ('init_views', ) def init_views(app): """Register the View Blueprints with the Application.""" app.register_blueprint(list_bp) + app.register_blueprint(products_bp) # Notify Initialization Complete app.logger.debug('Views Initialized') diff --git a/sigl/views/lists.py b/sigl/views/lists.py index ff51ebb..ecb4294 100644 --- a/sigl/views/lists.py +++ b/sigl/views/lists.py @@ -37,8 +37,8 @@ def home(): def create(): """Create Shopping List View.""" if request.method == 'POST': - list_name = request.form['name'] - list_notes = request.form['notes'] + list_name = request.form['name'].strip() + list_notes = request.form['notes'].strip() if not list_name: flash('Error: List Name is required') return render_template('lists/create.html.j2') @@ -119,8 +119,8 @@ def update(id): list_update( db.session, id, - name=request.form.get('name', sList.name), - notes=request.form.get('notes', sList.notes), + name=request.form.get('name', sList.name).strip(), + notes=request.form.get('notes', sList.notes).strip(), ) return redirect(url_for('lists.detail', id=id)) @@ -213,10 +213,10 @@ def addItem(id): ) productId = request.form['product'] - productName = request.form.get('productName', None) - productCategory = request.form.get('productCategory', None) - quantity = request.form.get('quantity', None) - notes = request.form.get('notes', None) + productName = request.form.get('productName', '').strip() + productCategory = request.form.get('productCategory', '').strip() + quantity = request.form.get('quantity', '').strip() + notes = request.form.get('notes', '').strip() if productId == 'new' or productId == '': productId = None @@ -249,8 +249,8 @@ def editItem(listId, entryId): try: entry = list_entry_by_id(db.session, listId, entryId) if request.method == 'POST': - quantity = request.form.get('quantity', None) - notes = request.form.get('notes', None) + quantity = request.form.get('quantity', '').strip() + notes = request.form.get('notes', '').strip() list_editItem( db.session, diff --git a/sigl/views/products.py b/sigl/views/products.py new file mode 100644 index 0000000..7a03533 --- /dev/null +++ b/sigl/views/products.py @@ -0,0 +1,147 @@ +"""Sigl Products View Blueprint. + +Simple Grocery List (Sigl) | sigl.app +Copyright (c) 2022 Asymworks, LLC. All Rights Reserved. +""" + +from flask import ( + Blueprint, + flash, redirect, render_template, request, url_for +) + +from sigl.exc import Error, NotFoundError +from sigl.database import db +from sigl.domain.service import ( + list_stores, + product_update, + products_all, + product_by_id, + product_create, + product_delete, + product_update, + product_addLocation, + product_removeLocation, +) + +__all__ = ('bp', ) + + +#: Lists Blueprint +bp = Blueprint('products', __name__) + + +@bp.route('/products') +def home(): + """All Products View.""" + products = products_all(db.session) + groups = dict() + for product in products: + cat = 'No Category' + if product.category: + cat = product.category + + if cat not in groups: + groups[cat] = [product] + else: + groups[cat].append(product) + + return render_template('products/home.html.j2', products=products, groups=groups) + + +@bp.route('/products/new', methods=('GET', 'POST')) +def create(): + """Create a new Product.""" + if request.method == 'POST': + product_name = request.form['name'].strip() + product_category = request.form['category'].strip() + product_notes = request.form['notes'].strip() + if not product_name: + flash('Error: Product Name is required') + return render_template('products/create.html.j2') + + product = product_create( + db.session, + product_name, + category=product_category, + notes=product_notes, + ) + return redirect(url_for('products.detail', id=product.id)) + return render_template('products/create.html.j2') + +@bp.route('/products/', methods=('GET', 'POST')) +def detail(id): + """Product Detail/Editing View.""" + try: + product = product_by_id(db.session, id); + if not product: + raise NotFoundError(f'Product {id} not found') + + except Error as e: + flash(str(e), 'error') + return redirect(url_for('products.home')) + + if request.method == 'POST': + try: + name = request.form['name'].strip() + category = request.form['category'].strip() + notes = request.form['notes'].strip() + + product_update( + db.session, + id, + name, + category, + notes, + ) + + return redirect(url_for('products.home')) + + except Error as e: + flash(str(e), 'error') + + return render_template( + 'products/detail.html.j2', + product=product, + stores=list_stores(db.session, None), + ) + + + +@bp.route('/products//delete', methods=('POST', )) +def delete(id): + """Delete a Product.""" + try: + product_delete(db.session, id) + except Error as e: + flash(str(e), 'error') + + return redirect(url_for('products.home')) + + +@bp.route('/products//addLocation', methods=('POST', )) +def addLocation(id): + """Add a Location to a Product.""" + store = request.form['store'].strip() + aisle = request.form['aisle'].strip() + bin = request.form['bin'].strip() + + try: + product_addLocation(db.session, id, store, aisle=aisle, bin=bin) + except Error as e: + flash(str(e), 'error') + + return redirect(url_for('products.detail', id=id)) + + +@bp.route('/products//removeLocation', methods=('POST', )) +def removeLocation(id): + """Remove a Location from a Product.""" + store = request.form['store'].strip() + print(request.form) + + try: + product_removeLocation(db.session, id, store) + except Error as e: + flash(str(e), 'error') + + return redirect(url_for('products.detail', id=id))