Compare commits

..

7 Commits

Author SHA1 Message Date
Notisset
836186f58b Application bound address automatically replaced in dockerfile. 2017-09-27 00:38:18 +02:00
Notisset
abb76836a1 Added Docker + docker-compose configuration 2017-09-26 00:52:06 +02:00
65b0a25429 Add declarative JSON requests. 2017-09-25 21:46:23 +02:00
14cc8ec7c4 Refactor token validity check. 2017-09-25 21:22:41 +02:00
6b159b6eda Switch from Flask to aiohttp.
This repository now provides the REST API only.

Implemented:
 * Authorization

Need improvement:
 * Product listing

Missing:
 * Selling
2017-09-25 19:38:45 +02:00
fff8a4879e Clean little code 2017-09-25 15:02:14 +02:00
f2d9a01682 Add floating point input 2017-09-25 15:02:14 +02:00
20 changed files with 350 additions and 331 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
.git
.idea

6
.gitignore vendored
View File

@ -97,3 +97,9 @@ ENV/
conf/ conf/
pos.db pos.db
pos.log* pos.log*
# vim swapfiles
*.swp
# IntelliJ
.idea

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM alpine:latest
MAINTAINER Notisset <notisset@autistici.org>
#Pillow requires the following dependencies to build: musl-dev zlib-dev jpeg-dev gcc python3-dev
RUN apk update && apk add musl-dev zlib-dev jpeg-dev gcc python3 python3-dev
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY . /var/www/site
RUN sed -i '/Address/s/= .*/= 0.0.0.0/' /var/www/site/conf/core.ini
EXPOSE 8080
ENTRYPOINT python3 /var/www/site/web.py

139
README.md
View File

@ -23,6 +23,7 @@ examples. The default path for configurations in `conf/` but you can also use
this order. this order.
For a testing environment you can just do: For a testing environment you can just do:
``` ```
mkdir conf mkdir conf
cp docs/config_core/core_debug_sqlite.ini conf/core.ini cp docs/config_core/core_debug_sqlite.ini conf/core.ini
@ -42,10 +43,10 @@ cp docs/config_logging/logging_production.yaml conf/logging.yaml
and then edit the conf/core.ini file to adjust the database params. Don't forget and then edit the conf/core.ini file to adjust the database params. Don't forget
to change the SECRET_KEY value (`openssl rand -hex 32` will help you). to change the SECRET_KEY value (`openssl rand -hex 32` will help you).
If you want to change the log file path open your `conf/logging.yaml` and If you want to change the log file path open your `conf/logging.yaml` and
change the `filename` field of the `file` entry inside the `handlers` category. change the `filename` field of the `file` entry inside the `handlers` category.
## Building the database ### Building the database
You also need to add some entries to the database. You also need to add some entries to the database.
@ -55,47 +56,137 @@ First of all add a new user. Get inside the virtualenv and then just do:
python3 cli.py user add username password python3 cli.py user add username password
``` ```
Add some categories with:
```
python3 cli.py category add "Birra"
python3 cli.py category add "Super"
python3 cli.py category add "Altro"
```
Add some products with: Add some products with:
``` ```
python3 cli.py product add "Birra media" 3 python3 cli.py product add -c 1 "Birra media" 3
python3 cli.py product add "Birra grande" 4 python3 cli.py product add -c 1 "Birra grande" 4
python3 cli.py product add "Cocktail" 5 python3 cli.py product add -c 2 "Vino" 4
python3 cli.py product add "Vino" 4 python3 cli.py product add -c 2 "Cocktail" 5
python3 cli.py product add "Amaro" 2 python3 cli.py product add -c 2 "Amaro" 2
python3 cli.py product add "Acqua" 0.5 python3 cli.py product add -c 3 "Acqua" 0.5
``` ```
And finally add and event you can play with: And finally add and event you can play with:
``` ```
python3 cli.py event add "My party" --start "2017-03-19 22:00" --end "2017-03-22 07:00" python3 cli.py event add "My party" "2017-03-19 22:00" "2017-03-22 07:00"
``` ```
## Running ## Running
You can run this software with from the virtualenv with: You can run this software within the virtualenv with:
``` ```
python3 web.py python3 web.py
``` ```
If you want to use a read httpd you can setup uwsgi as follows: ## REST API
``` ### Authentication
[uwsgi]
socket = 127.0.0.1:9000
chdir = /var/www/macao-pos * **URL**: `/api/token`
wsgi-file = web.py * **Method**: `POST`
virtualenv = env * **Success response**:
* **Code**: 200
* **Content**:
```
{
"token": "3ea90c63-4b92-465e-bee8-018a4c569252",
"created_at": "2017-09-25T18:50:38.620881",
"expires_at": "2017-09-25T18:50:38.620881"
}
```
* **Error response**:
* ***Malformed request***
* **Code**: 400
* **Content**:
```
{
"err": "malformed_request",
"msg": "Missing username and/or password keys."
}
```
* ***Invalid credentials***
* **Code**: 400
* **Content**:
```
{
"err": "invalid_credentials"
}
```
* **Sample call**:
```
curl -X POST \
-H "Accept: application/json" \
-d '{"username": "gino", "password": "paoli"}' \
"http://127.0.0.1:8080/api/token"
```
master ### Logout
workers = 1
max-requests = 200 * **URL**: `/api/token`
harakiri = 30 * **Method**: `DELETE`
die-on-term * **Success response**:
``` * **Code**: 200
* **Content**:
```
{}
```
* **Error response**:
* ***Malformed request***
* **Code**: 400
* **Content**:
```
{
"err": "malformed_request",
"msg": "Missing Authorization header."
}
```
* ***Unauthorizred***
* **Code**: 401
* **Content**:
```
{
"err": "unauthorized",
"msg": "The token is not valid."
}
```
* ***Forbidden***
* **Code**: 403
* **Content**:
```
{
"err": "forbidden",
"msg": "The token has expired."
}
```
* **Sample call**:
```
curl -X DELETE \
-H "Authorization: 3ea90c63-4b92-465e-bee8-018a4c569252" \
"http://127.0.0.1:8080/api/token"
```
## Contributing ## Contributing
Fork → commit → pull request Before pushing any commit make sure flake8 doesn't complain running:
```
flake8 web.py cli.py pos/*.py
```
1. Clone this repository
2. Create a new branch
3. Make your patch. Split it in different commits if it's huge
4. Fork this repo
5. Push your branch to your fork
6. Issue a pull request

