major: improvements and functions

Features:
- Added page for an overview of establishments with a possibility to
request a membership.
- Added page for admins of an establishment to accept or deny those
candidates. This is currently only usable via URL, the navigation to
this site is not yet implemented in the HTML files.
- Added page to add new establishments by providing a name.

Improvements:
- Better folder structure.
- The establishment-specific overview can now be viewed with another
URL, as well as some other pages.

Bugfixes:
- Seriously I don't know anymore what I fixed and what not. But it works
just better now :)

Future:
- Angular has been added to separate the Flask-Backend with the
frontend. Angular is currently not connected to the backend, but this
will change in the future.
This commit is contained in:
Lunaresk 2023-03-19 12:31:10 +01:00
parent 764738b20d
commit 2e33f9b5a8
130 changed files with 13980 additions and 585 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
# Ignore pycaches
logs
__pycache__
.flaskenv
.env*
.flaskenv*
!.env.project
!.env.vault

12
.env.vault Normal file
View File

@ -0,0 +1,12 @@
#################################################################################
# #
# This file uniquely identifies your project in dotenv-vault. #
# You SHOULD commit this file to source control. #
# #
# Generated with 'npx dotenv-vault new' #
# #
# Learn more at https://dotenv.org/env-vault #
# #
#################################################################################
DOTENV_VAULT=vlt_54536d38674329461d10b74ec7e56be43df6021bb5267bedec1f774758371784

226
.gitignore vendored
View File

