diff --git a/app/__init__.py b/app/__init__.py index 66a844d..2542ee0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 \ No newline at end of file +from app import routes, models \ No newline at end of file diff --git a/app/forms.py b/app/forms.py index b1908e4..12dd95e 100644 --- a/app/forms.py +++ b/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") \ No newline at end of file diff --git a/app/models.py b/app/models.py index aa9b4b9..8677fc3 100644 --- a/app/models.py +++ b/app/models.py @@ -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"" + return f"" + +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"" 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"" 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"" @@ -103,30 +116,6 @@ class ItemReceipt(db.Model): def __repr__(self) -> str: return f"" -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): diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..5316d20 --- /dev/null +++ b/app/routes.py @@ -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 "

Hello, World!", 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) \ No newline at end of file diff --git a/app/static/sidebars.css b/app/static/sidebars.css new file mode 100644 index 0000000..6949a37 --- /dev/null +++ b/app/static/sidebars.css @@ -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; } diff --git a/app/templates/admin/new_item.html b/app/templates/admin/new_item.html new file mode 100644 index 0000000..52c9c57 --- /dev/null +++ b/app/templates/admin/new_item.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+ {{ form.hidden_tag() }} +
{{form.id.label}}
+
{{form.id()}}
+
{{form.name.label}}
+
{{form.name()}}
+
{{form.description.label}}
+
{{form.description()}}
+
{{form.date.label}}
+
{{form.date()}}
+
{{form.price_change.label}}
+
{{form.price_change()}}
+
{{form.amount_change.label}}
+
{{form.amount_change()}}
+
{{form.category.label}}
+
{{form.category()}}
+
{{form.brand.label}}
+
{{form.brand()}}
+
{{form.submit()}}
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 7a29738..44054ec 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -4,40 +4,90 @@ - + + {% if title %} + {{ title }} + {% else %} + Scan2Kasse + {% endif %} - -
-
- {% block content %}{% endblock %} -
-
+ \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..1a3eb64 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }} + {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }} + {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.remember_me() }} {{ form.remember_me.label }}

+

{{ form.submit() }}

+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/overview.html b/app/templates/overview.html index 1b80817..e919b90 100644 --- a/app/templates/overview.html +++ b/app/templates/overview.html @@ -1,27 +1,31 @@ {% extends "base.html" %} {% block content %} - {% for user, dates in results.items() %} -
-

{{ user }}: {{ dates.sum }} €

- {% for date, items in dates.items() %} - {% if date != "sum" %} -
-
-
-

{{ date }}

- {% for item, values in items.items() %} -
-
-
- {{ values.0 }}x {{ item }} je {{ values.1 }} -
+ {% for user in results %} +
+ +
+ {% for item_infos in user.item_infos %} +
+
+
+

{{ item_infos.date }}

+ {% for item in item_infos.item_list %} +
+
+
+ {{ item.amount }}x {{ item.name }} je {{ item.price/100 }} €
- {% endfor %} -
+
+ {% endfor %}
- {% endif %} - {% endfor %} +
+ {% endfor %} +
{% endfor %} {% endblock %} \ No newline at end of file diff --git a/app/utils/database_utils.py b/app/utils/database_utils.py index c4b2f37..102c9ec 100644 --- a/app/utils/database_utils.py +++ b/app/utils/database_utils.py @@ -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) \ No newline at end of file + 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() \ No newline at end of file diff --git a/app/utils/routes_utils.py b/app/utils/routes_utils.py new file mode 100644 index 0000000..77eb22e --- /dev/null +++ b/app/utils/routes_utils.py @@ -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()) \ No newline at end of file diff --git a/app/utils/view_utils.py b/app/utils/view_utils.py index 8c340ef..d18bfe4 100644 --- a/app/utils/view_utils.py +++ b/app/utils/view_utils.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/app/views.py b/app/views.py deleted file mode 100644 index 2f19f1b..0000000 --- a/app/views.py +++ /dev/null @@ -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 "

Hello, World!", 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) \ No newline at end of file diff --git a/migrations/versions/3d5fb9e86939_create_views.py b/migrations/versions/3d5fb9e86939_create_views.py new file mode 100644 index 0000000..c10a3b8 --- /dev/null +++ b/migrations/versions/3d5fb9e86939_create_views.py @@ -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;") diff --git a/migrations/versions/60e8f49dee49_full_structure.py b/migrations/versions/60e8f49dee49_full_structure.py deleted file mode 100644 index 44aeb64..0000000 --- a/migrations/versions/60e8f49dee49_full_structure.py +++ /dev/null @@ -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 ### diff --git a/migrations/versions/dbf88acb76bc_full_structure.py b/migrations/versions/dbf88acb76bc_full_structure.py new file mode 100644 index 0000000..713367d --- /dev/null +++ b/migrations/versions/dbf88acb76bc_full_structure.py @@ -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 ### diff --git a/migrations/versions/f79ee0125ba6_add_views.py b/migrations/versions/f79ee0125ba6_add_views.py deleted file mode 100644 index 5c6b34e..0000000 --- a/migrations/versions/f79ee0125ba6_add_views.py +++ /dev/null @@ -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 ### diff --git a/run.py b/run.py index ab20d65..b496b87 100644 --- a/run.py +++ b/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)