major: improvements and functions
Features: - Added page for an overview of establishments with a possibility to request a membership. - Added page for admins of an establishment to accept or deny those candidates. This is currently only usable via URL, the navigation to this site is not yet implemented in the HTML files. - Added page to add new establishments by providing a name. Improvements: - Better folder structure. - The establishment-specific overview can now be viewed with another URL, as well as some other pages. Bugfixes: - Seriously I don't know anymore what I fixed and what not. But it works just better now :) Future: - Angular has been added to separate the Flask-Backend with the frontend. Angular is currently not connected to the backend, but this will change in the future.
This commit is contained in:
parent
764738b20d
commit
2e33f9b5a8
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
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@ -10,7 +52,6 @@ __pycache__/
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@ -20,7 +61,6 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
@ -50,6 +90,7 @@ coverage.xml
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@ -63,7 +104,6 @@ db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
@ -72,6 +112,7 @@ instance/
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
@ -82,7 +123,9 @@ profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
#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__/
|
||||
|
||||
# Celery stuff
|
||||
@ -102,9 +160,7 @@ celerybeat.pid
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
.flaskenv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
@ -129,6 +185,157 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.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
|
||||
config.yaml
|
||||
scans.json
|
||||
@ -136,3 +343,8 @@ test.*
|
||||
*.db
|
||||
.vscode
|
||||
*.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 upgrade
|
||||
RUN apt update && apt -y upgrade
|
||||
RUN apt install -y libpq-dev gcc g++
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN python -m venv venv
|
||||
RUN venv/bin/pip install -r requirements.txt
|
||||
RUN venv/bin/pip install gunicorn
|
||||
RUN python -m pip install pipenv
|
||||
|
||||
COPY app app
|
||||
COPY migrations migrations
|
||||
COPY configs configs
|
||||
COPY run.py boot.sh ./
|
||||
COPY boot.sh ./
|
||||
RUN chmod +x boot.sh
|
||||
|
||||
COPY backend backend
|
||||
|
||||
ENV FLASK_APP run.py
|
||||
|
||||
RUN chown -R scan2kasse:scan2kasse ./
|
||||
USER scan2kasse
|
||||
RUN chown -R costhive:costhive ./
|
||||
USER costhive
|
||||
|
||||
RUN cd backend && pipenv install && pipenv install gunicorn && cd ..
|
||||
|
||||
EXPOSE 5000
|
||||
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,119 +0,0 @@
|
||||
from app import db, LOGGER
|
||||
from app.main.forms import NewItemForm
|
||||
from app.main import bp
|
||||
from app.models import AmountChange, Brand, Establishment, LoginToken, Item, PriceChange
|
||||
from app.utils import view_utils, database_utils
|
||||
from app.utils.routes_utils import render_custom_template as render_template
|
||||
from datetime import date
|
||||
from flask import abort, redirect, request, url_for
|
||||
from flask.json import jsonify
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
@bp.route('/')
|
||||
@bp.route('/index')
|
||||
def index():
|
||||
return render_template("base.html")
|
||||
|
||||
# @bp.route('/')
|
||||
# def test():
|
||||
# return "Hello World"
|
||||
|
||||
@bp.route('/overview', methods=['GET'])
|
||||
@login_required
|
||||
def get_report_from_user():
|
||||
if current_user.is_anonymous:
|
||||
abort(403)
|
||||
if 'month' in request.args:
|
||||
try:
|
||||
month = int(request.args['month'])
|
||||
except Exception as e:
|
||||
LOGGER.exception("")
|
||||
abort(400)
|
||||
else:
|
||||
if (month > 12 or month < 1):
|
||||
abort(400)
|
||||
LOGGER.info("Getting results.")
|
||||
results = database_utils.get_report(**request.args)
|
||||
LOGGER.debug(f"Results received.")
|
||||
# LOGGER.debug(str(results))
|
||||
if results:
|
||||
result_list = view_utils.group_results(results)
|
||||
else:
|
||||
result_list = []
|
||||
if request.content_type == "application/json":
|
||||
return jsonify(result_list)
|
||||
else:
|
||||
if "establishment" in request.args:
|
||||
return render_template("main/overview.html", results=result_list, establishment = Establishment.query.get(int(request.args['establishment'])))
|
||||
else:
|
||||
return render_template("main/overview.html", results=result_list)
|
||||
|
||||
@bp.route('/token_authorization')
|
||||
def token_authorization():
|
||||
LOGGER.debug("Token Login")
|
||||
if not request.json or 'login' not in request.json:
|
||||
LOGGER.debug("JSON not delivered or 'login' not in JSON")
|
||||
abort(400)
|
||||
if not LoginToken.query.filter_by(token=request.json['login']).first():
|
||||
LOGGER.debug(f"Token <{request.json['login']}> not recognized")
|
||||
abort(403)
|
||||
LOGGER.debug("Token accepted")
|
||||
return jsonify({}), 200
|
||||
|
||||
@bp.route('/token_insert', methods=['POST'])
|
||||
def insert():
|
||||
match request.json:
|
||||
case {'user': user, 'items': items, 'date': date}:
|
||||
failed = database_utils.insert_bought_items(user, items, date)
|
||||
case {'user': user, 'items': items}:
|
||||
failed = database_utils.insert_bought_items(user, items)
|
||||
case _:
|
||||
abort(400)
|
||||
if failed:
|
||||
return jsonify(failed), 400
|
||||
return jsonify({'inserted': True}), 201
|
||||
|
||||
@bp.route('/new_item', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new_item():
|
||||
if current_user.is_anonymous:
|
||||
abort(403)
|
||||
form=NewItemForm.new()
|
||||
if form.is_submitted():
|
||||
LOGGER.debug("submitted")
|
||||
if form.validate():
|
||||
LOGGER.debug("valid")
|
||||
else:
|
||||
LOGGER.debug(form.errors)
|
||||
if form.validate_on_submit():
|
||||
LOGGER.debug("valid form")
|
||||
brand = Brand.query.get(form.brand.data)
|
||||
new_item = Item(id = form.id.data, name = form.name.data, brand = brand.id, description = form.description.data)
|
||||
# if form.category.data:
|
||||
# category = Category.query.get(id = form.category.data)
|
||||
# new_item.Category = category
|
||||
new_item.PriceChange = [PriceChange(Item = new_item, date = date(2021, 12, 1), price = form.price_change.data)]
|
||||
if form.amount_change.data:
|
||||
new_item.AmountChange = [AmountChange(Item = new_item, date = date(2021, 12, 1), amount = form.amount_change.data)]
|
||||
db.session.add(new_item)
|
||||
db.session.commit()
|
||||
return redirect(url_for('main.index'))
|
||||
return render_template('main/new_item.html', form=form)
|
||||
|
||||
@bp.route('/overview/register_boughts', methods=['GET'])
|
||||
@login_required
|
||||
def check_unregistered_items():
|
||||
if current_user.is_anonymous or not request.args or 'establishment' not in request.args:
|
||||
abort(403)
|
||||
establishment = Establishment.query.get(int(request.args['establishment']))
|
||||
if current_user.id != establishment.owner:
|
||||
abort(403)
|
||||
results = database_utils.get_unregistered_and_register(establishment.id)
|
||||
if results:
|
||||
result_list = view_utils.group_results(results)
|
||||
else:
|
||||
result_list = []
|
||||
if request.content_type == "application/json":
|
||||
return jsonify(result_list)
|
||||
else:
|
||||
return render_template("main/overview.html", results=result_list)
|
||||
147
app/models.py
147
app/models.py
@ -1,147 +0,0 @@
|
||||
import jwt
|
||||
from app import db, login
|
||||
from datetime import date
|
||||
from flask import current_app
|
||||
from flask_login import UserMixin
|
||||
from time import time
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
item_category = db.Table("item_category",
|
||||
db.Column("item", db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue()),
|
||||
db.Column("category", db.ForeignKey("category.id"), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
)
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.BigInteger, primary_key=True)
|
||||
email = db.Column(db.String(255), nullable=False, unique=True)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
|
||||
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
|
||||
Bought = db.relationship("Bought", secondary="login_token",
|
||||
lazy='dynamic', overlaps="User,LoginToken")
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def get_reset_password_token(self, expires_in=600):
|
||||
return jwt.encode(
|
||||
{'reset_password': self.id, 'exp': time() + expires_in},
|
||||
current_app.config['SECRET_KEY'], algorithm='HS256')
|
||||
|
||||
@staticmethod
|
||||
def verify_reset_password_token(token):
|
||||
try:
|
||||
id = jwt.decode(token, current_app.config['SECRET_KEY'],
|
||||
algorithms=['HS256'])['reset_password']
|
||||
except:
|
||||
return
|
||||
return User.query.get(id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User {self.id} ({self.email})>"
|
||||
|
||||
class Establishment(db.Model):
|
||||
id = db.Column(db.BigInteger, primary_key=True)
|
||||
name = db.Column(db.String(64), nullable=False)
|
||||
owner = db.Column(db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
LoginToken = db.relationship("LoginToken", backref='Establishment', lazy='dynamic')
|
||||
Bought = db.relationship("Bought", secondary="login_token",
|
||||
lazy='dynamic', overlaps="Establishment,LoginToken,Bought")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Establishment {self.id} ({self.name})>"
|
||||
|
||||
class LoginToken(db.Model):
|
||||
user = db.Column(db.ForeignKey('user.id'), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
token = db.Column(db.String(15), nullable=False, unique=True)
|
||||
paid = db.Column(db.BigInteger, nullable=False, server_default=str(0))
|
||||
|
||||
Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"LoginToken {self.token}"
|
||||
|
||||
class Brand(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(32), nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Brand {self.id} ({self.name})>"
|
||||
|
||||
class Category(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(32), nullable=False, unique=True)
|
||||
|
||||
Item = db.relationship("Item", secondary=item_category, lazy="dynamic", back_populates="Category")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Category {self.id} ({self.name})>"
|
||||
|
||||
class Item(db.Model):
|
||||
id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
|
||||
name = db.Column(db.String(64), nullable=False)
|
||||
brand = db.Column(db.ForeignKey('brand.id'), nullable=False, server_onupdate=db.FetchedValue())
|
||||
description = db.Column(db.Text, nullable=False)
|
||||
|
||||
Category = db.relationship("Category", secondary=item_category, lazy="dynamic", back_populates="Item")
|
||||
Bought = db.relationship("Bought", backref='Item', lazy='dynamic')
|
||||
PriceChange = db.relationship("PriceChange", backref='Item', lazy='dynamic')
|
||||
AmountChange = db.relationship("AmountChange", backref='Item', lazy='dynamic')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Item {self.id} ({self.name})>"
|
||||
|
||||
class Bought(db.Model):
|
||||
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
date = db.Column(db.Date, primary_key=True)
|
||||
amount = db.Column(db.SmallInteger, nullable=False)
|
||||
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Bought Object>"
|
||||
|
||||
class PriceChange(db.Model):
|
||||
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
|
||||
price = db.Column(db.SmallInteger, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Price_Change {self.item} ({self.date})>"
|
||||
|
||||
class AmountChange(db.Model):
|
||||
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
|
||||
amount = db.Column(db.SmallInteger, nullable=False, server_default=str(1))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Amount_Change {self.item} ({self.date})>"
|
||||
|
||||
class Receipt(db.Model):
|
||||
id = db.Column(db.Numeric(precision=24, scale=0), primary_key=True, autoincrement=False)
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
from_user = db.Column(db.ForeignKey("login_token.token"), server_onupdate=db.FetchedValue())
|
||||
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
|
||||
|
||||
ItemReceipt = db.relationship("ItemReceipt", backref='Receipt', lazy='dynamic')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Receipt {self.id}>"
|
||||
|
||||
class ItemReceipt(db.Model):
|
||||
receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
item = db.Column(db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue())
|
||||
amount = db.Column(db.SmallInteger, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ItemReceipt {self.receipt}: {self.item}>"
|
||||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
@ -1,5 +0,0 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('receipts', __name__)
|
||||
|
||||
from app.receipts import forms, routes
|
||||
@ -1,125 +0,0 @@
|
||||
{% extends "bootstrap/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% if title %}
|
||||
{{ title }}
|
||||
{% else %}
|
||||
Scan2Kasse
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block metas %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href={{ url_for('static', filename="sidebars.css")}}>
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
<main>
|
||||
<div class="flex-shrink-0 p-3 bg-black bg-opacity-10 scrollbar-primary h-100 position-fixed" style="width: 280px;">
|
||||
<a href="/" class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom border-dark">
|
||||
<svg class="bi me-2" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
|
||||
<span class="fs-5 fw-semibold">Scan2Kasse</span>
|
||||
</a>
|
||||
<ul class="list-unstyled ps-0">
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true">
|
||||
Home (WIP)
|
||||
</button>
|
||||
<div class="collapse show" id="home-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">Übersicht</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Updates</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Reports</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false">
|
||||
Übersicht
|
||||
</button>
|
||||
<div class="collapse" id="dashboard-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
{% if establishments %}
|
||||
<!-- <li><a href="#" class="link-dark rounded">Allgemein</a></li> -->
|
||||
{% for establishment in establishments %}
|
||||
<li>
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#est_{{ establishment.id }}" aria-expanded="false">
|
||||
{{ establishment.name }}
|
||||
</button>
|
||||
<div class="collapse" id="est_{{ establishment.id }}">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="{{ url_for('main.get_report_from_user', establishment=establishment.id) }}" class="link-dark rounded">Übersicht</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<!-- <li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false">
|
||||
Orders
|
||||
</button>
|
||||
<div class="collapse" id="orders-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">New</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Processed</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Shipped</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Returned</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li> -->
|
||||
<li class="border-top border-dark my-3"></li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false">
|
||||
Account
|
||||
</button>
|
||||
<div class="collapse" id="account-collapse" style="">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<!-- <li><a href="#" class="link-dark rounded">New...</a></li>
|
||||
<li><a href="#" class="link-dark rounded">Settings</a></li> -->
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- <li><a href="#" class="link-dark rounded">Profile</a></li> -->
|
||||
<li><a href={{ url_for('auth.web_logout') }} class="link-dark rounded">Sign out</a></li>
|
||||
{% else %}
|
||||
<li><a href={{ url_for('auth.web_register') }} class="link-dark rounded">Register</a></li>
|
||||
<li><a href={{ url_for('auth.web_login') }} class="link-dark rounded">Sign in</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div style="width: 280px;"></div>
|
||||
<div class="container">
|
||||
<div class="row my-3"></div>
|
||||
<div class="row-md-3">
|
||||
{% block app_content %}{% endblock %}
|
||||
</div>
|
||||
<div class="row my-3"></div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
|
||||
{% endblock %}
|
||||
@ -1,17 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% import 'bootstrap/wtf.html' as wtf %}
|
||||
|
||||
{% block app_content %}
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ wtf.form_field(form.id, class=form.id.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.name, class=form.name.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.description, class=form.description.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.date, class=form.date.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.price_change, class=form.price_change.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.amount_change, class=form.amount_change.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.category, class=form.category.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.brand, class=form.brand.render_kw["class"] or "form-control mb-3") }}
|
||||
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@ -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
@ -10,8 +10,8 @@ class Config(object):
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace(
|
||||
'postgres://', 'postgresql://') or \
|
||||
(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_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
|
||||
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')}")
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
|
||||
@ -19,3 +19,4 @@ class Config(object):
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||
ADMINS = ['postmaster@wpgcommunity.net']
|
||||
POSTS_PER_PAGE = 10
|
||||
@ -8,8 +8,8 @@ Create Date: 2022-02-20 16:33:01.900378
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.utils.view_utils import selectable_price_per_amount_view, selectable_bought_with_prices_view
|
||||
from src import db
|
||||
from src.utils.view_utils import selectable_price_per_amount_view, selectable_bought_with_prices_view
|
||||
from sqlalchemy_utils import create_view
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
@ -20,6 +20,7 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.rename_table("item_receipt", "receipt_item")
|
||||
metadata = sa.MetaData()
|
||||
create_view('price_per_amount',
|
||||
selectable_price_per_amount_view(),
|
||||
@ -36,3 +37,4 @@ def downgrade():
|
||||
with db.engine.connect() as con:
|
||||
con.execute("DROP VIEW bought_with_prices;")
|
||||
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 app.models import *
|
||||
from src import create_app, db
|
||||
from src.models import Bought, Establishment, Item, LoginToken, Receipt, User
|
||||
|
||||
app = create_app()
|
||||
|
||||
@ -42,17 +42,21 @@ def create_app(config_class=Config):
|
||||
mail.init_app(app)
|
||||
migrate.init_app(app, db, render_as_batch=True)
|
||||
|
||||
from app.auth import bp as auth_bp
|
||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||
from app.errors import bp as errors_bp
|
||||
app.register_blueprint(errors_bp, url_prefix='/error')
|
||||
from app.main import bp as main_bp
|
||||
from src.main import bp as main_bp
|
||||
app.register_blueprint(main_bp)
|
||||
from app.api import bp as api_bp
|
||||
app.register_blueprint(api_bp, url_prefix="/api")
|
||||
from app.receipts import bp as receipts_bp
|
||||
app.register_blueprint(receipts_bp, url_prefix='/receipts')
|
||||
from src.auth import bp as auth_bp
|
||||
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
|
||||
|
||||
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 app.main import bp
|
||||
from app.models import LoginToken
|
||||
from app.utils import database_utils
|
||||
from src import LOGGER
|
||||
from src.api import bp
|
||||
from src.models.login_token import LoginToken
|
||||
from src.utils import database_utils
|
||||
from flask import abort, request
|
||||
from flask.json import jsonify
|
||||
|
||||
@bp.route('/auth')
|
||||
@bp.route('/')
|
||||
def token_authorization():
|
||||
LOGGER.debug("Token Login")
|
||||
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 app.utils.email import send_email
|
||||
from app.utils.routes_utils import render_custom_template as render_template
|
||||
from src.utils.email import send_email
|
||||
from src.utils.routes_utils import render_custom_template as render_template
|
||||
|
||||
|
||||
def send_password_reset_email(user):
|
||||
@ -1,4 +1,4 @@
|
||||
from app.models import User
|
||||
from src.models.user import User
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, PasswordField, StringField, SubmitField
|
||||
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
|
||||
@ -1,9 +1,9 @@
|
||||
from app import db
|
||||
from app.auth import bp
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ResetPasswordRequestForm
|
||||
from app.models import User
|
||||
from app.utils.routes_utils import render_custom_template as render_template
|
||||
from src import db
|
||||
from src.auth import bp
|
||||
from src.auth.email import send_password_reset_email
|
||||
from src.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ResetPasswordRequestForm
|
||||
from src.models.user import User
|
||||
from src.utils.routes_utils import render_custom_template as render_template
|
||||
from flask import flash, redirect, request, url_for
|
||||
from flask_login import current_user, login_user, logout_user
|
||||
from werkzeug.urls import url_parse
|
||||
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,5 +1,5 @@
|
||||
from app import db
|
||||
from app.errors import bp
|
||||
from src import db
|
||||
from src.errors import bp
|
||||
from flask import render_template
|
||||
|
||||
@bp.app_errorhandler(403)
|
||||
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,17 +1,18 @@
|
||||
from app.models import Brand, Category
|
||||
from src.models.brand import Brand
|
||||
from src.models.category import Category
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import DateField, FloatField, IntegerField, SelectField, SelectMultipleField, StringField, SubmitField
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
|
||||
class NewItemForm(FlaskForm):
|
||||
id = IntegerField("Product EAN", validators=[DataRequired()])
|
||||
name = StringField("Name", validators=[DataRequired()])
|
||||
description = StringField("Description", validators=[DataRequired()])
|
||||
date = DateField("Insert Date", validators=[DataRequired()])
|
||||
price_change = FloatField("Price", validators=[DataRequired()])
|
||||
amount_change = IntegerField("Amount", validators=[Optional()])
|
||||
category = SelectMultipleField("Categories", choices=[], validators=[Optional()])
|
||||
brand = SelectField("Brand", choices=[], validators=[DataRequired()])
|
||||
id = IntegerField("Product EAN", validators=[DataRequired()], render_kw={"class": "form-control"})
|
||||
name = StringField("Name", validators=[DataRequired()], render_kw={"class": "form-control"})
|
||||
description = StringField("Description", validators=[DataRequired()], render_kw={"class": "form-control"})
|
||||
date = DateField("Insert Date", validators=[DataRequired()], render_kw={"class": "form-control"})
|
||||
price_change = FloatField("Price", validators=[DataRequired()], render_kw={"class": "form-control"})
|
||||
amount_change = IntegerField("Amount", validators=[Optional()], render_kw={"class": "form-control"})
|
||||
category = SelectMultipleField("Categories", choices=[], validators=[Optional()], render_kw={"class": "form-control"})
|
||||
brand = SelectField("Brand", choices=[], validators=[DataRequired()], render_kw={"class": "form-control"})
|
||||
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
|
||||
|
||||
@classmethod
|
||||
@ -20,18 +21,3 @@ class NewItemForm(FlaskForm):
|
||||
form.category.choices = [(c.id, c.name) for c in Category.query.order_by("name").all()]
|
||||
form.brand.choices = [(b.id, b.name) for b in Brand.query.order_by("name").all()]
|
||||
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__)
|
||||
|
||||
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
|
||||
@ -14,3 +14,11 @@ class UploadReceiptForm(FlaskForm):
|
||||
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
|
||||
@ -1,11 +1,12 @@
|
||||
from app import db, LOGGER
|
||||
from app.receipts import bp
|
||||
from app.receipts.forms import CheckItemsForm, UploadReceiptForm
|
||||
from app.models import Receipt, LoginToken
|
||||
from app.utils.routes_utils import render_custom_template as render_template
|
||||
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
|
||||
from app.utils.pdf_receipt_parser import PDFReceipt
|
||||
|
||||
PDFDir = "./"
|
||||
|
||||
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 %}
|
||||
@ -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 %}
|
||||
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