@ -1,3 +1,45 @@
# Created by https://www.toptal.com/developers/gitignore/api/flask,python,git,visualstudiocode,angular,venv
# Edit at https://www.toptal.com/developers/gitignore?templates=flask,python,git,visualstudiocode,angular,venv
### Angular ###
## Angular ##
# compiled output
dist/
tmp/
app/**/*.js
app/**/*.js.map
# dependencies
node_modules/
bower_components/
# IDEs and editors
.idea/
# misc
.sass-cache/
connect.lock/
coverage/
libpeerconnection.log/
npm-debug.log
testem.log
typings/
.angular/
# e2e
e2e/*.js
e2e/*.map
# System Files
.DS_Store/
### Flask ###
instance/*
!instance/.gitignore
.webassets-cache
.env
### Flask.Python Stack ###
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
@ -10,7 +52,6 @@ __pycache__/
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
@ -20,7 +61,6 @@ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
pip-wheel-metadata/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
@ -50,6 +90,7 @@ coverage.xml
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/
# Translations # Translations
*.mo *.mo
@ -63,7 +104,6 @@ db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
@ -72,6 +112,7 @@ instance/
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
@ -82,7 +123,9 @@ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
.python-version # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -91,7 +134,22 @@ ipython_config.py
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
@ -102,9 +160,7 @@ celerybeat.pid
*.sage.py *.sage.py
# Environments # Environments
.env
.venv .venv
.flaskenv
env/ env/
venv/ venv/
ENV/ ENV/
@ -129,6 +185,157 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# Celery stuff
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
# pytype static type analyzer
# Cython debug symbols
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
### venv ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/flask,python,git,visualstudiocode,angular,venv
# Misc # Misc
config.yaml config.yaml
scans.json scans.json
@ -136,3 +343,8 @@ test.*
*.db *.db
.vscode .vscode
*.tar *.tar
*.code-workspace
.env*
.flaskenv*
!.env.project
!.env.vault

View File

@ -1,27 +1,25 @@
FROM python@sha256:38000b248a186dcae150fe2f64d23bd44a0730347d1e5e4d1faedd449a9a4913 FROM python@sha256:53da4973924b6b3da6eb34f98e4e9dffdaf1cc05b468da73c69e3a862c36ee19
# python:3.10.10-slim-bullseye
RUN useradd costhive
RUN useradd scan2kasse WORKDIR /home/costhive
WORKDIR /home/scan2kasse RUN apt update && apt -y upgrade
RUN apt update && apt upgrade
RUN apt install -y libpq-dev gcc g++ RUN apt install -y libpq-dev gcc g++
COPY requirements.txt requirements.txt RUN python -m pip install pipenv
RUN python -m venv venv
RUN venv/bin/pip install -r requirements.txt
RUN venv/bin/pip install gunicorn
COPY app app COPY boot.sh ./
COPY migrations migrations
COPY configs configs
COPY run.py boot.sh ./
RUN chmod +x boot.sh RUN chmod +x boot.sh
COPY backend backend
ENV FLASK_APP run.py ENV FLASK_APP run.py
RUN chown -R scan2kasse:scan2kasse ./ RUN chown -R costhive:costhive ./
USER scan2kasse USER costhive
RUN cd backend && pipenv install && pipenv install gunicorn && cd ..
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["./boot.sh"] ENTRYPOINT ["./boot.sh"]

View File

@ -1 +1 @@
# WG_Scan2Kasse # CostHive

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('api', __name__)
from app.api import routes

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import forms, routes

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('errors', __name__)
from app.errors import handlers

View File

@ -1,119 +0,0 @@
from app import db, LOGGER
from app.main.forms import NewItemForm
from app.main import bp
from app.models import AmountChange, Brand, Establishment, LoginToken, Item, PriceChange
from app.utils import view_utils, database_utils
from app.utils.routes_utils import render_custom_template as render_template
from datetime import date
from flask import abort, redirect, request, url_for
from flask.json import jsonify
from flask_login import current_user, login_required
@bp.route('/')
@bp.route('/index')
def index():
return render_template("base.html")
# @bp.route('/')
# def test():
# return "Hello World"
@bp.route('/overview', methods=['GET'])
@login_required
def get_report_from_user():
if current_user.is_anonymous:
abort(403)
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(**request.args)
LOGGER.debug(f"Results received.")
# LOGGER.debug(str(results))
if results:
result_list = view_utils.group_results(results)
else:
result_list = []
if request.content_type == "application/json":
return jsonify(result_list)
else:
if "establishment" in request.args:
return render_template("main/overview.html", results=result_list, establishment = Establishment.query.get(int(request.args['establishment'])))
else:
return render_template("main/overview.html", results=result_list)
@bp.route('/token_authorization')
def token_authorization():
LOGGER.debug("Token Login")
if not request.json or 'login' not in request.json:
LOGGER.debug("JSON not delivered or 'login' not in JSON")
abort(400)
if not LoginToken.query.filter_by(token=request.json['login']).first():
LOGGER.debug(f"Token <{request.json['login']}> not recognized")
abort(403)
LOGGER.debug("Token accepted")
return jsonify({}), 200
@bp.route('/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
@bp.route('/new_item', methods=['GET', 'POST'])
@login_required
def new_item():
if current_user.is_anonymous:
abort(403)
form=NewItemForm.new()
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
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('main.index'))
return render_template('main/new_item.html', form=form)
@bp.route('/overview/register_boughts', methods=['GET'])
@login_required
def check_unregistered_items():
if current_user.is_anonymous or not request.args or 'establishment' not in request.args:
abort(403)
establishment = Establishment.query.get(int(request.args['establishment']))
if current_user.id != establishment.owner:
abort(403)
results = database_utils.get_unregistered_and_register(establishment.id)
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("main/overview.html", results=result_list)

View File

@ -1,147 +0,0 @@
import jwt
from app import db, login
from datetime import date
from flask import current_app
from flask_login import UserMixin
from time import time
from werkzeug.security import generate_password_hash, check_password_hash
item_category = db.Table("item_category",
db.Column("item", db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue()),
db.Column("category", db.ForeignKey("category.id"), primary_key=True, server_onupdate=db.FetchedValue())
)
class User(UserMixin, db.Model):
id = db.Column(db.BigInteger, primary_key=True)
email = db.Column(db.String(255), nullable=False, unique=True)
password_hash = db.Column(db.String(128), nullable=False)
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="User,LoginToken")
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
def __repr__(self) -> str:
return f"<User {self.id} ({self.email})>"
class Establishment(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
name = db.Column(db.String(64), nullable=False)
owner = db.Column(db.ForeignKey('user.id'), nullable=False)
LoginToken = db.relationship("LoginToken", backref='Establishment', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="Establishment,LoginToken,Bought")
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, server_onupdate=db.FetchedValue())
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
token = db.Column(db.String(15), nullable=False, unique=True)
paid = db.Column(db.BigInteger, nullable=False, server_default=str(0))
Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic')
def __repr__(self) -> str:
return f"LoginToken {self.token}"
class Brand(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False)
def __repr__(self) -> str:
return f"<Brand {self.id} ({self.name})>"
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False, unique=True)
Item = db.relationship("Item", secondary=item_category, lazy="dynamic", back_populates="Category")
def __repr__(self) -> str:
return f"<Category {self.id} ({self.name})>"
class Item(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
name = db.Column(db.String(64), nullable=False)
brand = db.Column(db.ForeignKey('brand.id'), nullable=False, server_onupdate=db.FetchedValue())
description = db.Column(db.Text, nullable=False)
Category = db.relationship("Category", secondary=item_category, lazy="dynamic", back_populates="Item")
Bought = db.relationship("Bought", backref='Item', lazy='dynamic')
PriceChange = db.relationship("PriceChange", backref='Item', lazy='dynamic')
AmountChange = db.relationship("AmountChange", backref='Item', lazy='dynamic')
def __repr__(self) -> str:
return f"<Item {self.id} ({self.name})>"
class Bought(db.Model):
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True)
amount = db.Column(db.SmallInteger, nullable=False)
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
def __repr__(self) -> str:
return f"<Bought Object>"
class PriceChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
price = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Price_Change {self.item} ({self.date})>"
class AmountChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
amount = db.Column(db.SmallInteger, nullable=False, server_default=str(1))
def __repr__(self) -> str:
return f"<Amount_Change {self.item} ({self.date})>"
class Receipt(db.Model):
id = db.Column(db.Numeric(precision=24, scale=0), primary_key=True, autoincrement=False)
date = db.Column(db.Date, nullable=False)
from_user = db.Column(db.ForeignKey("login_token.token"), server_onupdate=db.FetchedValue())
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
ItemReceipt = db.relationship("ItemReceipt", backref='Receipt', lazy='dynamic')
def __repr__(self) -> str:
return f"<Receipt {self.id}>"
class ItemReceipt(db.Model):
receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue())
amount = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<ItemReceipt {self.receipt}: {self.item}>"
@login.user_loader
def load_user(id):
return User.query.get(int(id))

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('receipts', __name__)
from app.receipts import forms, routes

View File

@ -1,125 +0,0 @@
{% extends "bootstrap/base.html" %}
{% block title %}
{% if title %}
{{ title }}
{% else %}
Scan2Kasse
{% endif %}
{% endblock %}
{% block metas %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
{% block styles %}
<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={{ url_for('static', filename="sidebars.css")}}>
{% endblock %}
{% block navbar %}
<main>
<div class="flex-shrink-0 p-3 bg-black bg-opacity-10 scrollbar-primary h-100 position-fixed" style="width: 280px;">
<a href="/" class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom border-dark">
<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 (WIP)
</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="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false">
Übersicht
</button>
<div class="collapse" id="dashboard-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
{% if establishments %}
<!-- <li><a href="#" class="link-dark rounded">Allgemein</a></li> -->
{% for establishment in establishments %}
<li>
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#est_{{ establishment.id }}" aria-expanded="false">
{{ establishment.name }}
</button>
<div class="collapse" id="est_{{ establishment.id }}">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<li><a href="{{ url_for('main.get_report_from_user', establishment=establishment.id) }}" class="link-dark rounded">Übersicht</a></li>
</ul>
</div>
</li>
{% endfor %}
{% endif %}
</ul>
</div>
</li>
<!-- <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 border-dark 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">Settings</a></li> -->
{% if current_user.is_authenticated %}
<!-- <li><a href="#" class="link-dark rounded">Profile</a></li> -->
<li><a href={{ url_for('auth.web_logout') }} class="link-dark rounded">Sign out</a></li>
{% else %}
<li><a href={{ url_for('auth.web_register') }} class="link-dark rounded">Register</a></li>
<li><a href={{ url_for('auth.web_login') }} class="link-dark rounded">Sign in</a></li>
{% endif %}
</ul>
</div>
</li>
</ul>
</div>
{% endblock %}
{% block content %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div style="width: 280px;"></div>
<div class="container">
<div class="row my-3"></div>
<div class="row-md-3">
{% block app_content %}{% endblock %}
</div>
<div class="row my-3"></div>
</div>
</main>
{% endblock %}
{% block scripts %}
<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>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
{% endblock %}

View File

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<form action="" method="post">
{{ form.hidden_tag() }}
{{ wtf.form_field(form.id, class=form.id.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.name, class=form.name.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.description, class=form.description.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.date, class=form.date.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.price_change, class=form.price_change.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.amount_change, class=form.amount_change.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.category, class=form.category.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.brand, class=form.brand.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
</form>
{% endblock %}

View File

@ -1,38 +0,0 @@
{% extends "base.html" %}
{% block app_content %}
{% if establishment %}
{% if current_user.id == establishment.owner %}
<button type="button" class="btn btn-outline-dark px-2" data-bs-toggle="button" autocomplete="off" onclick="window.location.href='{{ url_for('main.check_unregistered_items', establishment=establishment.id) }}'">
Abrechnung
</button>
{% endif %}
{% endif %}
{% for user in results %}
<div class="card">
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
<div class="card-header">
<h3>{{ user.email }}: {{ user.sum/100 }} €</h3>
</div>
</button>
<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>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

25
backend/Pipfile Normal file
View File

@ -0,0 +1,25 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
email-validator = "*"
flask = "*"
flask-bootstrap = "*"
flask-login = "*"
flask-mail = "*"
flask-migrate = "*"
flask-sqlalchemy = "*"
flask-wtf = "*"
psycopg2-binary = "*"
pyjwt = "*"
python-dotenv = "*"
pymupdf = "*"
sqlalchemy-utils = "*"
[dev-packages]
[requires]
python_version = "3.10"
python_full_version = "3.10.10"

500
backend/Pipfile.lock generated Normal file
View File

@ -0,0 +1,500 @@
{
"_meta": {
"hash": {
"sha256": "92648b89cfca30fdd2c1a1258fd13c9249dffdcd9450c342a5c8bdf5d433b852"
},
"pipfile-spec": 6,
"requires": {
"python_full_version": "3.10.10",
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"alembic": {
"hashes": [
"sha256:4d3bd32ecdbb7bbfb48a9fe9e6d6fd6a831a1b59d03e26e292210237373e7db5",
"sha256:6f1c2207369bf4f49f952057a33bb017fbe5c148c2a773b46906b806ea6e825f"
],
"markers": "python_version >= '3.7'",
"version": "==1.9.4"
},
"blinker": {
"hashes": [
"sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36",
"sha256:923e5e2f69c155f2cc42dafbbd70e16e3fde24d2d4aa2ab72fbe386238892462"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.5"
},
"click": {
"hashes": [
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.3"
},
"colorama": {
"hashes": [
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
],
"markers": "platform_system == 'Windows'",
"version": "==0.4.6"
},
"dnspython": {
"hashes": [
"sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9",
"sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==2.3.0"
},
"dominate": {
"hashes": [
"sha256:520101360892ebf9d0553f67d37e359ff92403d8a1e33814030503088a05da49",
"sha256:5fe4258614687c6d3de67b0bbd881ed435a93a19742ae187344055db17052402"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.7.0"
},
"email-validator": {
"hashes": [
"sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda",
"sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"
],
"index": "pypi",
"version": "==1.3.1"
},
"flask": {
"hashes": [
"sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d",
"sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d"
],
"index": "pypi",
"version": "==2.2.3"
},
"flask-bootstrap": {
"hashes": [
"sha256:cb08ed940183f6343a64e465e83b3a3f13c53e1baabb8d72b5da4545ef123ac8"
],
"index": "pypi",
"version": "==3.3.7.1"
},
"flask-login": {
"hashes": [
"sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f",
"sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3"
],
"index": "pypi",
"version": "==0.6.2"
},
"flask-mail": {
"hashes": [
"sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"
],
"index": "pypi",
"version": "==0.9.1"
},
"flask-migrate": {
"hashes": [
"sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3",
"sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"
],
"index": "pypi",
"version": "==4.0.4"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec",
"sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"
],
"index": "pypi",
"version": "==3.0.3"
},
"flask-wtf": {
"hashes": [
"sha256:41c4244e9ae626d63bed42ae4785b90667b885b1535d5a4095e1f63060d12aa9",
"sha256:7887d6f1ebb3e17bf648647422f0944c9a469d0fcf63e3b66fb9a83037e38b2c"
],
"index": "pypi",
"version": "==1.1.1"
},
"greenlet": {
"hashes": [
"sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a",
"sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a",
"sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43",
"sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33",
"sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8",
"sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088",
"sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca",
"sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343",
"sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645",
"sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db",
"sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df",
"sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3",
"sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86",
"sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2",
"sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a",
"sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf",
"sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7",
"sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394",
"sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40",
"sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3",
"sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6",
"sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74",
"sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0",
"sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3",
"sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91",
"sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5",
"sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9",
"sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8",
"sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b",
"sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6",
"sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb",
"sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73",
"sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b",
"sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df",
"sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9",
"sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f",
"sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0",
"sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857",
"sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a",
"sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249",
"sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30",
"sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292",
"sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b",
"sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d",
"sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b",
"sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c",
"sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca",
"sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7",
"sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75",
"sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae",
"sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b",
"sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470",
"sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564",
"sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9",
"sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099",
"sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0",
"sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5",
"sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19",
"sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1",
"sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"
],
"markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
"version": "==2.0.2"
},
"idna": {
"hashes": [
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.4"
},
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"jinja2": {
"hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.2"
},
"mako": {
"hashes": [
"sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818",
"sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"
],
"markers": "python_version >= '3.7'",
"version": "==1.2.4"
},
"markupsafe": {
"hashes": [
"sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
"sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
"sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
"sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
"sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
"sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
"sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
"sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
"sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
"sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
"sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
"sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
"sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
"sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
"sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
"sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
"sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
"sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
"sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
"sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
"sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
"sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
"sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
"sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
"sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
"sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
"sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
"sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
"sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
"sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
"sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
"sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
"sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
"sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
"sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
"sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
"sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
"sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
"sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
"sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
"sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
"sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
"sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
"sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
"sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
"sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
"sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
"sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
"sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
"sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"psycopg2-binary": {
"hashes": [
"sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50",
"sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425",
"sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f",
"sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0",
"sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460",
"sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41",
"sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85",
"sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd",
"sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0",
"sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd",
"sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147",
"sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c",
"sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903",
"sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba",
"sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632",
"sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577",
"sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c",
"sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7",
"sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867",
"sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2",
"sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9",
"sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff",
"sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a",
"sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302",
"sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1",
"sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79",
"sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835",
"sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42",
"sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e",
"sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61",
"sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32",
"sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68",
"sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1",
"sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60",
"sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8",
"sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b",
"sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a",
"sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec",
"sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5",
"sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2",
"sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16",
"sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5",
"sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6",
"sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1",
"sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503",
"sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b",
"sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d",
"sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28",
"sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4",
"sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5",
"sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5",
"sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e",
"sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f",
"sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636",
"sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d",
"sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64",
"sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb",
"sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882",
"sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720",
"sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896",
"sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267",
"sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7",
"sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f",
"sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91",
"sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c",
"sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24",
"sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee",
"sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d",
"sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b",
"sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935",
"sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"
],
"index": "pypi",
"version": "==2.9.5"
},
"pyjwt": {
"hashes": [
"sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd",
"sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"
],
"index": "pypi",
"version": "==2.6.0"
},
"pymupdf": {
"hashes": [
"sha256:06860a8e60ba14f25aac5db90803d59b1a2fdac24c2b65dc6f9876ff36df586e",
"sha256:069ba56c28cb9b603d016c63f23a21bf22a00c5b2a92286bf3dd6a2759e119d4",
"sha256:0b08b6afbb656f0e4582a8ee44c2ce0ab6235bb81830ec92d79f6e4893db639c",
"sha256:0c85efbc08c83c8d91ae1cc7bb7e06c7cd7e203e9d0ed806ff22db173999f30c",
"sha256:18fbdcb35c144b26a3a507bfa73f98401a27819f29dca496eb470b9a1ec09fad",
"sha256:19fe49966f3a072a258cdfbab3d8c5b11cb7c2fb7c8e6abf3398d9e55af6f1ca",
"sha256:1fc4c8aee186326b3f138be8a4ac16a343e76c8ec45b5afab2d105a5e3b02d80",
"sha256:2a870f02b2767e9371bbb3d0263c3994f59679a64b1b9dd1a6b0d9a15c963d68",
"sha256:2bb5f9ce33054ebfbf63c41e68606d51e3ad5d85def0af5ebb6f0dd276e61037",
"sha256:2fbb96336c5cc065f6e5359f034a689e4d64ca7697dc5965b03f105b2ecb7ba1",
"sha256:36181c4cb27740791d611d0224dd18ac78e23a1f06aa2151394c5b9194b4f885",
"sha256:7e5a1ed49df5eee7834fb37adb5c94354e98b23fa997e67034b157790a31bbfc",
"sha256:909afad284c24f25a831b37e433e444dc4cb5b3e38b3f761b1e4acad5c65327d",
"sha256:ac2f61632bcd4c9532a26b52684ca05a4c8b7b96d6be947134b3e01a420904fd",
"sha256:b2b197bb0f00159f4584f51c7bf4d897aa5fb88de3c1f964347047a710c1f1be",
"sha256:bf204723782ca481cd120e1ab124b0d59fd387103e15675ae462415bfa22b3a2",
"sha256:c040c9ac98b7a1afc06fe55ffedbf6a305c24bf1c97597838f208c68035971f4",
"sha256:c09cbc7924ddf99452bb0d70438d64753815d75526d6d94293249f1665691d84",
"sha256:d85c360abd72c14e302a2cefd02ebd01568f5c669700c7fc1ed056e9cdf3c285",
"sha256:dab0459791175dea813e6130e08b9026d3b9d66854f0dbb7ab51ab1619875d24",
"sha256:dddd3fe6fa20b3e2642c10e66c97cfd3727010d3dafe653f16dce52396ef0208",
"sha256:e334e7baea701d4029faa034520c722c9c55e8315b7ccf4d1b686eba8e293f32",
"sha256:e5273ff0c3bf08428ef956c4a5e5e0b475667cc3e1cb7b41d9f5131636996e59",
"sha256:eb29cbfd34d5ef99c657539c4f48953ea15f4b1e4e162b48efcffa5c4101ce1d",
"sha256:f3ffa9a8c643da39aba80e787d2c1771d5be07b0e15bf193ddd1fda888bbca88",
"sha256:f815741a435c62a0036bbcbf5fa6c533567bd69c5338d413714fc57b22db93e0",
"sha256:f99e6c2109c38c17cd5f1b82c9d35f1e815a71a3464b765f41f3c8cf875dcf4c",
"sha256:fa0334247549def667d9f5358e558ec93815c78cebdb927a81dda38d681bf550",
"sha256:fdba6bf77d1d3cd57ea93181c494f4b138523000ff74b25c523be7dce0058ac9",
"sha256:ff7d01cb563c3ca18880ba6a2661351f07b8a54f6599272be2e3568524e5e721"
],
"index": "pypi",
"version": "==1.21.1"
},
"python-dotenv": {
"hashes": [
"sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba",
"sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"
],
"index": "pypi",
"version": "==1.0.0"
},
"sqlalchemy": {
"hashes": [
"sha256:011ef3c33f30bae5637c575f30647e0add98686642d237f0c3a1e3d9b35747fa",
"sha256:0adca8a3ca77234a142c5afed29322fb501921f13d1d5e9fa4253450d786c160",
"sha256:1644c603558590f465b3fa16e4557d87d3962bc2c81fd7ea85b582ecf4676b31",
"sha256:2267c004e78e291bba0dc766a9711c389649cf3e662cd46eec2bc2c238c637bd",
"sha256:25e4e54575f9d2af1eab82d3a470fca27062191c48ee57b6386fe09a3c0a6a33",
"sha256:2a2f9120eb32190bdba31d1022181ef08f257aed4f984f3368aa4e838de72bc0",
"sha256:2c82395e2925639e6d320592943608070678e7157bd1db2672a63be9c7889434",
"sha256:3f927340b37fe65ec42e19af7ce15260a73e11c6b456febb59009bfdfec29a35",
"sha256:54aa9f40d88728dd058e951eeb5ecc55241831ba4011e60c641738c1da0146b7",
"sha256:57dcd9eed52413f7270b22797aa83c71b698db153d1541c1e83d45ecdf8e95e7",
"sha256:582053571125895d008d4b8d9687d12d4bd209c076cdbab3504da307e2a0a2bd",
"sha256:59cf0cdb29baec4e074c7520d7226646a8a8f856b87d8300f3e4494901d55235",
"sha256:6363697c938b9a13e07f1bc2cd433502a7aa07efd55b946b31d25b9449890621",
"sha256:662a79e80f3e9fe33b7861c19fedf3d8389fab2413c04bba787e3f1139c22188",
"sha256:67901b91bf5821482fcbe9da988cb16897809624ddf0fde339cd62365cc50032",
"sha256:679b9bd10bb32b8d3befed4aad4356799b6ec1bdddc0f930a79e41ba5b084124",
"sha256:738c80705e11c1268827dbe22c01162a9cdc98fc6f7901b429a1459db2593060",
"sha256:77a380bf8721b416782c763e0ff66f80f3b05aee83db33ddfc0eac20bcb6791f",
"sha256:77d05773d5c79f2d3371d81697d54ee1b2c32085ad434ce9de4482e457ecb018",
"sha256:817aab80f7e8fe581696dae7aaeb2ceb0b7ea70ad03c95483c9115970d2a9b00",
"sha256:81f1ea264278fcbe113b9a5840f13a356cb0186e55b52168334124f1cd1bc495",
"sha256:8a88b32ce5b69d18507ffc9f10401833934ebc353c7b30d1e056023c64f0a736",
"sha256:8ff0a7c669ec7cdb899eae7e622211c2dd8725b82655db2b41740d39e3cda466",
"sha256:918c2b553e3c78268b187f70983c9bc6f91e451a4f934827e9c919e03d258bd7",
"sha256:954f1ad73b78ea5ba5a35c89c4a5dfd0f3a06c17926503de19510eb9b3857bde",
"sha256:95a18e1a6af2114dbd9ee4f168ad33070d6317e11bafa28d983cc7b585fe900b",
"sha256:9946ee503962859f1a9e1ad17dff0859269b0cb453686747fe87f00b0e030b34",
"sha256:9a7ecaf90fe9ec8e45c86828f4f183564b33c9514e08667ca59e526fea63893a",
"sha256:a42e6831e82dfa6d16b45f0c98c69e7b0defc64d76213173456355034450c414",
"sha256:b01dce097cf6f145da131a53d4cce7f42e0bfa9ae161dd171a423f7970d296d0",
"sha256:b5deafb4901618b3f98e8df7099cd11edd0d1e6856912647e28968b803de0dae",
"sha256:b67d6e626caa571fb53accaac2fba003ef4f7317cb3481e9ab99dad6e89a70d6",
"sha256:c1e8edc49b32483cd5d2d015f343e16be7dfab89f4aaf66b0fa6827ab356880d",
"sha256:c621f05859caed5c0aab032888a3d3bde2cae3988ca151113cbecf262adad976",
"sha256:ce54965a94673a0ebda25e7c3a05bf1aa74fd78cc452a1a710b704bf73fb8402",
"sha256:d8efdda920988bcade542f53a2890751ff680474d548f32df919a35a21404e3f",
"sha256:dc7b9f55c2f72c13b2328b8a870ff585c993ba1b5c155ece5c9d3216fa4b18f6",
"sha256:dd801375f19a6e1f021dabd8b1714f2fdb91cbc835cd13b5dd0bd7e9860392d7",
"sha256:f342057422d6bcfdd4996e34cd5c7f78f7e500112f64b113f334cdfc6a0c593d",
"sha256:f696828784ab2c07b127bfd2f2d513f47ec58924c29cff5b19806ac37acee31c",
"sha256:fdb2686eb01f670cdc6c43f092e333ff08c1cf0b646da5256c1237dc4ceef4ae"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.4"
},
"sqlalchemy-utils": {
"hashes": [
"sha256:4c7098d4857d5cad1248bf7cd940727aecb75b596a5574b86a93b37079929520",
"sha256:af803089a7929803faeb6173b90f29d1a67ad02f1d1e732f40b054a8eb3c7370"
],
"index": "pypi",
"version": "==0.40.0"
},
"typing-extensions": {
"hashes": [
"sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
"sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
],
"markers": "python_version >= '3.7'",
"version": "==4.5.0"
},
"visitor": {
"hashes": [
"sha256:2c737903b2b6864ebc6167eef7cf3b997126f1aa94bdf590f90f1436d23e480a"
],
"version": "==0.1.3"
},
"werkzeug": {
"hashes": [
"sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe",
"sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"
],
"markers": "python_version >= '3.7'",
"version": "==2.2.3"
},
"wtforms": {
"hashes": [
"sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc",
"sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b"
],
"markers": "python_version >= '3.7'",
"version": "==3.0.1"
}
},
"develop": {}
}

0
backend/README.md Normal file
View File

View File

@ -19,3 +19,4 @@ class Config(object):
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['postmaster@wpgcommunity.net'] ADMINS = ['postmaster@wpgcommunity.net']
POSTS_PER_PAGE = 10

View File

@ -8,8 +8,8 @@ Create Date: 2022-02-20 16:33:01.900378
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from app import db from src import db
from app.utils.view_utils import selectable_price_per_amount_view, selectable_bought_with_prices_view from src.utils.view_utils import selectable_price_per_amount_view, selectable_bought_with_prices_view
from sqlalchemy_utils import create_view from sqlalchemy_utils import create_view
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
@ -20,6 +20,7 @@ depends_on = None
def upgrade(): def upgrade():
op.rename_table("item_receipt", "receipt_item")
metadata = sa.MetaData() metadata = sa.MetaData()
create_view('price_per_amount', create_view('price_per_amount',
selectable_price_per_amount_view(), selectable_price_per_amount_view(),
@ -36,3 +37,4 @@ def downgrade():
with db.engine.connect() as con: with db.engine.connect() as con:
con.execute("DROP VIEW bought_with_prices;") con.execute("DROP VIEW bought_with_prices;")
con.execute("DROP VIEW price_per_amount;") con.execute("DROP VIEW price_per_amount;")
op.rename_table("receipt_item", "item_receipt")

View File

@ -0,0 +1,53 @@
"""establishment candidate table
Revision ID: 610854a6e2a0
Revises: 2be4d1ae5493
Create Date: 2023-03-18 13:36:12.953900
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '610854a6e2a0'
down_revision = '2be4d1ae5493'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('establishment_candidate',
sa.Column('user', sa.BigInteger(), nullable=False),
sa.Column('establishment', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(['establishment'], ['establishment.id'], ),
sa.ForeignKeyConstraint(['user'], ['user.id'], ),
sa.PrimaryKeyConstraint('user', 'establishment')
)
op.create_table('payment',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('token', sa.String(length=15), nullable=False),
sa.Column('date', sa.Date(), server_default=sa.text('now()'), nullable=False),
sa.Column('amount', sa.BigInteger(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['token'], ['login_token.token'], ),
sa.PrimaryKeyConstraint('id', 'token')
)
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.alter_column('token',
existing_type=sa.VARCHAR(length=15),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.alter_column('token',
existing_type=sa.VARCHAR(length=15),
nullable=True)
op.drop_table('payment')
op.drop_table('establishment_candidate')
# ### end Alembic commands ###

View File

@ -1,5 +1,5 @@
from app import create_app, db from src import create_app, db
from app.models import * from src.models import Bought, Establishment, Item, LoginToken, Receipt, User
app = create_app() app = create_app()

View File

@ -42,17 +42,21 @@ def create_app(config_class=Config):
mail.init_app(app) mail.init_app(app)
migrate.init_app(app, db, render_as_batch=True) migrate.init_app(app, db, render_as_batch=True)
from app.auth import bp as auth_bp from src.main import bp as main_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp, url_prefix='/error')
from app.main import bp as main_bp
app.register_blueprint(main_bp) app.register_blueprint(main_bp)
from app.api import bp as api_bp from src.auth import bp as auth_bp
app.register_blueprint(api_bp, url_prefix="/api") app.register_blueprint(auth_bp)
from app.receipts import bp as receipts_bp from src.errors import bp as error_bp
app.register_blueprint(receipts_bp, url_prefix='/receipts') app.register_blueprint(error_bp)
from src.api import bp as api_bp
app.register_blueprint(api_bp)
from src.receipts import bp as receipts_bp
app.register_blueprint(receipts_bp)
from src.item import bp as item_bp
app.register_blueprint(item_bp)
from src.establishment import bp as establishment_bp
app.register_blueprint(establishment_bp)
return app return app
from app import models from src.models import *

View File

@ -0,0 +1,6 @@
from flask import Blueprint
bp = Blueprint('api', __name__, url_prefix='/api')
from src.api.v1 import bp as bp_v1
bp.register_blueprint(bp_v1)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('v1', __name__, url_prefix='/v1')
from src.api.v1 import routes

View File

@ -1,11 +1,11 @@
from app import LOGGER from src import LOGGER
from app.main import bp from src.api import bp
from app.models import LoginToken from src.models.login_token import LoginToken
from app.utils import database_utils from src.utils import database_utils
from flask import abort, request from flask import abort, request
from flask.json import jsonify from flask.json import jsonify
@bp.route('/auth') @bp.route('/')
def token_authorization(): def token_authorization():
LOGGER.debug("Token Login") LOGGER.debug("Token Login")
if not request.json or 'login' not in request.json: if not request.json or 'login' not in request.json:

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__, url_prefix='/auth')
from src.auth import forms, routes

View File

@ -1,6 +1,6 @@
from flask import current_app from flask import current_app
from app.utils.email import send_email from src.utils.email import send_email
from app.utils.routes_utils import render_custom_template as render_template from src.utils.routes_utils import render_custom_template as render_template
def send_password_reset_email(user): def send_password_reset_email(user):

View File

@ -1,4 +1,4 @@
from app.models import User from src.models.user import User
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import BooleanField, PasswordField, StringField, SubmitField from wtforms import BooleanField, PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError from wtforms.validators import DataRequired, Email, EqualTo, ValidationError

View File

@ -1,9 +1,9 @@
from app import db from src import db
from app.auth import bp from src.auth import bp
from app.auth.email import send_password_reset_email from src.auth.email import send_password_reset_email
from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ResetPasswordRequestForm from src.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ResetPasswordRequestForm
from app.models import User from src.models.user import User
from app.utils.routes_utils import render_custom_template as render_template from src.utils.routes_utils import render_custom_template as render_template
from flask import flash, redirect, request, url_for from flask import flash, redirect, request, url_for
from flask_login import current_user, login_user, logout_user from flask_login import current_user, login_user, logout_user
from werkzeug.urls import url_parse from werkzeug.urls import url_parse

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('error', __name__, url_prefix='/error')
from src.errors import handlers

View File

@ -1,5 +1,5 @@
from app import db from src import db
from app.errors import bp from src.errors import bp
from flask import render_template from flask import render_template
@bp.app_errorhandler(403) @bp.app_errorhandler(403)

View File

@ -0,0 +1,11 @@
from flask import Blueprint
bp = Blueprint('establishment', __name__, url_prefix='/establishment')
from src.establishment.new import bp as bp_new
bp.register_blueprint(bp_new)
from src.establishment.list import bp as bp_list
bp.register_blueprint(bp_list)
from src.establishment.overview import bp as bp_overview
bp.register_blueprint(bp_overview)
from src.establishment.candidates import bp as bp_candidates
bp.register_blueprint(bp_candidates)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint("candidates", __name__, url_prefix='candidates')
from src.establishment.candidates import routes

View File

@ -0,0 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import HiddenField, SubmitField
class EvaluateCandidateForm(FlaskForm):
candidate_id = HiddenField("X")
accept = SubmitField("Akzeptieren", render_kw={"class": "btn btn-success mt-3"})
deny = SubmitField("Ablehnen", render_kw={"class": "btn btn-danger mt-3"})

View File

@ -0,0 +1,26 @@
from flask import abort, redirect, url_for
from flask_login import current_user, login_required
from src import db, LOGGER
from src.establishment.candidates import bp
from src.establishment.candidates.forms import EvaluateCandidateForm
from src.models import Establishment, EstablishmentCandidate, LoginToken, User
from src.utils.routes_utils import render_custom_template as render_template
from src.utils.database_utils import generate_token
@bp.route("/<establishment_id>", methods=["GET", "POST"])
@login_required
def candidates(establishment_id):
establishment = Establishment.query.get_or_404(establishment_id)
if(current_user == establishment.User):
form = EvaluateCandidateForm()
establishment_candidates = EstablishmentCandidate.query.filter_by(establishment = establishment.id).all()
if(form.validate_on_submit()):
if(form.accept.data):
login_token = LoginToken(Establishment = establishment, User = User.query.get(form.candidate_id.data), token = generate_token())
db.session.add(login_token)
establishment_candidate = EstablishmentCandidate.query.filter_by(establishment = establishment.id, user = form.candidate_id.data).first()
db.session.delete(establishment_candidate)
db.session.commit()
return redirect(url_for('.candidates', establishment_id = establishment_id))
return render_template("establishment/candidates/candidates.html", establishment_candidates=establishment_candidates, form=form)
abort(403)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint("list", __name__, url_prefix='list')
from src.establishment.list import routes

View File

@ -0,0 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import HiddenField, SubmitField
class JoinEstablishmentForm(FlaskForm):
id = HiddenField("X")
submit = SubmitField("Anfragen")

View File

@ -0,0 +1,33 @@
# from src import db, LOGGER
from flask import abort, current_app, redirect, request, url_for
from flask_login import current_user
from src import db
from src.models import Establishment
from src.establishment.list import bp
from src.establishment.list.forms import JoinEstablishmentForm
from src.establishment.list.utils import backend_validation
from src.models import EstablishmentCandidate
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/show_establishments', methods=["GET", "POST"])
def show_establishments():
page = request.args.get('page', 1, type=int)
establishments = Establishment.query.order_by(Establishment.id.asc()).paginate(
page=page, per_page=current_app.config['POSTS_PER_PAGE'], error_out=False)
next_url = url_for(".show_establishments",
page=establishments.next_num) if establishments.has_next else None
prev_url = url_for(".show_establishments",
page=establishments.prev_num) if establishments.has_prev else None
if current_user.is_authenticated:
form = JoinEstablishmentForm()
if (form.validate_on_submit()):
if (backend_validation(form)):
establishment_candidate = EstablishmentCandidate(
user=current_user.id, establishment=form.id.data)
db.session.add(establishment_candidate)
db.session.commit()
return redirect(url_for('establishment.list.show_establishments'))
return abort(403)
return render_template("establishment/list/establishment_list.html", establishments=establishments.items, form=form, next_url=next_url, prev_url=prev_url)
return render_template("establishment/list/establishment_list.html", establishments=establishments.items, next_url=next_url, prev_url=prev_url)

View File

@ -0,0 +1,7 @@
from flask_login import current_user
from src.models import Establishment
def backend_validation(join_establishment_form):
establishment = Establishment.query.get(join_establishment_form.id.data)
return not (establishment.LoginToken.filter_by(user=current_user.id).first() or current_user.EstablishmentCandidate.filter_by(establishment=join_establishment_form.id.data).first())

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint("new", __name__, url_prefix='new')
from src.establishment.new import forms, routes

View File

@ -0,0 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NewEstablishmentForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,24 @@
from src import db, LOGGER
from src.models import Establishment, LoginToken
from src.establishment.new import bp
from src.establishment.new.forms import NewEstablishmentForm
from src.utils import database_utils
from src.utils.routes_utils import render_custom_template as render_template
from flask import redirect, url_for
from flask_login import current_user, login_required
@bp.route('/create_establishment', methods=['GET', 'POST'])
@login_required
def create_new_establishment():
form = NewEstablishmentForm()
if form.validate_on_submit():
LOGGER.debug("valid form")
establishment = Establishment(name=form.name.data, owner=current_user.id)
db.session.add(establishment)
db.session.commit()
new_token = database_utils.generate_token()
login_token = LoginToken(user=current_user.id, establishment=establishment.id, token=new_token)
db.session.add(login_token)
db.session.commit()
return redirect(url_for('main.index'))
return render_template('establishment/new/new_establishment.html', form=form)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('overview', __name__, url_prefix='/overview')
from src.establishment.overview import routes

View File

@ -0,0 +1,11 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NewCategoryForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,36 @@
from flask import abort, request
from flask.json import jsonify
from flask_login import current_user, login_required
from src import LOGGER
from src.establishment.overview import bp
from src.models import Establishment
from src.utils import view_utils, database_utils
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/<establishment_id>', methods=['GET'])
@login_required
def get_report_from_user(establishment_id):
establishment = Establishment.query.filter_by(id=int(establishment_id)).first_or_404()
if current_user.is_anonymous:
abort(403)
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(**request.args, **{"establishment": establishment_id})
LOGGER.debug(f"Results received.")
# LOGGER.debug(str(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("establishment/overview/overview.html", results=result_list, establishment=Establishment.query.get(int(establishment_id)))

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('item', __name__, url_prefix='/item')
from src.item.new import bp as bp_new_item
bp.register_blueprint(bp_new_item)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('new', __name__, url_prefix='/new')
from src.item.new import forms, routes

View File

@ -1,17 +1,18 @@
from app.models import Brand, Category from src.models.brand import Brand
from src.models.category import Category
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import DateField, FloatField, IntegerField, SelectField, SelectMultipleField, StringField, SubmitField from wtforms import DateField, FloatField, IntegerField, SelectField, SelectMultipleField, StringField, SubmitField
from wtforms.validators import DataRequired, Optional from wtforms.validators import DataRequired, Optional
class NewItemForm(FlaskForm): class NewItemForm(FlaskForm):
id = IntegerField("Product EAN", validators=[DataRequired()]) id = IntegerField("Product EAN", validators=[DataRequired()], render_kw={"class": "form-control"})
name = StringField("Name", validators=[DataRequired()]) name = StringField("Name", validators=[DataRequired()], render_kw={"class": "form-control"})
description = StringField("Description", validators=[DataRequired()]) description = StringField("Description", validators=[DataRequired()], render_kw={"class": "form-control"})
date = DateField("Insert Date", validators=[DataRequired()]) date = DateField("Insert Date", validators=[DataRequired()], render_kw={"class": "form-control"})
price_change = FloatField("Price", validators=[DataRequired()]) price_change = FloatField("Price", validators=[DataRequired()], render_kw={"class": "form-control"})
amount_change = IntegerField("Amount", validators=[Optional()]) amount_change = IntegerField("Amount", validators=[Optional()], render_kw={"class": "form-control"})
category = SelectMultipleField("Categories", choices=[], validators=[Optional()]) category = SelectMultipleField("Categories", choices=[], validators=[Optional()], render_kw={"class": "form-control"})
brand = SelectField("Brand", choices=[], validators=[DataRequired()]) brand = SelectField("Brand", choices=[], validators=[DataRequired()], render_kw={"class": "form-control"})
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"}) submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
@classmethod @classmethod
@ -20,18 +21,3 @@ class NewItemForm(FlaskForm):
form.category.choices = [(c.id, c.name) for c in Category.query.order_by("name").all()] form.category.choices = [(c.id, c.name) for c in Category.query.order_by("name").all()]
form.brand.choices = [(b.id, b.name) for b in Brand.query.order_by("name").all()] form.brand.choices = [(b.id, b.name) for b in Brand.query.order_by("name").all()]
return form return form
class NewCategoryForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewEstablishmentForm(FlaskForm):
establishment_name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class JoinEstablishmentForm(FlaskForm):
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,38 @@
from datetime import date
from flask import abort, redirect, url_for
from flask_login import current_user, login_required
from src import db, LOGGER
from src.models import AmountChange, Brand, Item, PriceChange
from src.item.new import bp
from src.item.new.forms import NewItemForm
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/new_item', methods=['GET', 'POST'])
@login_required
def new_item():
if current_user.is_anonymous:
abort(403)
form = NewItemForm.new()
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
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('main.index'))
return render_template('item/new/new_item.html', form=form)

View File

@ -2,4 +2,4 @@ from flask import Blueprint
bp = Blueprint('main', __name__) bp = Blueprint('main', __name__)
from app.main import forms, routes from src.main import forms, routes

11
backend/src/main/forms.py Normal file
View File

@ -0,0 +1,11 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NewCategoryForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,7 @@
from src.main import bp
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/')
@bp.route('/index')
def index():
return render_template("base.html")

View File

@ -0,0 +1,14 @@
from src.models.amount_change import AmountChange
from src.models.bought import Bought
from src.models.brand import Brand
from src.models.category import Category
from src.models.establishment import Establishment
from src.models.establishment_candidate import EstablishmentCandidate
from src.models.item_category import item_category
from src.models.item import Item
from src.models.login_token import LoginToken
from src.models.payment import Payment
from src.models.price_change import PriceChange
from src.models.receipt_item import ReceiptItem
from src.models.receipt import Receipt
from src.models.user import User

View File

@ -0,0 +1,10 @@
from src import db
from datetime import date
class AmountChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
amount = db.Column(db.SmallInteger, nullable=False, server_default=str(1))
def __repr__(self) -> str:
return f"<Amount_Change {self.item} ({self.date})>"

View File

@ -0,0 +1,11 @@
from src import db
class Bought(db.Model):
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True)
amount = db.Column(db.SmallInteger, nullable=False)
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
def __repr__(self) -> str:
return f"<Bought Object>"

View File

@ -0,0 +1,10 @@
from src import db
class Brand(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False)
Item = db.relationship("Item", backref='Brand', lazy='dynamic')
def __repr__(self) -> str:
return f"<Brand {self.id} ({self.name})>"

View File

@ -0,0 +1,11 @@
from src import db
from src.models.item_category import item_category
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False, unique=True)
Item = db.relationship("Item", secondary=item_category, lazy="dynamic", back_populates="Category")
def __repr__(self) -> str:
return f"<Category {self.id} ({self.name})>"

View File

@ -0,0 +1,14 @@
from src import db
class Establishment(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
name = db.Column(db.String(64), nullable=False)
owner = db.Column(db.ForeignKey('user.id'), nullable=False)
LoginToken = db.relationship("LoginToken", backref='Establishment', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="Establishment,LoginToken,Bought")
EstablishmentCandidate = db.relationship("EstablishmentCandidate", backref='Establishment', lazy='dynamic')
def __repr__(self) -> str:
return f"<Establishment {self.id} ({self.name})>"

View File

@ -0,0 +1,8 @@
from src import db
class EstablishmentCandidate(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True)
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True)
def __repr__(self) -> str:
return f"<EstablishmentCandidate {self.user} ({self.establishment})>"

View File

@ -0,0 +1,17 @@
from src import db
from src.models.item_category import item_category
class Item(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
name = db.Column(db.String(64), nullable=False)
brand = db.Column(db.ForeignKey('brand.id'), nullable=False, server_onupdate=db.FetchedValue())
description = db.Column(db.Text, nullable=False)
AmountChange = db.relationship("AmountChange", backref='Item', lazy='dynamic')
Bought = db.relationship("Bought", backref='Item', lazy='dynamic')
Category = db.relationship("Category", secondary=item_category, lazy="dynamic", back_populates="Item")
PriceChange = db.relationship("PriceChange", backref='Item', lazy='dynamic')
ReceiptItem = db.relationship("ReceiptItem", backref='Item', lazy='dynamic')
def __repr__(self) -> str:
return f"<Item {self.id} ({self.name})>"

View File

@ -0,0 +1,6 @@
from src import db
item_category = db.Table("item_category",
db.Column("item", db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue()),
db.Column("category", db.ForeignKey("category.id"), primary_key=True, server_onupdate=db.FetchedValue())
)

View File

@ -0,0 +1,12 @@
from src import db
class LoginToken(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True, server_onupdate=db.FetchedValue())
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
token = db.Column(db.String(15), nullable=False, unique=True)
Payment = db.relationship("Payment", backref='LoginToken', lazy='dynamic')
Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic')
def __repr__(self) -> str:
return f"<LoginToken {self.token}>"

View File

@ -0,0 +1,10 @@
from src import db
class Payment(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, nullable=False, server_default=db.func.now())
amount = db.Column(db.BigInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"<Payment {self.id}>"

View File

@ -0,0 +1,10 @@
from src import db
from datetime import date
class PriceChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
price = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Price_Change {self.item} ({self.date})>"

View File

@ -0,0 +1,12 @@
from src import db
class Receipt(db.Model):
id = db.Column(db.Numeric(precision=24, scale=0), primary_key=True, autoincrement=False)
date = db.Column(db.Date, nullable=False)
from_user = db.Column(db.ForeignKey("login_token.token"), server_onupdate=db.FetchedValue())
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
ReceiptItem = db.relationship("ReceiptItem", backref='Receipt', lazy='dynamic')
def __repr__(self) -> str:
return f"<Receipt {self.id}>"

View File

@ -0,0 +1,9 @@
from src import db
class ReceiptItem(db.Model):
receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue())
amount = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<ReceiptItem {self.receipt}: {self.item}>"

View File

@ -0,0 +1,45 @@
import jwt
from src import db, login
from flask import current_app
from flask_login import UserMixin
from time import time
from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin, db.Model):
id = db.Column(db.BigInteger, primary_key=True)
email = db.Column(db.String(255), nullable=False, unique=True)
password_hash = db.Column(db.String(128), nullable=False)
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="User,LoginToken")
EstablishmentCandidate = db.relationship("EstablishmentCandidate", backref='User', lazy='dynamic')
Establishment = db.relationship("Establishment", backref="User", lazy="dynamic")
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
def __repr__(self) -> str:
return f"<User {self.id} ({self.email})>"
@login.user_loader
def load_user(id):
return User.query.get(int(id))

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('receipts', __name__, url_prefix='/receipts')
from src.receipts import forms, routes

View File

@ -14,3 +14,11 @@ class UploadReceiptForm(FlaskForm):
class CheckItemsForm(FlaskForm): class CheckItemsForm(FlaskForm):
items = MultiCheckboxField("Items") items = MultiCheckboxField("Items")
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"}) submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
#TODO create new() method which loads the form for a specific receipt
@classmethod
def new(cls, itemArray):
"""
"""
form = cls()
return form

View File

@ -1,11 +1,12 @@
from app import db, LOGGER from src import db, LOGGER
from app.receipts import bp from src.models.receipt import Receipt
from app.receipts.forms import CheckItemsForm, UploadReceiptForm from src.models.login_token import LoginToken
from app.models import Receipt, LoginToken from src.receipts import bp
from app.utils.routes_utils import render_custom_template as render_template from src.receipts.forms import CheckItemsForm, UploadReceiptForm
from src.utils.pdf_receipt_parser import PDFReceipt
from src.utils.routes_utils import render_custom_template as render_template
from flask import abort, request, url_for from flask import abort, request, url_for
from flask_login import current_user, login_required from flask_login import current_user, login_required
from app.utils.pdf_receipt_parser import PDFReceipt
PDFDir = "./" PDFDir = "./"

View File

@ -0,0 +1,126 @@
{% extends "bootstrap/base.html" %}
{% block title %}
{% if title %}
{{ title }}
{% else %}
CostHive
{% endif %}
{% endblock %}
{% block metas %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
{% block styles %}
<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={{ url_for('static', filename="sidebars.css")}}>
{% endblock %}
{% block navbar %}
<main>
<div class="flex-shrink-0 p-3 bg-black bg-opacity-10 scrollbar-primary h-100 position-fixed" style="width: 280px;">
<a href="/"
class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom border-dark">
<svg class="bi me-2" width="30" height="24">
<use xlink:href="#bootstrap"></use>
</svg>
<span class="fs-5 fw-semibold">
{% if title %}
{{ title }}
{% else %}
CostHive
{% endif %}
</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 (WIP)
</button>
<div class="collapse show" id="home-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<li><a href="{{ url_for('establishment.list.show_establishments') }}"
class="link-dark rounded">Einrichtungen</a></li>
<li><a href="#" class="link-dark rounded">Inventar</a></li>
</ul>
</div>
</li>
{% if current_user.is_authenticated %}
<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">
Übersicht
</button>
<div class="collapse" id="dashboard-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
{% if current_user_establishments %}
{% for establishment in current_user_establishments %}
<li>
<a href="{{ url_for('establishment.overview.get_report_from_user', establishment_id=establishment.id) }}"
class="link-dark rounded">{{ establishment.name }}</a>
</li>
{% endfor %}
{% endif %}
<li>
<a href="{{ url_for('establishment.new.create_new_establishment') }}"
class="link-dark rounded"> Neu</a>
</li>
</ul>
</div>
</li>
{% endif %}
<li class="border-top border-dark 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">Settings</a></li> -->
{% if current_user.is_authenticated %}
<!-- <li><a href="#" class="link-dark rounded">Profile</a></li> -->
<li><a href={{ url_for('auth.web_logout') }} class="link-dark rounded">Sign out</a></li>
{% else %}
<li><a href={{ url_for('auth.web_register') }} class="link-dark rounded">Register</a></li>
<li><a href={{ url_for('auth.web_login') }} class="link-dark rounded">Sign in</a></li>
{% endif %}
</ul>
</div>
</li>
</ul>
</div>
{% endblock %}
{% block content %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div style="width: 280px;"></div>
<div class="container">
<div class="row my-3"></div>
<div class="row-md-3">
{% block app_content %}{% endblock %}
</div>
<div class="row my-3"></div>
</div>
</main>
{% endblock %}
{% block scripts %}
<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>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
{% endblock %}

View File

@ -0,0 +1,6 @@
<form action="" method="post">
{{ form.csrf_token }}
{{ form.candidate_id(value=candidate.user) }}
{{ form.accept }}
{{ form.deny }}
</form>

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<table class="table">
<tbody>
{% for candidate in establishment_candidates %}
<tr>
<td>{{ candidate.User.email }}</td>
<td>{% include "establishment/candidates/_candidate_evaluation.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "utils/general/_page_navigation.html" %}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
{% if current_user.is_authenticated %}
<th scope="col">Anfrage senden</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for establishment in establishments %}
<tr>
<th scope="row">{{ establishment.id }}</th>
<td>{{ establishment.name }}</td>
{% if current_user.is_authenticated %}
<td>{% include 'establishment/list/establishment_request.html' %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% include "utils/general/_page_navigation.html" %}
{% endblock %}

View File

@ -0,0 +1,11 @@
<form action="" method="post">
{{ form.csrf_token }}
{{ form.id(value=establishment.id) }}
{% if current_user.EstablishmentCandidate.filter_by(establishment = establishment.id).first() %}
{{ form.submit(value="Bereits angefragt", disabled=True) }}
{% elif establishment.LoginToken.filter_by(user = current_user.id).first() %}
{{ form.submit(value="Bereits Mitglied", disabled=True) }}
{% else %}
{{ form.submit }}
{% endif %}
</form>

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
{% include 'establishment/new/new_establishment_form.html' %}
{% endblock %}

View File

@ -0,0 +1,5 @@
<form action="" method="post">
{{ form.hidden_tag() }}
{{ wtf.form_field(form.name, class=form.name.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
</form>

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block app_content %}
{% if establishment %}
{% if current_user.id == establishment.owner %}
<button type="button" class="btn btn-outline-dark px-2" data-bs-toggle="button" autocomplete="off"
onclick="window.location.href='{{ url_for('establishment.overview.get_report_from_user', establishment_id=establishment.id) }}'">
Abrechnung
</button>
{% endif %}
{% endif %}
{% for user in results %}
<div class="card">
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
<div class="card-header">
<h3>{{ user.email }}: {{ user.sum/100 }} €</h3>
</div>
</button>
<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>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
{% include 'item/new/new_item_form.html' %}
{% endblock %}

View File

@ -0,0 +1,13 @@
{% from 'utils/form/_render_field.html' import render_field %}
<form action="" method="post">
{{ form.hidden_tag() }}
{{ render_field(form.id) }}
{{ render_field(form.name) }}
{{ render_field(form.description) }}
{{ render_field(form.date) }}
{{ render_field(form.price_change) }}
{{ render_field(form.amount_change) }}
{{ render_field(form.category) }}
{{ render_field(form.brand) }}
{{ form.submit }}
</form>

View File

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block app_content %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% macro render_field(field) %}
<div class="form-group">
{{ field.label }} {{ field(**kwargs)|safe }} {% if field.errors %}
<ul class="errors">
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}

Some files were not shown because too many files have changed in this diff Show More