Merge branch 'feat_receipt_upload'
This commit is contained in:
commit
8069a794e2
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Ignore pycaches
|
||||||
|
|
||||||
|
logs
|
||||||
|
__pycache__
|
||||||
|
.flaskenv
|
||||||
|
.env*
|
||||||
|
.flaskenv*
|
||||||
|
!.env.project
|
||||||
|
!.env.vault
|
||||||
12
.env.vault
Normal file
12
.env.vault
Normal 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
226
.gitignore
vendored
@ -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
|
||||||
28
Dockerfile
28
Dockerfile
@ -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"]
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
bp = Blueprint('api', __name__)
|
|
||||||
|
|
||||||
from app.api import routes
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
bp = Blueprint('auth', __name__)
|
|
||||||
|
|
||||||
from app.auth import forms, routes
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
bp = Blueprint('errors', __name__)
|
|
||||||
|
|
||||||
from app.errors import handlers
|
|
||||||
@ -1,116 +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:
|
|
||||||
abort(400)
|
|
||||||
if not LoginToken.query.filter_by(token=request.json['login']).first():
|
|
||||||
abort(403)
|
|
||||||
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('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)
|
|
||||||
143
app/models.py
143
app/models.py
@ -1,143 +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=True, unique=True)
|
|
||||||
paid = db.Column(db.BigInteger, nullable=False, server_default=str(0))
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
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))
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% import 'bootstrap/wtf.html' as wtf %}
|
|
||||||
|
|
||||||
{% block app_content %}
|
|
||||||
<h1>Register</h1>
|
|
||||||
<form action="" method="post">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
<p>
|
|
||||||
{{ wtf.form_field(form.email) }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ wtf.form_field(form.password) }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ wtf.form_field(form.password2) }}
|
|
||||||
</p>
|
|
||||||
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,116 +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><a href="{{ url_for('main.get_report_from_user', establishment=establishment.id) }}" class="link-dark rounded">{{ establishment.name }}</a></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 %}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block app_content %}
|
|
||||||
<form action="" method="post">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
<div class="row">{{form.id.label}}</div>
|
|
||||||
<div class="row">{{form.id()}}</div>
|
|
||||||
<div class="row">{{form.name.label}}</div>
|
|
||||||
<div class="row">{{form.name()}}</div>
|
|
||||||
<div class="row">{{form.description.label}}</div>
|
|
||||||
<div class="row">{{form.description()}}</div>
|
|
||||||
<div class="row">{{form.date.label}}</div>
|
|
||||||
<div class="row">{{form.date()}}</div>
|
|
||||||
<div class="row">{{form.price_change.label}}</div>
|
|
||||||
<div class="row">{{form.price_change()}}</div>
|
|
||||||
<div class="row">{{form.amount_change.label}}</div>
|
|
||||||
<div class="row">{{form.amount_change()}}</div>
|
|
||||||
<div class="row">{{form.category.label}}</div>
|
|
||||||
<div class="row">{{form.category()}}</div>
|
|
||||||
<div class="row">{{form.brand.label}}</div>
|
|
||||||
<div class="row">{{form.brand()}}</div>
|
|
||||||
<div class="row">{{form.submit()}}</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@ -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
25
backend/Pipfile
Normal 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
500
backend/Pipfile.lock
generated
Normal 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
0
backend/README.md
Normal file
@ -6,12 +6,12 @@ load_dotenv(os.path.join(basedir, '.env'))
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') or "MY_5€cr37_K€Y"
|
SECRET_KEY = os.environ.get('SECRET_KEY') or "s0m37h!n6-obfu5c471ng"
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace(
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace(
|
||||||
'postgres://', 'postgresql://') or \
|
'postgres://', 'postgresql://') or \
|
||||||
(f"postgresql://{os.environ.get('DATABASE_USER', 'scan2kasse')}:{os.environ.get('DATABASE_PASS', 'asdf1337')}"
|
(f"postgresql://{os.environ.get('DATABASE_USER', 'scan2kasse')}:{os.environ.get('DATABASE_PASS', 'asdf1337')}"
|
||||||
f"@{os.environ.get('DATABASE_HOST', 'localhost')}:{os.environ.get('DATABASE_PORT', '5432')}"
|
f"@{os.environ.get('DATABASE_HOST', 'localhost')}:{os.environ.get('DATABASE_PORT', '5432')}"
|
||||||
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
|
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||||
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
|
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
|
||||||
@ -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
|
||||||
@ -2,19 +2,25 @@
|
|||||||
keys=root, main
|
keys=root, main
|
||||||
|
|
||||||
[handlers]
|
[handlers]
|
||||||
keys=console, file
|
keys=console, file, void
|
||||||
|
|
||||||
[formatters]
|
[formatters]
|
||||||
keys=stdout
|
keys=stdout
|
||||||
|
|
||||||
[logger_root]
|
[logger_root]
|
||||||
level = DEBUG
|
handlers = void
|
||||||
|
level = CRITICAL
|
||||||
|
|
||||||
[logger_main]
|
[logger_main]
|
||||||
handlers = console, file
|
handlers = console, file
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
qualname = main
|
qualname = main
|
||||||
|
|
||||||
|
[handler_void]
|
||||||
|
class = logging.StreamHandler
|
||||||
|
level = CRITICAL
|
||||||
|
formatter = stdout
|
||||||
|
|
||||||
[handler_console]
|
[handler_console]
|
||||||
class = logging.StreamHandler
|
class = logging.StreamHandler
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
@ -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")
|
||||||
@ -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 ###
|
||||||
@ -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()
|
||||||
|
|
||||||
@ -42,15 +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)
|
|
||||||
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 src.errors import bp as error_bp
|
||||||
|
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 *
|
||||||
6
backend/src/api/__init__.py
Normal file
6
backend/src/api/__init__.py
Normal 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)
|
||||||
5
backend/src/api/v1/__init__.py
Normal file
5
backend/src/api/v1/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('v1', __name__, url_prefix='/v1')
|
||||||
|
|
||||||
|
from src.api.v1 import routes
|
||||||
@ -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:
|
||||||
5
backend/src/auth/__init__.py
Normal file
5
backend/src/auth/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||||
|
|
||||||
|
from src.auth import forms, routes
|
||||||
@ -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):
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from app.models import User
|
from src.models.user import User
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
from wtforms import BooleanField, PasswordField, StringField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
|
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
@ -1,14 +1,14 @@
|
|||||||
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
|
||||||
|
|
||||||
@bp.route(f'/register', methods=['GET', 'POST'])
|
@bp.route('/register', methods=['GET', 'POST'])
|
||||||
def web_register():
|
def web_register():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
@ -22,7 +22,7 @@ def web_register():
|
|||||||
return redirect(url_for('auth.web_login'))
|
return redirect(url_for('auth.web_login'))
|
||||||
return render_template('auth/register.html', title='Register', form=form)
|
return render_template('auth/register.html', title='Register', form=form)
|
||||||
|
|
||||||
@bp.route(f'/login', methods=['GET', 'POST'])
|
@bp.route('/login', methods=['GET', 'POST'])
|
||||||
def web_login():
|
def web_login():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
@ -39,7 +39,7 @@ def web_login():
|
|||||||
return redirect(next_page)
|
return redirect(next_page)
|
||||||
return render_template('auth/login.html', title='Sign In', form=form)
|
return render_template('auth/login.html', title='Sign In', form=form)
|
||||||
|
|
||||||
@bp.route(f'/logout')
|
@bp.route('/logout')
|
||||||
def web_logout():
|
def web_logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
5
backend/src/errors/__init__.py
Normal file
5
backend/src/errors/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('error', __name__, url_prefix='/error')
|
||||||
|
|
||||||
|
from src.errors import handlers
|
||||||
@ -1,7 +1,11 @@
|
|||||||
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)
|
||||||
|
def not_allowed_error(error):
|
||||||
|
return render_template('errors/403.html'), 403
|
||||||
|
|
||||||
@bp.app_errorhandler(404)
|
@bp.app_errorhandler(404)
|
||||||
def not_found_error(error):
|
def not_found_error(error):
|
||||||
return render_template('errors/404.html'), 404
|
return render_template('errors/404.html'), 404
|
||||||
11
backend/src/establishment/__init__.py
Normal file
11
backend/src/establishment/__init__.py
Normal 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)
|
||||||
5
backend/src/establishment/candidates/__init__.py
Normal file
5
backend/src/establishment/candidates/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint("candidates", __name__, url_prefix='candidates')
|
||||||
|
|
||||||
|
from src.establishment.candidates import routes
|
||||||
7
backend/src/establishment/candidates/forms.py
Normal file
7
backend/src/establishment/candidates/forms.py
Normal 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"})
|
||||||
26
backend/src/establishment/candidates/routes.py
Normal file
26
backend/src/establishment/candidates/routes.py
Normal 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)
|
||||||
5
backend/src/establishment/list/__init__.py
Normal file
5
backend/src/establishment/list/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint("list", __name__, url_prefix='list')
|
||||||
|
|
||||||
|
from src.establishment.list import routes
|
||||||
7
backend/src/establishment/list/forms.py
Normal file
7
backend/src/establishment/list/forms.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import HiddenField, SubmitField
|
||||||
|
|
||||||
|
|
||||||
|
class JoinEstablishmentForm(FlaskForm):
|
||||||
|
id = HiddenField("X")
|
||||||
|
submit = SubmitField("Anfragen")
|
||||||
33
backend/src/establishment/list/routes.py
Normal file
33
backend/src/establishment/list/routes.py
Normal 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)
|
||||||
7
backend/src/establishment/list/utils.py
Normal file
7
backend/src/establishment/list/utils.py
Normal 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())
|
||||||
5
backend/src/establishment/new/__init__.py
Normal file
5
backend/src/establishment/new/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint("new", __name__, url_prefix='new')
|
||||||
|
|
||||||
|
from src.establishment.new import forms, routes
|
||||||
7
backend/src/establishment/new/forms.py
Normal file
7
backend/src/establishment/new/forms.py
Normal 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"})
|
||||||
24
backend/src/establishment/new/routes.py
Normal file
24
backend/src/establishment/new/routes.py
Normal 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)
|
||||||
5
backend/src/establishment/overview/__init__.py
Normal file
5
backend/src/establishment/overview/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('overview', __name__, url_prefix='/overview')
|
||||||
|
|
||||||
|
from src.establishment.overview import routes
|
||||||
11
backend/src/establishment/overview/forms.py
Normal file
11
backend/src/establishment/overview/forms.py
Normal 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"})
|
||||||
36
backend/src/establishment/overview/routes.py
Normal file
36
backend/src/establishment/overview/routes.py
Normal 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)))
|
||||||
5
backend/src/item/__init__.py
Normal file
5
backend/src/item/__init__.py
Normal 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)
|
||||||
5
backend/src/item/new/__init__.py
Normal file
5
backend/src/item/new/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('new', __name__, url_prefix='/new')
|
||||||
|
|
||||||
|
from src.item.new import forms, routes
|
||||||
@ -1,18 +1,19 @@
|
|||||||
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")
|
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls):
|
def new(cls):
|
||||||
@ -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"})
|
|
||||||
38
backend/src/item/new/routes.py
Normal file
38
backend/src/item/new/routes.py
Normal 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)
|
||||||
@ -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
11
backend/src/main/forms.py
Normal 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"})
|
||||||
7
backend/src/main/routes.py
Normal file
7
backend/src/main/routes.py
Normal 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")
|
||||||
14
backend/src/models/__init__.py
Normal file
14
backend/src/models/__init__.py
Normal 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
|
||||||
10
backend/src/models/amount_change.py
Normal file
10
backend/src/models/amount_change.py
Normal 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})>"
|
||||||
11
backend/src/models/bought.py
Normal file
11
backend/src/models/bought.py
Normal 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>"
|
||||||
10
backend/src/models/brand.py
Normal file
10
backend/src/models/brand.py
Normal 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})>"
|
||||||
11
backend/src/models/category.py
Normal file
11
backend/src/models/category.py
Normal 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})>"
|
||||||
14
backend/src/models/establishment.py
Normal file
14
backend/src/models/establishment.py
Normal 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})>"
|
||||||
8
backend/src/models/establishment_candidate.py
Normal file
8
backend/src/models/establishment_candidate.py
Normal 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})>"
|
||||||
17
backend/src/models/item.py
Normal file
17
backend/src/models/item.py
Normal 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})>"
|
||||||
6
backend/src/models/item_category.py
Normal file
6
backend/src/models/item_category.py
Normal 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())
|
||||||
|
)
|
||||||
12
backend/src/models/login_token.py
Normal file
12
backend/src/models/login_token.py
Normal 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}>"
|
||||||
10
backend/src/models/payment.py
Normal file
10
backend/src/models/payment.py
Normal 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}>"
|
||||||
10
backend/src/models/price_change.py
Normal file
10
backend/src/models/price_change.py
Normal 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})>"
|
||||||
12
backend/src/models/receipt.py
Normal file
12
backend/src/models/receipt.py
Normal 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}>"
|
||||||
9
backend/src/models/receipt_item.py
Normal file
9
backend/src/models/receipt_item.py
Normal 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}>"
|
||||||
45
backend/src/models/user.py
Normal file
45
backend/src/models/user.py
Normal 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))
|
||||||
5
backend/src/receipts/__init__.py
Normal file
5
backend/src/receipts/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('receipts', __name__, url_prefix='/receipts')
|
||||||
|
|
||||||
|
from src.receipts import forms, routes
|
||||||
24
backend/src/receipts/forms.py
Normal file
24
backend/src/receipts/forms.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from flask_wtf.file import FileAllowed, FileField, FileRequired
|
||||||
|
from wtforms import BooleanField, SelectMultipleField, SubmitField, widgets
|
||||||
|
from wtforms.validators import Optional
|
||||||
|
|
||||||
|
class MultiCheckboxField(SelectMultipleField):
|
||||||
|
widget = widgets.ListWidget(prefix_label=False)
|
||||||
|
option_widget = widgets.CheckboxInput()
|
||||||
|
|
||||||
|
class UploadReceiptForm(FlaskForm):
|
||||||
|
pdfReceipt = FileField("PDF", validators=[FileRequired(), FileAllowed(["pdf"], "Invalid Format, must be .pdf")])
|
||||||
|
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
|
||||||
|
|
||||||
|
class CheckItemsForm(FlaskForm):
|
||||||
|
items = MultiCheckboxField("Items")
|
||||||
|
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
|
||||||
62
backend/src/receipts/routes.py
Normal file
62
backend/src/receipts/routes.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from src import db, LOGGER
|
||||||
|
from src.models.receipt import Receipt
|
||||||
|
from src.models.login_token import LoginToken
|
||||||
|
from src.receipts import bp
|
||||||
|
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_login import current_user, login_required
|
||||||
|
|
||||||
|
PDFDir = "./"
|
||||||
|
|
||||||
|
@bp.route('/upload_receipt', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def upload_receipt():
|
||||||
|
"""Upload of a receipt."""
|
||||||
|
if current_user.is_anonymous:
|
||||||
|
abort(403)
|
||||||
|
if "establishment" in request.args:
|
||||||
|
if LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first():
|
||||||
|
form = UploadReceiptForm()
|
||||||
|
LOGGER.debug(form.pdfReceipt.data)
|
||||||
|
if form.is_submitted():
|
||||||
|
LOGGER.debug("submitted")
|
||||||
|
if form.validate():
|
||||||
|
LOGGER.debug("valid")
|
||||||
|
else:
|
||||||
|
LOGGER.debug(form.errors)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
receipt = PDFReceipt(form.pdfReceipt.data)
|
||||||
|
dbReceipt = Receipt(id = receipt.id, date = receipt.date,
|
||||||
|
from_user = LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first().token)
|
||||||
|
form.pdfReceipt.data.save(PDFDir + f"{str(receipt.date)}_{receipt.id}.pdf")
|
||||||
|
db.session.add(dbReceipt)
|
||||||
|
db.session.commit()
|
||||||
|
return receipt.text.replace("\n", "<br>")
|
||||||
|
return render_template("receipts/upload.html", form = form)
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@bp.route('/confirm_receipt', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def confirm_receipt_items():
|
||||||
|
"""Check items from a receipt if they should be accounted for payment."""
|
||||||
|
if "receipt" in request.args:
|
||||||
|
receipt_details = Receipt.query.get(request.args['receipt'])
|
||||||
|
if current_user.is_anonymous and current_user.id == receipt_details.LoginToken.Establishment.owner:
|
||||||
|
receipt = PDFReceipt._getPDFReceiptFromFile(PDFDir + f"{receipt.date}_{receipt.id}.pdf")
|
||||||
|
form = CheckItemsForm()
|
||||||
|
# TODO: Precheck if items are already in database. If yes, check if item is present only once or multiple
|
||||||
|
# times and provide dropdown menu if necessary. If not, provide input field.
|
||||||
|
temp_choices = []
|
||||||
|
for item in receipt.items:
|
||||||
|
match item:
|
||||||
|
case {"itemname": itemname, "price": price}:
|
||||||
|
temp_choices.append((itemname.replace(" ", "_"), f"{itemname, price}"))
|
||||||
|
case {"itemname": itemname, "price": price, "amount": amount}:
|
||||||
|
temp_choices.append((itemname.replace(" ", "_"), f"{itemname}, {price} * {amount}"))
|
||||||
|
form.choices = temp_choices
|
||||||
|
if form.validate_on_submit():
|
||||||
|
pass # TODO
|
||||||
|
return render_template("receipts/confirm_items.html")
|
||||||
|
abort(403)
|
||||||
@ -5,16 +5,10 @@
|
|||||||
<h1>Sign In</h1>
|
<h1>Sign In</h1>
|
||||||
<form action="" method="post" novalidate>
|
<form action="" method="post" novalidate>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<p>
|
{{ wtf.form_field(form.email, class=form.email.render_kw["class"] or "form-control mb-3") }}
|
||||||
{{ wtf.form_field(form.email) }}
|
{{ wtf.form_field(form.password, class=form.password.render_kw["class"] or "form-control mb-3") }}
|
||||||
</p>
|
{{ wtf.form_field(form.remember_me, class=form.remember_me.render_kw["class"] or "mb-3") }}
|
||||||
<p>
|
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
|
||||||
{{ wtf.form_field(form.password) }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ wtf.form_field(form.remember_me) }}
|
|
||||||
</p>
|
|
||||||
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
|
|
||||||
</form>
|
</form>
|
||||||
<p>
|
<p>
|
||||||
Forgot Your Password?
|
Forgot Your Password?
|
||||||
13
backend/src/templates/auth/register.html
Normal file
13
backend/src/templates/auth/register.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<h1>Register</h1>
|
||||||
|
<form action="" method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ wtf.form_field(form.email, class=form.email.render_kw["class"] or "form-control mb-3") }}
|
||||||
|
{{ wtf.form_field(form.password, class=form.password.render_kw["class"] or "form-control mb-3") }}
|
||||||
|
{{ wtf.form_field(form.password2, class=form.password2.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 %}
|
||||||
126
backend/src/templates/base.html
Normal file
126
backend/src/templates/base.html
Normal 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 %}
|
||||||
7
backend/src/templates/errors/403.html
Normal file
7
backend/src/templates/errors/403.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<h1>You are not allowed to do this!</h1>
|
||||||
|
<h4>how dare you</h4>
|
||||||
|
<p><a href="{{ url_for('main.index') }}">Back</a></p>
|
||||||
|
{% endblock %}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<form action="" method="post">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{{ form.candidate_id(value=candidate.user) }}
|
||||||
|
{{ form.accept }}
|
||||||
|
{{ form.deny }}
|
||||||
|
</form>
|
||||||
@ -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 %}
|
||||||
@ -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 %}
|
||||||
@ -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>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
{% include 'establishment/new/new_establishment_form.html' %}
|
||||||
|
{% endblock %}
|
||||||
@ -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>
|
||||||
39
backend/src/templates/establishment/overview/overview.html
Normal file
39
backend/src/templates/establishment/overview/overview.html
Normal 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 %}
|
||||||
6
backend/src/templates/item/new/new_item.html
Normal file
6
backend/src/templates/item/new/new_item.html
Normal 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 %}
|
||||||
13
backend/src/templates/item/new/new_item_form.html
Normal file
13
backend/src/templates/item/new/new_item_form.html
Normal 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>
|
||||||
4
backend/src/templates/main/overview.html
Normal file
4
backend/src/templates/main/overview.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
{% endblock %}
|
||||||
9
backend/src/templates/receipts/confirm_items.html
Normal file
9
backend/src/templates/receipts/confirm_items.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<form action="" method="post" novalidate enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
10
backend/src/templates/receipts/upload.html
Normal file
10
backend/src/templates/receipts/upload.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<form action="" method="post" novalidate enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ wtf.form_field(form.pdfReceipt, class=form.pdfReceipt.render_kw["class"] or "form-control") }}
|
||||||
|
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
11
backend/src/templates/utils/form/_render_field.html
Normal file
11
backend/src/templates/utils/form/_render_field.html
Normal 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
Loading…
x
Reference in New Issue
Block a user