4
cli.py
View File

@ -4,14 +4,14 @@ from tabulate import tabulate
from datetime import datetime from datetime import datetime
from pos.config import Config from pos.config import Config
from pos.logging import init_logging, get_logger from pos.logging import setup_logging, get_logger
from pos.database import Database, User, Event, ProductCategory, Product,\ from pos.database import Database, User, Event, ProductCategory, Product,\
Transaction Transaction
config = Config() config = Config()
conf_db = config.core['DATABASE'] conf_db = config.core['DATABASE']
init_logging(config.logging) setup_logging(config.logging)
log = get_logger('cli') log = get_logger('cli')
db = Database(**conf_db) db = Database(**conf_db)

10
docker-compose.yml Normal file
View File

@ -0,0 +1,10 @@
version: '2'
services:
app:
build: .
restart: always
ports:
- "8080:8080"
volumes:
- .:/var/www/site
working_dir: /var/www/site

View File

@ -1,13 +1,11 @@
[GENERAL] [GENERAL]
Debug = True Address = 127.0.0.1
Port = 8080
[DATABASE] [DATABASE]
Engine = sqlite Engine = sqlite
Path = pos.db Path = pos.db
[FLASK]
SECRET_KEY = CHANGE_ME_NOW!
[PRINTER] [PRINTER]
Host = 192.168.1.100 Host = 192.168.1.100
Post = 9100 Post = 9100

View File

@ -1,5 +1,6 @@
[GENERAL] [GENERAL]
Debug = False Address = 127.0.0.1
Port = 8080
[DATABASE] [DATABASE]
Engine = mysql+pymysql Engine = mysql+pymysql
@ -9,9 +10,6 @@ Database = pos
User = pos User = pos
Password = secret Password = secret
[FLASK]
SECRET_KEY = CHANGE_ME_NOW!
[PRINTER] [PRINTER]
Host = 192.168.1.100 Host = 192.168.1.100
Post = 9100 Post = 9100

View File

