Add Frontend
This commit is contained in:
@@ -9,4 +9,4 @@ from .socketio import socketio
|
||||
|
||||
app = create_app()
|
||||
|
||||
socketio.run(app)
|
||||
socketio.run(app, host='0.0.0.0')
|
||||
|
||||
84
sigl/domain/service.py
Normal file
84
sigl/domain/service.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Sigl Domain Services.
|
||||
|
||||
Simple Grocery List (Sigl) | sigl.app
|
||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from sigl.exc import NotFoundError
|
||||
from .models import ListEntry, ShoppingList
|
||||
|
||||
|
||||
def lists_all(session: Session) -> List[ShoppingList]:
|
||||
"""Return all Shopping Lists."""
|
||||
return session.query(ShoppingList).all()
|
||||
|
||||
|
||||
def list_by_id(session: Session, id: int) -> Optional[ShoppingList]:
|
||||
"""Load a specific Shopping List."""
|
||||
return session.query(ShoppingList).filter(ShoppingList.id == id).one_or_none()
|
||||
|
||||
|
||||
def list_create(session: Session, name: str, *, notes=None) -> ShoppingList:
|
||||
"""Create a new Shopping List."""
|
||||
sList = ShoppingList(name=name, notes=notes)
|
||||
session.add(sList)
|
||||
session.commit()
|
||||
|
||||
return sList
|
||||
|
||||
|
||||
def list_delete(session: Session, id: int):
|
||||
"""Delete a Shopping List."""
|
||||
sList = list_by_id(session, id)
|
||||
if not sList:
|
||||
raise NotFoundError(f'List {id} does not exist')
|
||||
|
||||
session.delete(sList)
|
||||
session.commit()
|
||||
|
||||
|
||||
def list_stores(session: Session, id: 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.
|
||||
"""
|
||||
sList = list_by_id(session, id)
|
||||
if not sList:
|
||||
raise NotFoundError(f'List {id} does not exist')
|
||||
|
||||
stores = set()
|
||||
for e in sList.entries:
|
||||
for loc in (e.product.locations or []):
|
||||
stores.add(loc.store)
|
||||
|
||||
if '' in stores:
|
||||
stores.remove('')
|
||||
if None in stores:
|
||||
stores.remove(None)
|
||||
|
||||
return list(stores)
|
||||
|
||||
|
||||
def list_update(
|
||||
session: Session,
|
||||
id: int,
|
||||
name: Union[str,None],
|
||||
notes: Union[str,None],
|
||||
) -> ShoppingList:
|
||||
"""Update the Name and/or Notes of a Shopping List."""
|
||||
sList = list_by_id(session, id)
|
||||
if not sList:
|
||||
raise NotFoundError(f'List {id} does not exist')
|
||||
|
||||
sList.name = name
|
||||
sList.notes = notes
|
||||
|
||||
session.add(sList)
|
||||
session.commit()
|
||||
|
||||
return sList
|
||||
10
sigl/exc.py
10
sigl/exc.py
@@ -33,3 +33,13 @@ class ConfigError(Error):
|
||||
"""Class Constructor."""
|
||||
super().__init__(*args)
|
||||
self.config_key = config_key
|
||||
|
||||
|
||||
class DomainError(Error):
|
||||
"""Exception raised for domain logic errors."""
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(Error):
|
||||
"""Exception raised when an object cannot be found."""
|
||||
pass
|
||||
|
||||
@@ -61,7 +61,7 @@ def create_app(app_config=None, app_name=None):
|
||||
"""
|
||||
app = Flask(
|
||||
'sigl',
|
||||
template_folder='templates'
|
||||
static_folder='../static',
|
||||
)
|
||||
|
||||
# Load Application Name and Version from pyproject.toml
|
||||
@@ -134,8 +134,8 @@ def create_app(app_config=None, app_name=None):
|
||||
init_socketio(app)
|
||||
|
||||
# Initialize Frontend
|
||||
from .frontend import init_frontend
|
||||
init_frontend(app)
|
||||
from .views import init_views
|
||||
init_views(app)
|
||||
|
||||
# Startup Complete
|
||||
app.logger.info('{} startup complete'.format(app.config['APP_NAME']))
|
||||
|
||||
116
sigl/templates/base.html.j2
Normal file
116
sigl/templates/base.html.j2
Normal file
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
{% block head_scripts %}{% endblock %}
|
||||
{% if config['ENV'] == 'production' %}
|
||||
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='css/sigl.css') }}"> -->
|
||||
{% else %}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
{% endif %}
|
||||
{% block head_styles %}{% endblock %}
|
||||
</head>
|
||||
<body class="h-full bg-gray-200">
|
||||
<header>
|
||||
<nav class="bg-gray-800 border-b border-gray-600">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-12">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-8 w-8" src="https://tailwindui.com/img/logos/workflow-mark-indigo-500.svg" alt="Workflow">
|
||||
</div>
|
||||
<div>
|
||||
<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="#" class="text-gray-300 hover:bg-gray-700 hover:text-white px-2 py-1 rounded-md text-sm font-medium">Products</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="-mr-2 flex">
|
||||
<!-- Mobile menu button -->
|
||||
<button type="button" class="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" aria-expanded="false">
|
||||
<span class="sr-only">App Settings</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="max-w-3xl mx-auto px-2 bg-white md:border-l md:border-r border-b border-gray-300">
|
||||
{% block header %}{% endblock %}
|
||||
</section>
|
||||
</header>
|
||||
<main class="max-w-3xl mx-auto bg-white md:border-l md:border-r border-b md:rounded-b border-gray-300">
|
||||
{% block main %}{% endblock %}
|
||||
</main>
|
||||
{% block body_scripts %}{% endblock %}
|
||||
<div
|
||||
x-data="noticesHandler()"
|
||||
class="fixed inset-0 flex flex-col items-end justify-start h-screen w-screen"
|
||||
style="pointer-events:none"
|
||||
@notice.document="add($event.detail)"
|
||||
>
|
||||
<template x-for="notice of notices" :key="notice.id">
|
||||
<div
|
||||
x-show="visible.includes(notice)"
|
||||
x-transition:enter="transition ease-in duration-200"
|
||||
x-transition:enter-start="transform opacity-0 translate-y-2"
|
||||
x-transition:enter-end="transform opacity-100"
|
||||
x-transition:leave="transition ease-out duration-500"
|
||||
x-transition:leave-start="transform translate-x-0 opacity-100"
|
||||
x-transition:leave-end="transform translate-x-full opacity-0"
|
||||
@click="remove(notice.id)"
|
||||
class="rounded max-w-[75%] mt-4 mr-6 px-1 py-1 flex items-center justify-center text-white shadow-lg font-bold text-sm cursor-pointer"
|
||||
:class="{
|
||||
'bg-green-500': notice.type === 'success',
|
||||
'bg-blue-500': notice.type === 'info',
|
||||
'bg-orange-500': notice.type === 'warning',
|
||||
'bg-red-500': notice.type === 'error',
|
||||
}"
|
||||
style="pointer-events:all"
|
||||
x-text="notice.text"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<script language="javascript">
|
||||
document.addEventListener('alpine:init', function () {
|
||||
Alpine.data('noticesHandler', () => ({
|
||||
notices: [],
|
||||
visible: [],
|
||||
add(notice) {
|
||||
notice.id = Date.now()
|
||||
this.notices.push(notice)
|
||||
this.fire(notice.id)
|
||||
},
|
||||
fire(id) {
|
||||
this.visible.push(this.notices.find(notice => notice.id == id))
|
||||
const timeShown = 2000 * this.visible.length
|
||||
setTimeout(() => {
|
||||
this.remove(id)
|
||||
}, timeShown)
|
||||
},
|
||||
remove(id) {
|
||||
const notice = this.visible.find(notice => notice.id == id)
|
||||
const index = this.visible.indexOf(notice)
|
||||
this.visible.splice(index, 1)
|
||||
},
|
||||
}));
|
||||
});
|
||||
document.addEventListener('alpine:initialized', function () {
|
||||
{% for category, message in get_flashed_messages(with_categories=True) %}
|
||||
document.dispatchEvent(new CustomEvent('notice', { detail: { type: {{ category|tojson }}, text: {{ message|tojson }} } }));
|
||||
{% endfor %}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
29
sigl/templates/lists/create.html.j2
Normal file
29
sigl/templates/lists/create.html.j2
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% block title %}Create New List | Sigl{% endblock %}
|
||||
{% block header %}
|
||||
<div class="flex justify-between items-center py-1">
|
||||
<div class="text-sm font-bold text-gray-800">Create New List</div>
|
||||
<div class="flex justify-start items-start">
|
||||
<a href="{{ url_for('lists.home') }}" class="px-2 py-1 text-xs text-white bg-gray-600 hover:bg-gray-700 border rounded flex justify-between items-center">
|
||||
Cancel
|
||||
</a>
|
||||
</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">Shopping List 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="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 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>
|
||||
</form>
|
||||
{% endblock %}
|
||||
88
sigl/templates/lists/detail.html.j2
Normal file
88
sigl/templates/lists/detail.html.j2
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% block title %}{{ list.name }} | Sigl{% endblock %}
|
||||
{% block header %}
|
||||
<div class="flex justify-between items-center py-2">
|
||||
<div class="text-lg font-bold text-gray-800">{{ list.name }}</div>
|
||||
<div class="flex justify-start items-start pr-1">
|
||||
<a href="#" class="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>
|
||||
Add Item
|
||||
</a>
|
||||
<a href="#" class="ml-4 py-1 text-sm text-gray-800 hover:text-blue-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</a>
|
||||
<button id="delete-list-btn" class="ml-3 py-1 text-sm text-gray-800 hover:text-red-700" onclick="deleteList()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<div class="fixed bottom-0 flex max-w-3xl w-full mx-auto bg-white md:border-l md:border-r border-b md:rounded-b border-gray-300">
|
||||
<div class="w-full flex items-center justify-between px-2 py-2 text-sm">
|
||||
<label for="sorting" class="font-bold">Sort By: </label>
|
||||
<select name="sorting" id="sorting" class="flex-grow ml-2 p-1 bg-white border rounded" onchange="changeSorting(event)">
|
||||
<option value="none"{% if sortBy == 'none' %} selected="selected"{% endif %}>None</option>
|
||||
<option value="category"{% if sortBy == 'category' %} selected="selected"{% endif %}>Category</option>
|
||||
{% for store in stores %}
|
||||
<option value="store:{{ store }}"{% if sortBy == 'store' and sortStore|lower == store|lower %} selected="selected"{% endif %}>Location ({{ store }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if list.entries|length == 0 %}
|
||||
<div class="py-2 bg-white">
|
||||
<div class="ml-4 text-sm">No Items</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if sortBy == 'none' %}
|
||||
<ul>
|
||||
{% for e in list.entries %}
|
||||
<li>{{ e.product.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif sortBy == 'category' %}
|
||||
{% for hdr, entries in groups.items() %}
|
||||
<div class="text-sm text-gray-600 font-bold py-1 px-2">{{ hdr }}</div>
|
||||
<ul>
|
||||
{% for e in entries %}
|
||||
<li>{{ e.entry.product.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% elif sortBy == 'store' %}
|
||||
{% for hdr, entries in groups.items() %}
|
||||
<div class="text-sm text-gray-600 font-bold py-1 px-2">Aisle {{ hdr }}</div>
|
||||
<ul>
|
||||
{% for e in entries %}
|
||||
<li>{{ e.entry.product.name }}{% if e.bin %} (Bin {{e.bin}}){% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<form action="{{ url_for('lists.delete', id=list.id) }}" method="post" id="delete-list-form"></form>
|
||||
<script language="javascript">
|
||||
function changeSorting(e) {
|
||||
const value = e.target.value;
|
||||
const [ sort, store ] = value.split(':');
|
||||
if (store) {
|
||||
window.location = `?sort=${encodeURIComponent(sort)}&store=${encodeURIComponent(store)}`;
|
||||
} else {
|
||||
window.location = `?sort=${encodeURIComponent(sort)}`;
|
||||
}
|
||||
}
|
||||
function deleteList() {
|
||||
const form = document.getElementById('delete-list-form');
|
||||
if (form && confirm('Are you sure you want to delete list "{{ list.name }}"? This cannot be undone.')) {
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
28
sigl/templates/lists/home.html.j2
Normal file
28
sigl/templates/lists/home.html.j2
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
{% block title %}Home | Sigl{% endblock %}
|
||||
{% block header %}
|
||||
<div class="flex justify-between items-center py-2">
|
||||
<div class="text-lg font-bold text-gray-800">All Lists</div>
|
||||
<a href="{{ url_for('lists.create') }}" class="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 List
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
{% if lists|length == 0 %}
|
||||
<div class="py-2 border-b border-gray-300">
|
||||
<div class="ml-4 text-sm">No shopping lists</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="w-full">
|
||||
{% for lst in lists %}
|
||||
<li class="block w-full py-2{% if not loop.last %} border-b border-gray-300{% endif %}">
|
||||
<a href="{{ url_for('lists.detail', id=lst.id) }}"><div class="px-4">{{ lst.name }} ({{ lst.entries|length }} Items)</div></a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
17
sigl/views/__init__.py
Normal file
17
sigl/views/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Sigl View Blueprints.
|
||||
|
||||
Simple Grocery List (Sigl) | sigl.app
|
||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||
"""
|
||||
|
||||
from .lists import bp as list_bp
|
||||
|
||||
__all__ = ('init_views', )
|
||||
|
||||
|
||||
def init_views(app):
|
||||
"""Register the View Blueprints with the Application."""
|
||||
app.register_blueprint(list_bp)
|
||||
|
||||
# Notify Initialization Complete
|
||||
app.logger.debug('Views Initialized')
|
||||
116
sigl/views/lists.py
Normal file
116
sigl/views/lists.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Sigl Shopping List View Blueprint.
|
||||
|
||||
Simple Grocery List (Sigl) | sigl.app
|
||||
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
|
||||
"""
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
abort, flash, redirect, render_template, request, url_for
|
||||
)
|
||||
|
||||
from sigl.exc import Error
|
||||
from sigl.database import db
|
||||
from sigl.domain.service import (
|
||||
lists_all, list_by_id, list_create, list_delete,
|
||||
list_stores,
|
||||
)
|
||||
|
||||
__all__ = ('bp', )
|
||||
|
||||
|
||||
#: Lists Blueprint
|
||||
bp = Blueprint('lists', __name__)
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
@bp.route('/lists')
|
||||
def home():
|
||||
"""Sigl Home Page / All Shopping Lists View."""
|
||||
lists = lists_all(db.session)
|
||||
return render_template('lists/home.html.j2', lists=lists)
|
||||
|
||||
|
||||
@bp.route('/lists/new', methods=('GET', 'POST'))
|
||||
def create():
|
||||
"""Create Shopping List View."""
|
||||
if request.method == 'POST':
|
||||
list_name = request.form['name']
|
||||
list_notes = request.form['notes']
|
||||
if not list_name:
|
||||
flash('Error: List Name is required')
|
||||
return render_template('lists/create.html.j2')
|
||||
|
||||
list = list_create(db.session, list_name, notes=list_notes)
|
||||
return redirect(url_for('lists.detail', id=list.id))
|
||||
|
||||
else:
|
||||
return render_template('lists/create.html.j2')
|
||||
|
||||
|
||||
@bp.route('/lists/<int:id>')
|
||||
def detail(id):
|
||||
"""Shopping List Detail View."""
|
||||
try:
|
||||
sList = list_by_id(db.session, id)
|
||||
sortBy = request.args.get('sort', 'none')
|
||||
sortStore = request.args.get('store', '')
|
||||
|
||||
if sortBy not in ('none', 'category', 'store'):
|
||||
flash(f'Invalid sorting mode {sortBy}', 'warning')
|
||||
sortBy = 'category'
|
||||
|
||||
groups = dict()
|
||||
for e in sList.entries:
|
||||
if sortBy == 'category':
|
||||
category = e.product.category or 'Uncategorized'
|
||||
if category not in groups:
|
||||
groups[category] = [{'entry': e}]
|
||||
else:
|
||||
groups[category].append({'entry': e})
|
||||
|
||||
elif sortBy == 'store':
|
||||
aisle = 'Unknown'
|
||||
bin = None
|
||||
locs = e.product.locations
|
||||
for l in locs:
|
||||
if l.store.lower() == sortStore.lower():
|
||||
aisle = l.aisle
|
||||
bin = l.bin
|
||||
|
||||
if aisle not in groups:
|
||||
groups[aisle] = [{'entry': e, 'bin': bin}]
|
||||
else:
|
||||
groups[aisle].append({'entry': e, 'bin': bin})
|
||||
|
||||
else:
|
||||
category = 'Unsorted'
|
||||
if category not in groups:
|
||||
groups[category] = [{'entry': e}]
|
||||
else:
|
||||
groups[category].append({'entry': e})
|
||||
|
||||
flash('An error occurred during the processing of this request', 'error')
|
||||
return render_template(
|
||||
'lists/detail.html.j2',
|
||||
list=list_by_id(db.session, id),
|
||||
sortBy=sortBy,
|
||||
sortStore=sortStore,
|
||||
groups=groups,
|
||||
stores=list_stores(db.session, id),
|
||||
)
|
||||
|
||||
except Error as e:
|
||||
flash(str(e), 'error')
|
||||
return redirect(url_for('lists.home'))
|
||||
|
||||
|
||||
@bp.route('/lists/<int:id>/delete', methods=('POST', ))
|
||||
def delete(id):
|
||||
"""Delete a Shopping List."""
|
||||
try:
|
||||
list_delete(db.session, id)
|
||||
except Error as e:
|
||||
flash(str(e), 'error')
|
||||
|
||||
return redirect(url_for('lists.home'))
|
||||
Reference in New Issue
Block a user