major: refactor, renaming, huge changes
Everything kinda changed. Huge refactoring. Website now has sidebar instead of top navigation. Database got huge changes. More forms!
This commit is contained in:
parent
f713564238
commit
b705f82ecb
@ -28,5 +28,6 @@ app.config.from_file("configs/config.yaml", safe_load)
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db, render_as_batch=True)
|
||||
login = LoginManager(app)
|
||||
login.login_view = 'web_login'
|
||||
|
||||
from app import views, models
|
||||
from app import routes, models
|
||||
13
app/forms.py
13
app/forms.py
@ -1,7 +1,7 @@
|
||||
from app.models import Brand, Category
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectMultipleField, DateField, IntegerField, SelectField, FloatField
|
||||
from wtforms.validators import DataRequired
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField('Username', validators=[DataRequired()])
|
||||
@ -15,8 +15,8 @@ class NewItemForm(FlaskForm):
|
||||
description = StringField("Description", validators=[DataRequired()])
|
||||
date = DateField("Insert Date", validators=[DataRequired()])
|
||||
price_change = FloatField("Price", validators=[DataRequired()])
|
||||
amount_change = IntegerField("Amount")
|
||||
category = SelectMultipleField("Categories", choices=[(c.id, c.name) for c in Category.query.order_by("name").all()], validators=[DataRequired()])
|
||||
amount_change = IntegerField("Amount", validators=[Optional()])
|
||||
category = SelectMultipleField("Categories", choices=[(c.id, c.name) for c in Category.query.order_by("name").all()], validators=[Optional()])
|
||||
brand = SelectField("Brand", choices=[(b.id, b.name) for b in Brand.query.order_by("name").all()], validators=[DataRequired()])
|
||||
submit = SubmitField("Submit")
|
||||
|
||||
@ -26,4 +26,11 @@ class NewCategoryForm(FlaskForm):
|
||||
|
||||
class NewBrandForm(FlaskForm):
|
||||
name = StringField("Name", validators=[DataRequired()])
|
||||
submit = SubmitField("Submit")
|
||||
|
||||
class NeueWGForm(FlaskForm):
|
||||
wg_name = StringField("Name", validators=[DataRequired()])
|
||||
submit = SubmitField("Submit")
|
||||
|
||||
class WGBeitretenForm(FlaskForm):
|
||||
submit = SubmitField("Submit")
|
||||
@ -1,7 +1,5 @@
|
||||
from app import db, login
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy.sql import text
|
||||
from sqlalchemy_utils.view import create_view
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
item_category = db.Table("item_category",
|
||||
@ -10,11 +8,13 @@ item_category = db.Table("item_category",
|
||||
)
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.String(10), primary_key=True)
|
||||
name = db.Column(db.String(64), nullable=False)
|
||||
id = db.Column(db.BigInteger, primary_key=True)
|
||||
email = db.Column(db.String(64), nullable=False, unique=True)
|
||||
username = db.Column(db.String(64), nullable=False, unique=True)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
|
||||
Bought = db.relationship("Bought", backref='User', lazy='dynamic')
|
||||
# Bought = db.relationship("Bought", backref='User', lazy='dynamic')
|
||||
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
@ -23,11 +23,24 @@ class User(UserMixin, db.Model):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User {self.id} ({self.name})>"
|
||||
return f"<User {self.id} ({self.username})>"
|
||||
|
||||
class Establishment(db.Model):
|
||||
id = db.Column(db.BigInteger, primary_key=True)
|
||||
name = db.Column(db.String(64), nullable=False)
|
||||
|
||||
LoginToken = db.relationship("LoginToken", backref='Establishment', lazy='dynamic')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Establishment {self.id} ({self.name})>"
|
||||
|
||||
class LoginToken(db.Model):
|
||||
user = db.Column(db.ForeignKey('user.id'), primary_key=True)
|
||||
token = db.Column(db.String(32), nullable=False)
|
||||
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True)
|
||||
token = db.Column(db.String(15), nullable=True, unique=True)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"LoginToken {self.token}"
|
||||
|
||||
class Brand(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -38,7 +51,7 @@ class Brand(db.Model):
|
||||
|
||||
class Category(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(32), nullable=False)
|
||||
name = db.Column(db.String(32), nullable=False, unique=True)
|
||||
|
||||
Item = db.relationship("Item", secondary=item_category, lazy="dynamic", back_populates="Category")
|
||||
|
||||
@ -60,12 +73,12 @@ class Item(db.Model):
|
||||
return f"<Item {self.id} ({self.name})>"
|
||||
|
||||
class Bought(db.Model):
|
||||
user = db.Column(db.ForeignKey('user.id'), primary_key=True)
|
||||
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True)
|
||||
item = db.Column(db.ForeignKey('item.id'), primary_key=True)
|
||||
date = db.Column(db.Date, primary_key=True)
|
||||
amount = db.Column(db.SmallInteger, nullable=False)
|
||||
# registered = db.Column(db.Boolean, nullable=False, default=False)
|
||||
# paid = db.Column(db.SmallInteger, nullable=False, default=0)
|
||||
registered = db.Column(db.Boolean, nullable=False, default=False)
|
||||
paid = db.Column(db.SmallInteger, nullable=False, default=0)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Bought Object>"
|
||||
@ -103,30 +116,6 @@ class ItemReceipt(db.Model):
|
||||
def __repr__(self) -> str:
|
||||
return f"<ItemReceipt {self.receipt}: {self.item}>"
|
||||
|
||||
def query_price_per_amount_view():
|
||||
p = db.aliased(PriceChange, name="p")
|
||||
a = db.aliased(AmountChange, name="a")
|
||||
date = db.func.greatest(p.date, a.date).label("date")
|
||||
price = (db.func.ceil(p.price.cast(db.Float)/db.func.coalesce(a.amount, 1))/100).label("price")
|
||||
select = db.select(p.item.label("item"), date, price)
|
||||
select = select.distinct(p.item, date)
|
||||
select = select.join(a, p.item==a.item, isouter=True)
|
||||
select = select.order_by(p.item, db.desc(db.func.greatest(p.date, a.date)))
|
||||
return select
|
||||
|
||||
price_per_amount = create_view("price_per_amount", query_price_per_amount_view(), db.metadata)
|
||||
|
||||
def query_bought_with_prices_view():
|
||||
b = db.aliased(Bought, name="b")
|
||||
ppa = price_per_amount.alias("ppa")
|
||||
select = db.select(b.user.label("user"), b.date.label("date"), b.item.label("item"), b.amount.label("amount"), ppa.c.price.label("price"))
|
||||
select = select.distinct(b.user, b.date, b.item)
|
||||
select = select.join(ppa, b.item==ppa.c.item)
|
||||
select = select.where(ppa.c.date<=b.date)
|
||||
select = select.order_by(db.desc(b.user), db.desc(b.date), db.desc(b.item), db.desc(ppa.c.date))
|
||||
return select
|
||||
|
||||
bought_with_prices = create_view("bought_with_prices", query_bought_with_prices_view(), db.metadata)
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
|
||||
116
app/routes.py
Normal file
116
app/routes.py
Normal file
@ -0,0 +1,116 @@
|
||||
from app import app, db, LOGGER
|
||||
from app.forms import NewItemForm, LoginForm
|
||||
from app.models import LoginToken, User, Item, Brand, Category, PriceChange, AmountChange
|
||||
from app.utils import view_utils, database_utils, routes_utils
|
||||
from datetime import date
|
||||
from flask import abort, flash, redirect, request, url_for
|
||||
from flask.json import jsonify
|
||||
from flask_login import current_user, login_required, login_user, logout_user
|
||||
from werkzeug.urls import url_parse
|
||||
|
||||
APPNAME = "scan2kasse"
|
||||
|
||||
render_template = routes_utils.render_custom_template
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return "<h1>Hello, World!</h>", 200
|
||||
|
||||
@app.route('/test')
|
||||
def test():
|
||||
if request.args:
|
||||
LOGGER.debug(request.args['testing'])
|
||||
form = NewItemForm()
|
||||
return render_template("test.html", form=form)
|
||||
|
||||
|
||||
@app.route(f'/{APPNAME}/token_authorization')
|
||||
def token_authorization():
|
||||
LOGGER.debug("Token Login")
|
||||
if not request.json or 'login' not in request.json:
|
||||
abort(400)
|
||||
if not LoginToken.query.filter_by(token=request.json['login']).first():
|
||||
abort(403)
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
@app.route(f'/{APPNAME}/token_insert', methods=['POST'])
|
||||
def insert():
|
||||
match request.json:
|
||||
case {'user': user, 'items': items, 'date': date}:
|
||||
failed = database_utils.insert_bought_items(user, items, date)
|
||||
case {'user': user, 'items': items}:
|
||||
failed = database_utils.insert_bought_items(user, items)
|
||||
case _:
|
||||
abort(400)
|
||||
if failed:
|
||||
return jsonify(failed), 400
|
||||
return jsonify({'inserted': True}), 201
|
||||
|
||||
|
||||
@app.route(f'/{APPNAME}/login', methods=['GET', 'POST'])
|
||||
def web_login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
flash('Invalid username or password')
|
||||
return redirect(url_for('web_login'))
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
next_page = request.args.get('next')
|
||||
if not next_page or url_parse(next_page).netloc != '':
|
||||
next_page = url_for('index')
|
||||
return redirect(next_page)
|
||||
return render_template('login.html', title='Sign In', form=form)
|
||||
|
||||
|
||||
@app.route(f'/{APPNAME}/newitem', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_item():
|
||||
if current_user.is_anonymous:
|
||||
abort(403)
|
||||
form=NewItemForm()
|
||||
if form.is_submitted():
|
||||
LOGGER.debug("submitted")
|
||||
if form.validate():
|
||||
LOGGER.debug("valid")
|
||||
LOGGER.debug(form.errors)
|
||||
if form.validate_on_submit():
|
||||
LOGGER.debug("valid form")
|
||||
brand = Brand.query.get(form.brand.data)
|
||||
new_item = Item(id = form.id.data, name = form.name.data, brand = brand.id, description = form.description.data)
|
||||
# if form.category.data:
|
||||
# category = Category.query.get(id = form.category.data)
|
||||
# new_item.Category = category
|
||||
new_item.PriceChange = [PriceChange(Item = new_item, date = date(2021, 12, 1), price = form.price_change.data)]
|
||||
if form.amount_change.data:
|
||||
new_item.AmountChange = [AmountChange(Item = new_item, date = date(2021, 12, 1), amount = form.amount_change.data)]
|
||||
db.session.add(new_item)
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'))
|
||||
return render_template('admin/new_item.html', form=form)
|
||||
|
||||
@app.route(f'/{APPNAME}/overview', methods=['GET'])
|
||||
def get_report_from_user():
|
||||
if 'month' in request.args:
|
||||
try:
|
||||
month = int(request.args['month'])
|
||||
except Exception as e:
|
||||
LOGGER.exception("")
|
||||
abort(400)
|
||||
else:
|
||||
if (month > 12 or month < 1):
|
||||
abort(400)
|
||||
LOGGER.info("Getting results.")
|
||||
results = database_utils.get_report(kwargs = request.args)
|
||||
LOGGER.debug(f"Results received: {results}")
|
||||
if results:
|
||||
result_list = view_utils.group_results(results)
|
||||
else:
|
||||
result_list = []
|
||||
if request.content_type == "application/json":
|
||||
return jsonify(result_list)
|
||||
else:
|
||||
return render_template("overview.html", results=result_list)
|
||||
89
app/static/sidebars.css
Normal file
89
app/static/sidebars.css
Normal file
@ -0,0 +1,89 @@
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
max-height: 100vh;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.b-example-divider {
|
||||
flex-shrink: 0;
|
||||
width: 1.5rem;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, .1);
|
||||
border: solid rgba(0, 0, 0, .15);
|
||||
border-width: 1px 0;
|
||||
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.bi {
|
||||
vertical-align: -.125em;
|
||||
pointer-events: none;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.dropdown-toggle { outline: 0; }
|
||||
|
||||
.nav-flush .nav-link {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: .25rem .5rem;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, .65);
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.btn-toggle:hover,
|
||||
.btn-toggle:focus {
|
||||
color: rgba(0, 0, 0, .85);
|
||||
background-color: #d2f4ea;
|
||||
}
|
||||
|
||||
.btn-toggle::before {
|
||||
width: 1.25em;
|
||||
line-height: 0;
|
||||
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
||||
transition: transform .35s ease;
|
||||
transform-origin: .5em 50%;
|
||||
}
|
||||
|
||||
.btn-toggle[aria-expanded="true"] {
|
||||
color: rgba(0, 0, 0, .85);
|
||||
}
|
||||
.btn-toggle[aria-expanded="true"]::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.btn-toggle-nav a {
|
||||
display: inline-flex;
|
||||
padding: .1875rem .5rem;
|
||||
margin-top: .125rem;
|
||||
margin-left: 1.25rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-toggle-nav a:hover,
|
||||
.btn-toggle-nav a:focus {
|
||||
background-color: #d2f4ea;
|
||||
}
|
||||
|
||||
.scrollarea {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fw-semibold { font-weight: 600; }
|
||||
.lh-tight { line-height: 1.25; }
|
||||
24
app/templates/admin/new_item.html
Normal file
24
app/templates/admin/new_item.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="row">{{form.id.label}}</div>
|
||||
<div class="row">{{form.id()}}</div>
|
||||
<div class="row">{{form.name.label}}</div>
|
||||
<div class="row">{{form.name()}}</div>
|
||||
<div class="row">{{form.description.label}}</div>
|
||||
<div class="row">{{form.description()}}</div>
|
||||
<div class="row">{{form.date.label}}</div>
|
||||
<div class="row">{{form.date()}}</div>
|
||||
<div class="row">{{form.price_change.label}}</div>
|
||||
<div class="row">{{form.price_change()}}</div>
|
||||
<div class="row">{{form.amount_change.label}}</div>
|
||||
<div class="row">{{form.amount_change()}}</div>
|
||||
<div class="row">{{form.category.label}}</div>
|
||||
<div class="row">{{form.category()}}</div>
|
||||
<div class="row">{{form.brand.label}}</div>
|
||||
<div class="row">{{form.brand()}}</div>
|
||||
<div class="row">{{form.submit()}}</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -4,40 +4,90 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href={{ url_for('static', filename="sidebars.css")}}>
|
||||
{% if title %}
|
||||
<title>{{ title }}</title>
|
||||
{% else %}
|
||||
<title>Scan2Kasse</title>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Navbar</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Home</a>
|
||||
<main>
|
||||
<div class="flex-shrink-0 p-3 bg-white" style="width: 280px;">
|
||||
<a href="/" class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom">
|
||||
<svg class="bi me-2" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
|
||||
<span class="fs-5 fw-semibold">Scan2Kasse</span>
|
||||
</a>
|
||||
<ul class="list-unstyled ps-0">
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true">
|
||||
Home
|
||||
</button>
|
||||
<div class="collapse show" id="home-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">Übersicht</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Updates</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Reports</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Overview
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<li><a class="dropdown-item" href="#">Monthly</a></li>
|
||||
<li><a class="dropdown-item" href="#">Yearly</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#">Full Overview</a></li>
|
||||
</ul>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false">
|
||||
Dashboard
|
||||
</button>
|
||||
<div class="collapse" id="dashboard-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">Overview</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Weekly</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Monthly</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Annually</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- <li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false">
|
||||
Orders
|
||||
</button>
|
||||
<div class="collapse" id="orders-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">New</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Processed</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Shipped</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Returned</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li> -->
|
||||
<li class="border-top my-3"></li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false">
|
||||
Account
|
||||
</button>
|
||||
<div class="collapse" id="account-collapse" style="">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">New...</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Profile</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Settings</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Sign out</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="container">
|
||||
<div class="row-md-3">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="row-md-3">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
app/templates/login.html
Normal file
24
app/templates/login.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Sign In</h1>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
{{ form.username.label }}<br>
|
||||
{{ form.username(size=32) }}
|
||||
{% for error in form.username.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ form.password.label }}<br>
|
||||
{{ form.password(size=32) }}
|
||||
{% for error in form.password.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -1,27 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% for user, dates in results.items() %}
|
||||
<div class="row">
|
||||
<h3>{{ user }}: {{ dates.sum }} €</h3>
|
||||
{% for date, items in dates.items() %}
|
||||
{% if date != "sum" %}
|
||||
<div class="row">
|
||||
<div class="col-sm-1"></div>
|
||||
<div class="col">
|
||||
<h4>{{ date }}</h4>
|
||||
{% for item, values in items.items() %}
|
||||
<div class="row">
|
||||
<div class="col-sm-1"></div>
|
||||
<div class="col">
|
||||
{{ values.0 }}x {{ item }} je {{ values.1 }}
|
||||
</div>
|
||||
{% for user in results %}
|
||||
<div class="card">
|
||||
<a class="btn btn-primary" data-bs-toggle="collapse" href="#b{{ user.id }}" role="button" aria-expanded="false" aria-controls="b{{ user.id }}">
|
||||
<div class="card-header">
|
||||
<h3>{{ user.username }}: {{ user.sum/100 }} €</h3>
|
||||
</div>
|
||||
</a>
|
||||
<div class="collapse" id="b{{ user.id }}">
|
||||
{% for item_infos in user.item_infos %}
|
||||
<div class="card-body">
|
||||
<div class="col-sm-1"></div>
|
||||
<div class="col">
|
||||
<h4>{{ item_infos.date }}</h4>
|
||||
{% for item in item_infos.item_list %}
|
||||
<div class="row">
|
||||
<div class="col-sm-1"></div>
|
||||
<div class="col">
|
||||
{{ item.amount }}x {{ item.name }} je {{ item.price/100 }} €
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@ -1,17 +1,19 @@
|
||||
from calendar import month
|
||||
from app import db, LOGGER
|
||||
from app.models import Bought, bought_with_prices
|
||||
from app.models import Bought, Item, LoginToken, User
|
||||
from app.utils.view_utils import bought_with_prices as bwp
|
||||
from copy import deepcopy
|
||||
from datetime import date as dtdate, timedelta
|
||||
from psycopg2 import errors
|
||||
from random import choice as rndchoice
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from string import ascii_letters, digits
|
||||
|
||||
def insert_bought_items(user: str, items: dict, date: str = None):
|
||||
def insert_bought_items(token: str, items: dict, date: str = None):
|
||||
if not date:
|
||||
date = dtdate.today()
|
||||
for item, amount in deepcopy(items).items():
|
||||
query_insert = insert(Bought).values(user=user, item=int(item), date=date, amount=int(amount))
|
||||
query_insert = insert(Bought).values(token=token, item=int(item), date=date, amount=int(amount))
|
||||
query_insert = query_insert.on_conflict_do_update("bought_pkey", set_=dict(amount=text(f'bought.amount + {amount}')))
|
||||
try:
|
||||
db.session.execute(query_insert)
|
||||
@ -20,20 +22,28 @@ def insert_bought_items(user: str, items: dict, date: str = None):
|
||||
db.session.rollback()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
LOGGER.exception()
|
||||
LOGGER.exception("")
|
||||
else:
|
||||
del(items[item])
|
||||
return {'user':user, 'date': date, 'items': items} if items else {}
|
||||
return {'user':token, 'date': date, 'items': items} if items else {}
|
||||
|
||||
def get_report(**kwargs):
|
||||
query_select = bought_with_prices.select()
|
||||
query_select = db.session.query(bwp.c.token, User.username, bwp.c.date, bwp.c.item, Item.name, bwp.c.amount, bwp.c.price)
|
||||
query_select = query_select.select_from(bwp).join(LoginToken, LoginToken.token==bwp.c.token).join(User, LoginToken.user==User.id).join(Item, Item.id==bwp.c.item)
|
||||
if "user" in kwargs:
|
||||
query_select = query_select.where(bought_with_prices.c.user == kwargs['user'])
|
||||
query_select = query_select.where(bwp.c.token == kwargs['user'])
|
||||
match kwargs:
|
||||
case {"month": month}:
|
||||
year = kwargs["year"] if "year" in kwargs else dtdate.today().year
|
||||
query_select = query_select.where(bought_with_prices.c.date.between(dtdate(int(year), int(month), 1), dtdate(int(year), int(month)+1, 1)-timedelta(days=1)))
|
||||
query_select = query_select.where(bwp.c.date.between(dtdate(int(year), int(month), 1), dtdate(int(year), int(month)+1, 1)-timedelta(days=1)))
|
||||
case {"year": year}:
|
||||
query_select = query_select.where(bought_with_prices.c.date.between(dtdate(int(year), 1, 1), dtdate(int(year), 12, 31)))
|
||||
results = db.session.execute(query_select)
|
||||
return tuple(results)
|
||||
query_select = query_select.where(bwp.c.date.between(dtdate(int(year), 1, 1), dtdate(int(year), 12, 31)))
|
||||
query_select = query_select.order_by(bwp.c.token, bwp.c.date, bwp.c.item)
|
||||
results = query_select.all()
|
||||
return tuple(results)
|
||||
|
||||
def generate_token(length = 15, allowed_chars = ascii_letters + digits):
|
||||
new_token = "".join((rndchoice(allowed_chars) for i in range(length)))
|
||||
if not LoginToken.query.filter_by(token=new_token).first():
|
||||
return new_token
|
||||
return generate_token()
|
||||
18
app/utils/routes_utils.py
Normal file
18
app/utils/routes_utils.py
Normal file
@ -0,0 +1,18 @@
|
||||
from datetime import date
|
||||
from flask import render_template
|
||||
from flask_login import current_user
|
||||
|
||||
|
||||
def get_base_infos():
|
||||
infos = {}
|
||||
infos['now'] = date.today()
|
||||
infos['current_user'] = current_user
|
||||
if current_user.is_authenticated:
|
||||
tokens = current_user.LoginToken.all()
|
||||
establishments = [logintoken.Establishment for logintoken in tokens]
|
||||
if establishments:
|
||||
infos['establishments'] = establishments
|
||||
return infos
|
||||
|
||||
def render_custom_template(*args, **kwargs):
|
||||
return render_template(*args, **kwargs, **get_base_infos())
|
||||
@ -1,18 +1,55 @@
|
||||
from app import LOGGER
|
||||
from app import db, LOGGER
|
||||
from app.models import AmountChange, Bought, PriceChange
|
||||
from datetime import date
|
||||
from flask import render_template
|
||||
from flask_login import current_user
|
||||
from sqlalchemy_utils import create_view
|
||||
|
||||
def group_results(results: tuple) -> dict:
|
||||
result_dict = {}
|
||||
def group_results(results: tuple) -> list:
|
||||
result_list = []
|
||||
LOGGER.debug("Grouping...")
|
||||
for result in results:
|
||||
if result[0] not in result_dict:
|
||||
result_dict[result[0]] = {"sum": 0}
|
||||
if str(result[1]) not in result_dict[result[0]]:
|
||||
result_dict[result[0]][str(result[1])] = {}
|
||||
result_dict[result[0]][str(result[1])][result[2]] = (
|
||||
result[3], result[4])
|
||||
price = int(result[3]) * int(float(result[4].split(" ")[0].replace(",", "."))*100)
|
||||
result_dict[result[0]]["sum"] += price
|
||||
for key in result_dict.keys():
|
||||
result_dict[key]["sum"] /= 100
|
||||
try:
|
||||
result_user_index = [result[0] == result_item['id'] for result_item in result_list].index(True)
|
||||
except ValueError as e:
|
||||
result_list.append({"id": result[0], "name": result[1], "sum": 0, "item_infos": []})
|
||||
result_user_index = -1
|
||||
result_user = result_list[result_user_index]
|
||||
try:
|
||||
result_date_index = [result[2] == result_list_date['date'] for result_list_date in result_user["item_infos"]].index(True)
|
||||
except ValueError as e:
|
||||
result_user["item_infos"].append({'date': result[2], 'item_list': []})
|
||||
result_date_index = -1
|
||||
result_date = result_user['item_infos'][result_date_index]
|
||||
result_date['item_list'].append({'id': result[3], 'name': result[4], 'amount': result[5], 'price': result[6]})
|
||||
for result_user in result_list:
|
||||
for result_date in result_user['item_infos']:
|
||||
for result_item in result_date['item_list']:
|
||||
result_user['sum'] += result_item['amount'] * result_item['price']
|
||||
LOGGER.debug("Grouped.")
|
||||
return result_dict
|
||||
return result_list
|
||||
|
||||
def selectable_price_per_amount_view():
|
||||
p = db.aliased(PriceChange, name="p")
|
||||
a = db.aliased(AmountChange, name="a")
|
||||
date = db.func.greatest(p.date, a.date).label("date")
|
||||
price = db.func.ceil(p.price.cast(db.Float)/db.func.coalesce(a.amount, 1)).label("price")
|
||||
select = db.select(p.item.label("item"), date, price)
|
||||
select = select.distinct(p.item, date)
|
||||
select = select.join(a, p.item==a.item, isouter=True)
|
||||
select = select.order_by(p.item, db.desc(db.func.greatest(p.date, a.date)))
|
||||
return select
|
||||
|
||||
price_per_amount = create_view("price_per_amount", selectable_price_per_amount_view(), db.metadata)
|
||||
|
||||
def selectable_bought_with_prices_view():
|
||||
b = db.aliased(Bought, name="b")
|
||||
ppa = price_per_amount.alias("ppa")
|
||||
select = db.select(b.token.label("token"), b.date.label("date"), b.item.label("item"), b.amount.label("amount"), ppa.c.price.label("price"))
|
||||
select = select.distinct(b.token, b.date, b.item)
|
||||
select = select.join(ppa, b.item==ppa.c.item)
|
||||
select = select.where(ppa.c.date<=b.date)
|
||||
select = select.order_by(db.desc(b.token), db.desc(b.date), db.desc(b.item), db.desc(ppa.c.date))
|
||||
return select
|
||||
|
||||
bought_with_prices = create_view("bought_with_prices", selectable_bought_with_prices_view(), db.metadata)
|
||||
66
app/views.py
66
app/views.py
@ -1,66 +0,0 @@
|
||||
from app import app, LOGGER
|
||||
#from app.forms import NewItemForm
|
||||
from app.models import User
|
||||
from flask import abort, request, render_template
|
||||
from flask.json import jsonify
|
||||
from app.utils import view_utils, database_utils
|
||||
|
||||
APPNAME = "scan2kasse"
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return "<h1>Hello, World!</h>", 200
|
||||
|
||||
@app.route('/test')
|
||||
def test():
|
||||
if request.args:
|
||||
LOGGER.debug(request.args['testing'])
|
||||
#form = NewItemForm()
|
||||
#return render_template("test.html", form=form)
|
||||
|
||||
|
||||
@app.route(f'/{APPNAME}/login')
|
||||
def login():
|
||||
if not request.json or 'login' not in request.json:
|
||||
abort(400)
|
||||
if not User.query.get(request.json['login']):
|
||||
abort(403)
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
@app.route(f'/{APPNAME}/insert', methods=['POST'])
|
||||
def insert():
|
||||
match request.json:
|
||||
case {'user': user, 'items': items, 'date': date}:
|
||||
failed = database_utils.insert_bought_items(user, items, date)
|
||||
case {'user': user, 'items': items}:
|
||||
failed = database_utils.insert_bought_items(user, items)
|
||||
case _:
|
||||
abort(400)
|
||||
if failed:
|
||||
return jsonify(failed), 400
|
||||
return jsonify({'inserted': True}), 201
|
||||
|
||||
@app.route(f'/{APPNAME}/overview', methods=['GET'])
|
||||
def get_report_from_user():
|
||||
user, month, year = [None]*3
|
||||
if request.args:
|
||||
args = request.args
|
||||
if 'month' in args:
|
||||
month = int(args['month'])
|
||||
if 'year' in args:
|
||||
year = int(args['year'])
|
||||
if month and (month > 12 or month < 1):
|
||||
abort(400)
|
||||
LOGGER.info("Getting results.")
|
||||
results = database_utils.get_report(user=user, year=year, month=month)
|
||||
LOGGER.debug(f"Results received: {results}")
|
||||
if results:
|
||||
result_dict = view_utils.group_results(results)
|
||||
else:
|
||||
result_dict = {}
|
||||
if request.content_type == "application/json":
|
||||
return jsonify(result_dict)
|
||||
else:
|
||||
return render_template("overview.html", results=result_dict)
|
||||
39
migrations/versions/3d5fb9e86939_create_views.py
Normal file
39
migrations/versions/3d5fb9e86939_create_views.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""create views
|
||||
|
||||
Revision ID: 3d5fb9e86939
|
||||
Revises: dbf88acb76bc
|
||||
Create Date: 2022-02-20 01:58:37.444542
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.utils.view_utils import selectable_price_per_amount_view, selectable_bought_with_prices_view
|
||||
from sqlalchemy_utils import create_view
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3d5fb9e86939'
|
||||
down_revision = 'dbf88acb76bc'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
metadata = sa.MetaData()
|
||||
create_view('price_per_amount',
|
||||
selectable_price_per_amount_view(),
|
||||
metadata
|
||||
)
|
||||
create_view('bought_with_prices',
|
||||
selectable_bought_with_prices_view(),
|
||||
metadata
|
||||
)
|
||||
metadata.create_all(db.engine)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with db.engine.connect() as con:
|
||||
con.execute("DROP VIEW bought_with_prices;")
|
||||
con.execute("DROP VIEW price_per_amount;")
|
||||
@ -1,88 +0,0 @@
|
||||
"""full structure
|
||||
|
||||
Revision ID: 60e8f49dee49
|
||||
Revises:
|
||||
Create Date: 2022-02-03 19:24:24.715589
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '60e8f49dee49'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('brand',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=32), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('category',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=32), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.String(length=10), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.Column('password_hash', sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('item',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=True),
|
||||
sa.Column('brand', sa.Integer(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['brand'], ['brand.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('amount_change',
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('amount', sa.SmallInteger(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.PrimaryKeyConstraint('item', 'date')
|
||||
)
|
||||
op.create_table('bought',
|
||||
sa.Column('user', sa.String(length=10), nullable=False),
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('amount', sa.SmallInteger(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.ForeignKeyConstraint(['user'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('user', 'item', 'date')
|
||||
)
|
||||
op.create_table('item_category',
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('category', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['category'], ['category.id'], ),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.PrimaryKeyConstraint('item', 'category')
|
||||
)
|
||||
op.create_table('price_change',
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('price', sa.SmallInteger(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.PrimaryKeyConstraint('item', 'date')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('price_change')
|
||||
op.drop_table('item_category')
|
||||
op.drop_table('bought')
|
||||
op.drop_table('amount_change')
|
||||
op.drop_table('item')
|
||||
op.drop_table('user')
|
||||
op.drop_table('category')
|
||||
op.drop_table('brand')
|
||||
# ### end Alembic commands ###
|
||||
127
migrations/versions/dbf88acb76bc_full_structure.py
Normal file
127
migrations/versions/dbf88acb76bc_full_structure.py
Normal file
@ -0,0 +1,127 @@
|
||||
"""full structure
|
||||
|
||||
Revision ID: dbf88acb76bc
|
||||
Revises:
|
||||
Create Date: 2022-02-20 01:58:13.735541
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'dbf88acb76bc'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('brand',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=32), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('category',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=32), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('establishment',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('receipt',
|
||||
sa.Column('id', sa.Numeric(precision=22, scale=0), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('registered', sa.Boolean(), nullable=False),
|
||||
sa.Column('paid', sa.SmallInteger(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('email', sa.String(length=64), nullable=False),
|
||||
sa.Column('username', sa.String(length=64), nullable=False),
|
||||
sa.Column('password_hash', sa.String(length=128), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_table('item',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('name', sa.String(length=64), nullable=False),
|
||||
sa.Column('brand', sa.Integer(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['brand'], ['brand.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('login_token',
|
||||
sa.Column('user', sa.BigInteger(), nullable=False),
|
||||
sa.Column('establishment', sa.BigInteger(), nullable=False),
|
||||
sa.Column('token', sa.String(length=15), nullable=True),
|
||||
sa.ForeignKeyConstraint(['establishment'], ['establishment.id'], ),
|
||||
sa.ForeignKeyConstraint(['user'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('user', 'establishment'),
|
||||
sa.UniqueConstraint('token')
|
||||
)
|
||||
op.create_table('amount_change',
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('amount', sa.SmallInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.PrimaryKeyConstraint('item', 'date')
|
||||
)
|
||||
op.create_table('bought',
|
||||
sa.Column('token', sa.String(length=15), nullable=False),
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('amount', sa.SmallInteger(), nullable=False),
|
||||
sa.Column('registered', sa.Boolean(), nullable=False),
|
||||
sa.Column('paid', sa.SmallInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.ForeignKeyConstraint(['token'], ['login_token.token'], ),
|
||||
sa.PrimaryKeyConstraint('token', 'item', 'date')
|
||||
)
|
||||
op.create_table('item_category',
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('category', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['category'], ['category.id'], ),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.PrimaryKeyConstraint('item', 'category')
|
||||
)
|
||||
op.create_table('item_receipt',
|
||||
sa.Column('receipt', sa.Numeric(precision=22, scale=0), nullable=False),
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('amount', sa.SmallInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.ForeignKeyConstraint(['receipt'], ['receipt.id'], ),
|
||||
sa.PrimaryKeyConstraint('receipt', 'item')
|
||||
)
|
||||
op.create_table('price_change',
|
||||
sa.Column('item', sa.BigInteger(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('price', sa.SmallInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['item'], ['item.id'], ),
|
||||
sa.PrimaryKeyConstraint('item', 'date')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('price_change')
|
||||
op.drop_table('item_receipt')
|
||||
op.drop_table('item_category')
|
||||
op.drop_table('bought')
|
||||
op.drop_table('amount_change')
|
||||
op.drop_table('login_token')
|
||||
op.drop_table('item')
|
||||
op.drop_table('user')
|
||||
op.drop_table('receipt')
|
||||
op.drop_table('establishment')
|
||||
op.drop_table('category')
|
||||
op.drop_table('brand')
|
||||
# ### end Alembic commands ###
|
||||
@ -1,44 +0,0 @@
|
||||
"""add views
|
||||
|
||||
Revision ID: f79ee0125ba6
|
||||
Revises: 60e8f49dee49
|
||||
Create Date: 2022-02-07 13:29:26.663482
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from app import db
|
||||
from app.models import query_price_per_amount_view, query_bought_with_prices_view
|
||||
from alembic import op
|
||||
from sqlalchemy_utils import create_view
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f79ee0125ba6'
|
||||
down_revision = '60e8f49dee49'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
metadata = sa.MetaData()
|
||||
create_view('price_per_amount',
|
||||
query_price_per_amount_view(),
|
||||
metadata
|
||||
)
|
||||
create_view('bought_with_prices',
|
||||
query_bought_with_prices_view(),
|
||||
metadata
|
||||
)
|
||||
metadata.create_all(db.engine)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with db.engine.connect() as con:
|
||||
con.execute("DROP VIEW bought_with_prices;")
|
||||
con.execute("DROP VIEW price_per_amount;")
|
||||
#op.drop_table('bought_with_prices')
|
||||
#op.drop_table('price_per_amount')
|
||||
# ### end Alembic commands ###
|
||||
3
run.py
3
run.py
@ -4,7 +4,8 @@ from gevent.pywsgi import WSGIServer
|
||||
|
||||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
return {'db': db, 'User': User, 'Bought': Bought, 'Item': Item}
|
||||
return {'db': db, 'User': User, 'Bought': Bought, 'Item': Item,
|
||||
"LoginToken": LoginToken, "Establishment": Establishment, "Receipt": Receipt}
|
||||
|
||||
if __name__ == '__main__':
|
||||
http_server = WSGIServer(('', 5000), app)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user