@ -15,9 +15,12 @@ handlers:
filename: pos.log filename: pos.log
loggers: loggers:
pos:
level: DEBUG
handlers: [console,file]
sqlalchemy: sqlalchemy:
level: INFO level: INFO
handlers: [console,file] handlers: [console,file]
aiohttp:
level: INFO
handlers: [console, file]
pos:
level: DEBUG
handlers: [console,file]

View File

@ -17,9 +17,12 @@ handlers:
backupCount: 3 backupCount: 3
loggers: loggers:
sqlalchemy:
level: ERROR
handlers: [console,file]
aiohttp:
level: ERROR
handlers: [console, file]
pos: pos:
level: WARNING level: WARNING
handlers: [console,file] handlers: [console,file]
sqlalchemy:
level: CRITICAL
handlers: [console,file]

View File

@ -3,8 +3,6 @@ from configparser import ConfigParser
import yaml import yaml
APP_NAME = 'pos'
CONFIG_PATHS = ['conf/', '~/.config/pos', '/usr/local/etc/pos', '/etc/pos'] CONFIG_PATHS = ['conf/', '~/.config/pos', '/usr/local/etc/pos', '/etc/pos']
CONFIG_FILES = { CONFIG_FILES = {
'core': 'core.ini', 'core': 'core.ini',
@ -14,10 +12,8 @@ CONFIG_FILES = {
def get_default_path(): def get_default_path():
for p in CONFIG_PATHS: for p in CONFIG_PATHS:
if all([ if all([os.path.isfile(os.path.join(p, f))
os.path.isfile(os.path.join(p, f)) for f in CONFIG_FILES.values()]):
for f in CONFIG_FILES.values()
]):
return p return p
else: else:
raise Exception('Unable to find a configuration folder.') raise Exception('Unable to find a configuration folder.')

View File

@ -1,18 +1,20 @@
from datetime import datetime from datetime import datetime, timedelta
from uuid import uuid4
from contextlib import contextmanager from contextlib import contextmanager
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy.ext.declarative import sqlalchemy.ext.declarative
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import Table, Column, ForeignKey from sqlalchemy import Column, ForeignKey
from sqlalchemy import Integer, String, Boolean, DateTime from sqlalchemy import Integer, String, Boolean, DateTime
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy_utils import force_auto_coercion from sqlalchemy_utils import force_auto_coercion
from sqlalchemy_utils.types.password import PasswordType from sqlalchemy_utils.types.password import PasswordType
from pos.logging import get_logger from pos.logging import get_logger
log = get_logger('database')
# The database URL must follow RFC 1738 in the form # The database URL must follow RFC 1738 in the form
# dialect+driver://username:password@host:port/database # dialect+driver://username:password@host:port/database
@ -23,11 +25,7 @@ ENGINE_SQLITE_MEMORY = "sqlite://"
PASSWORD_SCHEMES = ['pbkdf2_sha512'] PASSWORD_SCHEMES = ['pbkdf2_sha512']
Base = sqlalchemy.ext.declarative.declarative_base() Base = sqlalchemy.ext.declarative.declarative_base()
log = get_logger('database')
force_auto_coercion()
class Database: class Database:
@ -74,14 +72,15 @@ class Database:
Base.metadata.create_all(self.engine) Base.metadata.create_all(self.engine)
force_auto_coercion()
@contextmanager @contextmanager
def get_session(self): def get_session(self):
session = self.Session() session = self.Session()
try: try:
yield session yield session
except SQLAlchemyError as e: except SQLAlchemyError as e:
log.critical("Error performing transaction:") log.critical("Error performing transaction: {}".format(e))
log.critical(e)
session.rollback() session.rollback()
else: else:
session.commit() session.commit()
@ -95,12 +94,8 @@ class User(Base):
username = Column(String, nullable=False, unique=True) username = Column(String, nullable=False, unique=True)
password = Column(PasswordType(schemes=PASSWORD_SCHEMES), nullable=False) password = Column(PasswordType(schemes=PASSWORD_SCHEMES), nullable=False)
is_active = Column(Boolean, nullable=False, server_default='1') is_active = Column(Boolean, nullable=False, server_default='1')
is_authenticated = Column(Boolean, nullable=False, server_default='0')
created_at = Column(DateTime, nullable=False, default=datetime.now) created_at = Column(DateTime, nullable=False, default=datetime.now)
def get_id(self):
return u'{}'.format(self.uid)
class Event(Base): class Event(Base):
__tablename__ = 'events' __tablename__ = 'events'
@ -137,22 +132,14 @@ class Product(Base):
category = relationship('ProductCategory', lazy='joined') category = relationship('ProductCategory', lazy='joined')
order_entry_association = Table(
'order_entry_associations', Base.metadata,
Column('transaction_uid', Integer, ForeignKey('transactions.uid')),
Column('order_uid', Integer, ForeignKey('orders.uid'))
)
class Transaction(Base): class Transaction(Base):
__tablename__ = 'transactions' __tablename__ = 'transactions'
uid = Column(Integer, primary_key=True) uid = Column(Integer, primary_key=True)
event_uid = Column(Integer, ForeignKey('events.uid'), nullable=False) event_uid = Column(Integer, ForeignKey('events.uid'), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.now) created_at = Column(DateTime, nullable=False, default=datetime.now)
event = relationship('Event') event = relationship('Event', lazy='joined')
orders = relationship('Order', lazy='joined', orders = relationship('Order', lazy='joined')
secondary=order_entry_association)
class Order(Base): class Order(Base):
@ -160,5 +147,28 @@ class Order(Base):
uid = Column(Integer, primary_key=True) uid = Column(Integer, primary_key=True)
product_uid = Column(Integer, ForeignKey('products.uid'), nullable=False) product_uid = Column(Integer, ForeignKey('products.uid'), nullable=False)
quantity = Column(Integer, nullable=False) quantity = Column(Integer, nullable=False)
transaction_uid = Column(Integer, ForeignKey('transactions.uid'),
nullable=False)
product = relationship('Product', lazy='joined') product = relationship('Product', lazy='joined')
transaction = relationship('Transaction', lazy='joined')
class AccessToken(Base):
__tablename__ = 'access_tokens'
uid = Column(Integer, primary_key=True)
user_uid = Column(Integer, ForeignKey('users.uid'), nullable=False)
token = Column(String(36), nullable=False, default=str(uuid4()))
is_active = Column(Boolean, nullable=False, server_default='1')
created_at = Column(DateTime, nullable=False, default=datetime.now)
expires_at = Column(DateTime, nullable=False,
default=(datetime.now() + timedelta(days=2)))
user = relationship('User', lazy='joined')
def is_valid(self):
return all([
self.is_active,
self.created_at < datetime.now(),
self.expires_at > datetime.now()
])

View File

@ -1,13 +1,11 @@
import logging import logging
import logging.config import logging.config
from pos.config import APP_NAME
root = logging.getLogger('pos')
root = logging.getLogger(APP_NAME) def setup_logging(config):
def init_logging(config):
logging.config.dictConfig(config) logging.config.dictConfig(config)

116
pos/rest.py Normal file
View File

@ -0,0 +1,116 @@
from functools import wraps
from aiohttp.web import json_response
from pos.database import User, ProductCategory, AccessToken
def needs(*needed):
def decorator(func):
@wraps(func)
async def wrapper(request):
request_json = await request.json()
if not all(k in request_json.keys() for k in needed) or
return json_response({
'err': 'malformed_request',
'msg': 'Missing one or more keys: {}.'.format(
", ".join(needed))
}, status=400)
else:
return func(request)
return wrapper
return decorator
def auth_required(func):
@wraps(func)
async def wrapper(request):
db = request.app['db']
headers = request.headers
if 'Authorization' not in headers.keys():
return json_response({'err': 'malformed_request',
'msg': 'Missing Authorization header.'},
status=400)
else:
remote_token = headers['Authorization']
with db.get_session() as session:
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
if not token:
return json_response({'err': 'unauthorized',
'msg': 'The token is not valid.'},
status=401)
elif not token.is_valid():
return json_response({'err': 'forbidden',
'msg': 'The token has expired.'},
status=403)
else:
return await func(request)
return wrapper
@needs('username', 'password')
async def token_create(request):
db = request.app['db']
request_json = await request.json()
username = request_json['username']
password = request_json['password']
with db.get_session() as session:
user = session.query(User) \
.filter_by(username=username) \
.one_or_none()
if not user or user.password != password:
return json_response({'err': 'invalid_credentials'},
status=400)
with db.get_session() as session:
token = AccessToken(user=user)
session.add(token)
return json_response({
'token': token.token,
'created_at': token.created_at.isoformat(),
'expires_at': token.created_at.isoformat()
})
@auth_required
async def token_destroy(request):
db = request.app['db']
remote_token = request.headers['Authorization']
with db.get_session() as session:
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
token.is_active = False
session.add(token)
return json_response({}, status=200)
@auth_required
async def product_list(request):
db = request.app['db']
with db.get_session() as session:
categories = session.query(ProductCategory).all()
return json_response({
'categories': [{
'uid': c.uid,
'name': c.name,
'products': [{
'uid': p.uid,
'name': p.name,
'price': p.price
} for p in c.products]
} for c in categories]
})

8
pos/routes.py Normal file
View File

@ -0,0 +1,8 @@
from pos.rest import product_list
from pos.rest import token_create, token_destroy
def setup_routes(app):
app.router.add_route('POST', '/api/token', token_create)
app.router.add_route('DELETE', '/api/token', token_destroy)
app.router.add_route('GET', '/api/products', product_list)

View File

@ -1,11 +1,9 @@
aiohttp
click>=6.0 click>=6.0
SQLAlchemy>=1.1.0 SQLAlchemy>=1.2.0b1
sqlalchemy_utils>=0.32.00 sqlalchemy_utils>=0.32.00
pymysql pymysql
babel
passlib passlib
tabulate tabulate
PyYAML PyYAML
flask>=0.12.0
flask_login>=0.4.0
python-escpos python-escpos

View File

@ -1,21 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="{{ url_for('static', filename='style/reset.css') }}">
<link rel="stylesheet"
href="{{ url_for('static', filename='style/screen.css') }}">
<title>{{ title }}</title>
</head>
<body>
<main>{% block main %}{% endblock %}</main>
{% with alerts = get_flashed_messages(with_categories=True) %}
{% if alerts %}{% for category, message in alerts %}
<div class="alert {{ category }}">{{ message }}</div>
{% endfor %}{% endif %}
{% endwith %}
</body>
</html>

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% block main %}
<h1>{{ title }}</h1>
<form id="login" action="/login" method="post">
<label for="username">Username</label>
<input id="username" name="username" type="text" required autofocus>
<label for="password">Password</label>
<input id="password" name="password" type="password" required>
<button type="submit">Login</button>
</form>
{% endblock %}

View File

@ -1,36 +0,0 @@
{% extends 'base.html' %}
{% block main %}
<div id="products">
{% for product in products %}
<button type="button"
onclick="addProduct({{ product.uid }})">
<span class="name">{{ product.name }}</span>
<span class="price">
€ {{ '{0:.02f}'.format(product.price / 100.0) }}
</span>
</button>
{% endfor %}
</div>
<div id="summary">
<div id="basket"></div>
<form id="sell" action="/sell" method="POST">
{% for product in products %}
<input type="hidden"
id="prod_{{ product.uid }}"
name="{{ product.uid }}"
data-name="{{ product.name }}"
data-price="{{ '{0:.02f}'.format(product.price / 100.0) }}"
value="0">
{% endfor %}
<p>Total: € <span id="total">0.00</span></p>
<button type="submit">Print</input>
</form>
</div>
<script type="text/javascript" src="{{ url_for('static', filename='js/sell.js') }}">
<script type="text/javascript">
{%- for product in products %}
document.getElementById('prod_{{ product.uid }}').value = 0
{%- endfor %}
</script>
{% endblock %}

206
web.py Normal file → Executable file
View File

@ -1,197 +1,33 @@
from datetime import datetime #!/usr/bin/env python3
from time import sleep import asyncio
import escpos.printer from aiohttp import web
from flask import Flask, redirect, request, render_template, flash
from flask_login import LoginManager, login_user, logout_user, login_required
from pos.config import Config from pos.config import Config
from pos.logging import init_logging, get_logger from pos.logging import setup_logging, get_logger
from pos.database import Database, User, Product, Event, Transaction, Order from pos.database import Database
from pos.routes import setup_routes
config = Config()
debug = config.core['GENERAL'].getboolean('Debug', False)
conf_db = config.core['DATABASE']
conf_printer = config.core['PRINTER']
conf_flask = config.core['FLASK']
init_logging(config.logging)
log = get_logger('web') log = get_logger('web')
printer = escpos.printer.Network(
conf_printer['Host'],
port=conf_printer.getint('Port'),
timeout=15)
def setup_app(loop, config):
app = web.Application(loop=loop)
db = Database(**conf_db) app['config'] = config
app['db'] = Database(**config.core['DATABASE'])
app = Flask(__name__) setup_routes(app)
app.config.update(
debug=debug,
SECRET_KEY=conf_flask['SECRET_KEY']
)
login_manager = LoginManager() return app
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'error'
@login_manager.user_loader
def load_user(uid):
user = None
with db.get_session() as session:
user = session.query(User).get(uid)
return user
@app.route('/')
def home():
return redirect('/sell')
@app.route('/login', methods=['GET'])
def login_page():
return render_template('login.html', title='Login')
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
if not all([username, password]):
flash("Fill all fields.", 'error')
return login_page(), 400
with db.get_session() as session:
user = session.query(User)\
.filter(User.username == username)\
.filter(User.is_active == 1)\
.one_or_none()
if user is None:
flash("User not found.", 'error')
return login_page(), 400
if user.password == password:
login_user(user)
session.add(user)
user.is_authenticated = True
flash("Succesfully logged in.", 'success')
return redirect('/')
else:
flash("Wrong password.", 'error')
return login_page(), 400
@app.route('/logout')
def logout():
logout_user()
return redirect('/')
def get_products(session):
return session.query(Product)\
.filter(Product.is_active == 1)\
.order_by(Product.sort.asc())\
.all()
def get_event(session):
return session.query(Event)\
.filter(Event.starts_at < datetime.now())\
.filter((Event.ends_at.is_(None))
| (Event.ends_at > datetime.now()))\
.one_or_none()
@app.route('/sell', methods=['GET'])
@login_required
def sell_page():
with db.get_session() as session:
products = get_products(session)
event = get_event(session)
return render_template('sell.html',
title='Sell', event=event, products=products)
def print_orders(transaction, cat, orders):
printer.open()
printer.set(align='CENTER')
printer.image('static/img/macao-logo-printer.png', impl='bitImageColumn')
printer.set(align='CENTER', text_type='B')
printer.text(transaction.event.name.upper())
printer.text("\n\n")
for o in orders:
printer.set(align='LEFT', width=2, height=2)
printer.text("{} x {}".format(o.quantity, o.product.name.upper()))
printer.text("\n")
printer.text("\n")
printer.set(align='RIGHT')
printer.text("{} #{}-{}-{}"
.format(datetime.strftime(transaction.created_at,
"%Y-%m-%d %H:%M"),
transaction.event.uid, transaction.uid, cat))
printer.cut()
printer.close()
sleep(0.7)
def print_transaction(transaction):
categorized_orders = {}
for o in transaction.orders:
uid = o.product.category_uid
if not categorized_orders.get(uid, False):
categorized_orders[uid] = []
categorized_orders[uid].append(o)
for cat, orders in categorized_orders.items():
print_orders(transaction, cat, orders)
@app.route('/sell', methods=['POST'])
@login_required
def sell():
with db.get_session() as session:
event = get_event(session)
if event is None:
flash("Can't perform order without an ongoing event!", 'error')
return sell_page()
quantities = request.form.values()
if not any(int(qty) > 0 for qty in quantities):
flash("Can't perform empty order!", 'error')
return sell_page()
with db.get_session() as session:
items = request.form.items()
orders = [
Order(product_uid=uid, quantity=qty)
for uid, qty in items
if int(qty) > 0
]
transaction = Transaction(event_uid=event.uid, orders=orders)
session.add(transaction)
session.flush()
print_transaction(transaction)
flash("Success!", 'success')
return sell_page()
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=debug) config = Config()
setup_logging(config.logging)
loop = asyncio.get_event_loop()
app = setup_app(loop, config)
web.run_app(app,
host=config.core.get('GENERAL', 'Address'),
port=config.core.getint('GENERAL', 'Port'))