Add Product Views
This commit is contained in:
@@ -4,12 +4,13 @@ Simple Grocery List (Sigl) | sigl.app
|
|||||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from nis import cat
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from sigl.exc import DomainError, NotFoundError
|
from sigl.exc import DomainError, NotFoundError
|
||||||
from .models import ListEntry, Product, ShoppingList
|
from .models import ListEntry, Product, ProductLocation, ShoppingList
|
||||||
|
|
||||||
|
|
||||||
def list_addItem(
|
def list_addItem(
|
||||||
@@ -132,12 +133,16 @@ def list_editItem(
|
|||||||
return entry
|
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.
|
"""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
|
||||||
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)
|
sList = list_by_id(session, id)
|
||||||
if not sList:
|
if not sList:
|
||||||
raise NotFoundError(f'List {id} does not exist')
|
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]:
|
def product_by_id(session: Session, id: int) -> Optional[Product]:
|
||||||
"""Load a specific Product."""
|
"""Load a specific Product."""
|
||||||
return session.query(Product).filter(Product.id == id).one_or_none()
|
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()
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="ml-4 flex items-baseline space-x-4">
|
<div class="ml-4 flex items-baseline space-x-4">
|
||||||
<a href="{{ url_for('lists.home') }}" class="text-white border-b border-white mx-2 py-1 text-sm font-medium" aria-current="page">Shopping Lists</a>
|
<a href="{{ url_for('lists.home') }}" class="{% if request.blueprint == 'lists' %}text-white border-b border-white mx-2 py-1 text-sm font-medium{% else %}text-gray-300 hover:bg-gray-700 hover:text-white px-2 py-1 rounded-md text-sm font-medium{% endif %}" aria-current="page">Shopping Lists</a>
|
||||||
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white px-2 py-1 rounded-md text-sm font-medium">Products</a>
|
<a href="{{ url_for('products.home') }}" class="{% if request.blueprint == 'products' %}text-white border-b border-white mx-2 py-1 text-sm font-medium{% else %}text-gray-300 hover:bg-gray-700 hover:text-white px-2 py-1 rounded-md text-sm font-medium{% endif %}">Products</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<a href="{{ url_for('lists.detail', id=list.id) }}" class="px-2 py-1 text-sm text-white bg-gray-600 hover:bg-gray-700 border rounded flex justify-between items-center">
|
<a href="{{ url_for('lists.detail', id=list.id) }}" class="px-2 py-1 text-sm text-white bg-gray-600 hover:bg-gray-700 border rounded flex justify-between items-center">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
<button id="delete-list-btn" class="flex ml-2 px-2 py-1 text-sm text-white bg-red-600 hover:bg-red-700 border rounded flex justify-between items-center" onclick="deleteItem()">
|
<button type="button" id="delete-list-btn" class="flex ml-2 px-2 py-1 text-sm text-white bg-red-600 hover:bg-red-700 border rounded flex justify-between items-center" onclick="deleteItem()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="pr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" class="pr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{% block main %}
|
{% block main %}
|
||||||
{% if lists|length == 0 %}
|
{% if lists|length == 0 %}
|
||||||
<div class="py-2 border-b border-gray-300">
|
<div class="py-2 border-b border-gray-300">
|
||||||
<div class="ml-4 text-sm">No shopping lists</div>
|
<div class="ml-4">No shopping lists</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<ul class="w-full">
|
<ul class="w-full">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<a href="{{ url_for('lists.detail', id=list.id) }}" class="px-2 py-1 text-sm text-white bg-gray-600 hover:bg-gray-700 border rounded flex justify-between items-center">
|
<a href="{{ url_for('lists.detail', id=list.id) }}" class="px-2 py-1 text-sm text-white bg-gray-600 hover:bg-gray-700 border rounded flex justify-between items-center">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
<button id="delete-list-btn" class="ml-2 px-2 py-1 text-sm text-white bg-red-600 hover:bg-red-700 border rounded flex justify-between items-center" onclick="deleteList()">
|
<button type="submit" id="delete-list-btn" class="ml-2 px-2 py-1 text-sm text-white bg-red-600 hover:bg-red-700 border rounded flex justify-between items-center" onclick="deleteList()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
35
sigl/templates/products/create.html.j2
Normal file
35
sigl/templates/products/create.html.j2
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
{% block title %}Create New Product | Sigl{% endblock %}
|
||||||
|
{% block header %}
|
||||||
|
<div class="flex flex-col justify-start items-start sm:flex-row sm:justify-between sm:items-center sm:py-1">
|
||||||
|
<div class="text-sm w-full text-gray-800 py-1 border-b sm:border-none">Create New Product</span></span></div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<form method="post">
|
||||||
|
<div class="py-2 px-4 flex flex-col">
|
||||||
|
<div class="flex flex-col pb-4">
|
||||||
|
<label for="name" class="py-1 text-xs text-gray-700 font-semibold">Product Name:</label>
|
||||||
|
<input type="text" name="name" id="name" class="p-1 text-sm border border-gray-200 rounded" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-4">
|
||||||
|
<label for="category" class="py-1 text-xs text-gray-700 font-semibold">Product Category:</label>
|
||||||
|
<input type="text" name="category" id="category" class="p-1 text-sm border border-gray-200 rounded" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-4">
|
||||||
|
<label for="notes" class="py-1 text-xs text-gray-700 font-semibold">Notes:</label>
|
||||||
|
<textarea name="notes" id="notes" class="p-1 text-sm border border-gray-200 rounded"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<div class="flex justify-start items-start">
|
||||||
|
<a href="{{ url_for('products.home') }}" class="px-2 py-1 text-sm text-white bg-gray-600 hover:bg-gray-700 border rounded flex justify-between items-center">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="px-2 py-1 border rounded text-sm text-white bg-blue-600 hover:bg-blue-700">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
150
sigl/templates/products/detail.html.j2
Normal file
150
sigl/templates/products/detail.html.j2
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
{% block title %}{{ product.name }} | Sigl{% endblock %}
|
||||||
|
{% block header %}
|
||||||
|
<div class="flex flex-col justify-start items-start sm:flex-row sm:justify-between sm:items-center sm:py-1">
|
||||||
|
<div class="text-sm w-full text-gray-800 py-1 border-b sm:border-none">Edit <span class="font-bold ">{{ product.name }}</span></span></div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<form method="post">
|
||||||
|
<div class="py-2 px-4 flex flex-col">
|
||||||
|
<div class="flex flex-col pb-4">
|
||||||
|
<label for="name" class="py-1 text-xs text-gray-700 font-semibold">Product Name:</label>
|
||||||
|
<input type="text" name="name" id="name" class="p-1 text-sm border border-gray-200 rounded" value="{{ product.name }}" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-4">
|
||||||
|
<label for="category" class="py-1 text-xs text-gray-700 font-semibold">Product Category:</label>
|
||||||
|
<input type="text" name="category" id="category" class="p-1 text-sm border border-gray-200 rounded" value="{{ product.category }}" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-4">
|
||||||
|
<label for="notes" class="py-1 text-xs text-gray-700 font-semibold">Notes:</label>
|
||||||
|
<textarea name="notes" id="notes" class="p-1 text-sm border border-gray-200 rounded">{{ product.notes or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="pb-4">
|
||||||
|
<fieldset>
|
||||||
|
<legend class="text-sm font-semibold">Product Locations</legend>
|
||||||
|
<table class="w-full table-fixed mx-2">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-sm">
|
||||||
|
<th class="w-2/5 text-left">Store</th>
|
||||||
|
<th class="w-1/4 text-left">Aisle</th>
|
||||||
|
<th class="w-1/4 text-left">Bin</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for loc in product.locations %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ loc.store }}</td>
|
||||||
|
<td>{{ loc.aisle }}</td>
|
||||||
|
<td>{{ loc.bin or '' }}</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="pt-1 text-red-600 hover:text-red-700" onclick='removeLocation("{{ loc.store }}")'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mx-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td><input type="text" id="locStore" class="w-full border rounded text" /></td>
|
||||||
|
<td><input type="text" id="locAisle" class="w-full border rounded text" /></td>
|
||||||
|
<td><input type="text" id="locBin" class="w-full border rounded text" /></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="pt-1 text-green-600 hover:text-green-700" onclick="addLocation()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mx-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="{{ url_for('products.home') }}" class="px-2 py-1 text-sm text-white bg-gray-600 hover:bg-gray-700 border rounded flex justify-between items-center">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
<button type="button" id="delete-item-btn" class="flex ml-2 px-2 py-1 text-sm text-white bg-red-600 hover:bg-red-700 border rounded flex justify-between items-center" onclick="deleteItem()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="pr-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
Delete Product
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="px-2 py-1 border rounded text-sm text-white bg-blue-600 hover:bg-blue-700">Update Item</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form action="{{ url_for('products.delete', id=product.id) }}" method="post" id="delete-item-form"></form>
|
||||||
|
<form action="{{ url_for('products.addLocation', id=product.id) }}" method="post" id="add-loc-form">
|
||||||
|
<input type="hidden" name="store" id="addLocationStore" />
|
||||||
|
<input type="hidden" name="aisle" id="addLocationAisle" />
|
||||||
|
<input type="hidden" name="bin" id="addLocationBin" />
|
||||||
|
</form>
|
||||||
|
<form action="{{ url_for('products.removeLocation', id=product.id) }}" method="post" id="remove-loc-form">
|
||||||
|
<input type="hidden" name="store" id="removeLocationStore" />
|
||||||
|
</form>
|
||||||
|
<script language="javascript">
|
||||||
|
function deleteItem() {
|
||||||
|
const delForm = document.getElementById('delete-item-form');
|
||||||
|
console.log(delForm);
|
||||||
|
if (delForm && confirm('Are you sure you want to delete product "{{ product.name }}"? This cannot be undone.')) {
|
||||||
|
console.log(delForm.action);
|
||||||
|
delForm.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function addLocation() {
|
||||||
|
const form = document.getElementById('add-loc-form');
|
||||||
|
const fStore = document.getElementById('addLocationStore');
|
||||||
|
const fAisle = document.getElementById('addLocationAisle');
|
||||||
|
const fBin = document.getElementById('addLocationBin');
|
||||||
|
|
||||||
|
const iStore = document.getElementById('locStore');
|
||||||
|
const iAisle = document.getElementById('locAisle');
|
||||||
|
const iBin = document.getElementById('locBin');
|
||||||
|
|
||||||
|
if (!form || !fStore || !fAisle || !fBin || !iStore || !iAisle || !iBin) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent('notice', {
|
||||||
|
detail: {
|
||||||
|
type: 'error',
|
||||||
|
text: 'An internal error occurred when adding the product location.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fStore.value = iStore.value;
|
||||||
|
fAisle.value = iAisle.value;
|
||||||
|
fBin.value = iBin.value;
|
||||||
|
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
function removeLocation(store) {
|
||||||
|
const form = document.getElementById('remove-loc-form');
|
||||||
|
const fStore = document.getElementById('removeLocationStore');
|
||||||
|
|
||||||
|
if (!form || !fStore) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent('notice', {
|
||||||
|
detail: {
|
||||||
|
type: 'error',
|
||||||
|
text: 'An internal error occurred when removing the product location.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fStore.value = store;
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
82
sigl/templates/products/home.html.j2
Normal file
82
sigl/templates/products/home.html.j2
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{% extends 'base.html.j2' %}
|
||||||
|
{% block title %}Products | Sigl{% endblock %}
|
||||||
|
{% block header %}
|
||||||
|
<div class="flex justify-between items-center py-2">
|
||||||
|
<div class="text-lg grow mr-2 border-b text-gray-800">
|
||||||
|
<input type="search" id="search" class="w-full outline-none focus:outline-none" placeholder="Search Products" oninput="filterList()" />
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('products.create') }}" class="shrink-0 px-2 py-1 text-sm text-white bg-green-600 hover:bg-green-700 border rounded flex justify-between items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
New Product
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
{% if products|length == 0 %}
|
||||||
|
<div class="py-2 border-b border-gray-300">
|
||||||
|
<div class="ml-4">No Products</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% for hdr in groups.keys()|sort %}
|
||||||
|
{% set outer_loop = loop %}
|
||||||
|
<div class="sigl-category text-sm text-gray-600 font-bold py-1 px-4 bg-gray-50 border-b border-gray-300">{{ hdr }}</div>
|
||||||
|
<ul class="sigl-list">
|
||||||
|
{% for e in groups[hdr]|sort(attribute='name') %}
|
||||||
|
<li data-value="{{ e.name|lower }}" class="sigl-list-item block w-full py-2{% if not (loop.last and outer_loop.last) %} border-b border-gray-300{% endif %}">
|
||||||
|
<a href="{{ url_for('products.detail', id=e.id) }}"><div class="px-4">{{ e.name }}</div></a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
<div id="sigl-no-products-found" class="hidden py-2 border-b border-gray-300">
|
||||||
|
<div class="ml-4">No Products Found</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<script language="javascript">
|
||||||
|
function filterList() {
|
||||||
|
let nshown = 0;
|
||||||
|
const searchEl = document.getElementById('search');
|
||||||
|
const searchText = searchEl && searchEl.value.toLowerCase() || '';
|
||||||
|
document.querySelectorAll('.sigl-list-item').forEach(function (itm) {
|
||||||
|
const itmValue = itm.dataset.value;
|
||||||
|
if (searchText === '') {
|
||||||
|
console.log(`Showing ${itmValue}`)
|
||||||
|
itm.classList.remove('hidden');
|
||||||
|
nshown += 1;
|
||||||
|
} else {
|
||||||
|
if (itmValue.includes(searchText)) {
|
||||||
|
console.log(`Hiding ${itmValue}`)
|
||||||
|
itm.classList.remove('hidden');
|
||||||
|
nshown += 1;
|
||||||
|
} else {
|
||||||
|
console.log(`Showing ${itmValue}`)
|
||||||
|
itm.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.sigl-category').forEach(function (itm) {
|
||||||
|
const ul = itm.nextElementSibling;
|
||||||
|
const lis = [].slice.call(ul.children);
|
||||||
|
const nShowing = lis
|
||||||
|
.map((itm) => itm.classList.contains('hidden') ? 0 : 1)
|
||||||
|
.reduce((p, i) => p + i, 0);
|
||||||
|
|
||||||
|
if (nShowing === 0) {
|
||||||
|
itm.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
itm.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const noneFound = document.getElementById('sigl-no-products-found');
|
||||||
|
if (nshown === 0) {
|
||||||
|
noneFound.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
noneFound.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,6 +5,7 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .lists import bp as list_bp
|
from .lists import bp as list_bp
|
||||||
|
from .products import bp as products_bp
|
||||||
|
|
||||||
__all__ = ('init_views', )
|
__all__ = ('init_views', )
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ __all__ = ('init_views', )
|
|||||||
def init_views(app):
|
def init_views(app):
|
||||||
"""Register the View Blueprints with the Application."""
|
"""Register the View Blueprints with the Application."""
|
||||||
app.register_blueprint(list_bp)
|
app.register_blueprint(list_bp)
|
||||||
|
app.register_blueprint(products_bp)
|
||||||
|
|
||||||
# Notify Initialization Complete
|
# Notify Initialization Complete
|
||||||
app.logger.debug('Views Initialized')
|
app.logger.debug('Views Initialized')
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ def home():
|
|||||||
def create():
|
def create():
|
||||||
"""Create Shopping List View."""
|
"""Create Shopping List View."""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
list_name = request.form['name']
|
list_name = request.form['name'].strip()
|
||||||
list_notes = request.form['notes']
|
list_notes = request.form['notes'].strip()
|
||||||
if not list_name:
|
if not list_name:
|
||||||
flash('Error: List Name is required')
|
flash('Error: List Name is required')
|
||||||
return render_template('lists/create.html.j2')
|
return render_template('lists/create.html.j2')
|
||||||
@@ -119,8 +119,8 @@ def update(id):
|
|||||||
list_update(
|
list_update(
|
||||||
db.session,
|
db.session,
|
||||||
id,
|
id,
|
||||||
name=request.form.get('name', sList.name),
|
name=request.form.get('name', sList.name).strip(),
|
||||||
notes=request.form.get('notes', sList.notes),
|
notes=request.form.get('notes', sList.notes).strip(),
|
||||||
)
|
)
|
||||||
return redirect(url_for('lists.detail', id=id))
|
return redirect(url_for('lists.detail', id=id))
|
||||||
|
|
||||||
@@ -213,10 +213,10 @@ def addItem(id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
productId = request.form['product']
|
productId = request.form['product']
|
||||||
productName = request.form.get('productName', None)
|
productName = request.form.get('productName', '').strip()
|
||||||
productCategory = request.form.get('productCategory', None)
|
productCategory = request.form.get('productCategory', '').strip()
|
||||||
quantity = request.form.get('quantity', None)
|
quantity = request.form.get('quantity', '').strip()
|
||||||
notes = request.form.get('notes', None)
|
notes = request.form.get('notes', '').strip()
|
||||||
|
|
||||||
if productId == 'new' or productId == '':
|
if productId == 'new' or productId == '':
|
||||||
productId = None
|
productId = None
|
||||||
@@ -249,8 +249,8 @@ def editItem(listId, entryId):
|
|||||||
try:
|
try:
|
||||||
entry = list_entry_by_id(db.session, listId, entryId)
|
entry = list_entry_by_id(db.session, listId, entryId)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
quantity = request.form.get('quantity', None)
|
quantity = request.form.get('quantity', '').strip()
|
||||||
notes = request.form.get('notes', None)
|
notes = request.form.get('notes', '').strip()
|
||||||
|
|
||||||
list_editItem(
|
list_editItem(
|
||||||
db.session,
|
db.session,
|
||||||
|
|||||||
147
sigl/views/products.py
Normal file
147
sigl/views/products.py
Normal file
@@ -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/<int:id>', 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/<int:id>/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/<int:id>/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/<int:id>/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))
|
||||||
Reference in New Issue
Block a user